diff --git a/.changeset/orange-mice-bet.md b/.changeset/orange-mice-bet.md new file mode 100644 index 000000000000..bb7f2032905c --- /dev/null +++ b/.changeset/orange-mice-bet.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't redirect in forks diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js index eb9c61885c9c..42d7a94f3b50 100644 --- a/packages/kit/src/core/sync/write_root.js +++ b/packages/kit/src/core/sync/write_root.js @@ -72,9 +72,12 @@ export function write_root(manifest_data, output) { ${ isSvelte5Plus() ? dedent` - let { stores, page, constructors, components = [], form, ${levels + let { stores, page, constructors, components = [], fork, form, ${levels .map((l) => `data_${l} = null`) .join(', ')} } = $props(); + if (browser) { + setContext('__sveltekit_fork', () => fork); + } ` : dedent` export let stores; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index f07164930983..89b5fa767151 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -282,8 +282,9 @@ const preload_tokens = new Set(); export let pending_invalidate; /** - * @type {Map} + * @type {Map}>} * A map of id -> query info with all queries that currently exist in the app. + * `forks` is a Map of every fork (or undefined when in the current world) and how often they're used in that context. */ export const query_map = new Map(); @@ -540,10 +541,21 @@ async function _preload_data(intent) { // resolve, bail rather than creating an orphan fork if (lc === load_cache && result.type === 'loaded') { try { - return svelte.fork(() => { - root.$set(result.props); - update(result.props.page); - }); + let fork = svelte.fork(() => {}); + if (!fork.run) { + // backwards compatibility for Svelte versions that don't have `fork.run` + fork.discard(); + return svelte.fork(() => { + root.$set({ ...result.props, fork }); + update(result.props.page); + }); + } else { + fork.run(() => { + root.$set({ ...result.props, fork }); + update(result.props.page); + }); + return fork; + } } catch { // if it errors, it's because the experimental flag isn't enabled } @@ -1855,6 +1867,16 @@ if (import.meta.hot) { }); } +/** + * @param {import('svelte').Fork} fork + * @param {string} location + */ +export async function redirect_fork(fork, location) { + if ((await load_cache?.fork) === fork && load_cache) { + load_cache.promise = Promise.resolve({ type: 'redirect', location }); + } +} + /** @typedef {(typeof PRELOAD_PRIORITIES)['hover'] | (typeof PRELOAD_PRIORITIES)['tap']} PreloadDataPriority */ function setup_preload() { diff --git a/packages/kit/src/runtime/client/remote-functions/query.svelte.js b/packages/kit/src/runtime/client/remote-functions/query.svelte.js index 196a959d25a7..44ec4703cc95 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -23,7 +23,7 @@ export function query(id) { } } - return create_remote_function(id, (cache_key, payload) => { + return create_remote_function(id, (cache_key, payload, forks) => { return new Query(cache_key, async () => { if (Object.hasOwn(remote_responses, cache_key)) { return remote_responses[cache_key]; @@ -31,7 +31,7 @@ export function query(id) { const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`; - const result = await remote_request(url); + const result = await remote_request(url, forks); return devalue.parse(result, app.decoders); }); }); diff --git a/packages/kit/src/runtime/client/remote-functions/shared.svelte.js b/packages/kit/src/runtime/client/remote-functions/shared.svelte.js index 2aecf25778fc..f2f1dbfbb349 100644 --- a/packages/kit/src/runtime/client/remote-functions/shared.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/shared.svelte.js @@ -2,16 +2,17 @@ /** @import { RemoteFunctionResponse } from 'types' */ /** @import { Query } from './query.svelte.js' */ import * as devalue from 'devalue'; -import { app, goto, query_map, remote_responses } from '../client.js'; +import { app, goto, redirect_fork, query_map, remote_responses } from '../client.js'; import { HttpError, Redirect } from '@sveltejs/kit/internal'; -import { tick } from 'svelte'; +import { getContext, tick } from 'svelte'; import { create_remote_key, stringify_remote_arg } from '../../shared.js'; /** * * @param {string} url + * @param {Map} forks */ -export async function remote_request(url) { +export async function remote_request(url, forks = new Map()) { const response = await fetch(url, { headers: { // TODO in future, when we support forking, we will likely need @@ -29,7 +30,27 @@ export async function remote_request(url) { const result = /** @type {RemoteFunctionResponse} */ (await response.json()); if (result.type === 'redirect') { - await goto(result.location); + if ( + forks.size === 0 || + Array.from(forks.keys()).some( + (fork) => + !fork || + !fork.isDiscarded || // isDiscarded et al was introduced later, do this for backwards compatibility + fork.isCommitted() + ) + ) { + // If this query is used in at least one non-forked context, + // it means it's part of the current world, therefore perform a regular redirect + await goto(result.location); + } else { + for (const fork of /** @type {MapIterator} */ (forks.keys())) { + if (!fork.isDiscarded()) { + redirect_fork(fork, result.location); + break; // there can only be one current fork + } + } + } + throw new Redirect(307, result.location); } @@ -43,10 +64,18 @@ export async function remote_request(url) { /** * Client-version of the `query`/`prerender`/`cache` function from `$app/server`. * @param {string} id - * @param {(key: string, args: string) => any} create + * @param {(key: string, args: string, forks: Map) => any} create */ export function create_remote_function(id, create) { return (/** @type {any} */ arg) => { + /** @type {import('svelte').Fork | undefined} */ + let fork = undefined; + try { + fork = getContext('__sveltekit_fork')?.(); + } catch { + // not called in a reactive context + } + const payload = stringify_remote_arg(arg, app.hooks.transport); const cache_key = create_remote_key(id, payload); let entry = query_map.get(cache_key); @@ -54,11 +83,22 @@ export function create_remote_function(id, create) { let tracking = true; try { $effect.pre(() => { - if (entry) entry.count++; + if (entry) { + entry.count++; + entry.forks.set(fork, (entry.forks.get(fork) ?? 0) + 1); + } return () => { const entry = query_map.get(cache_key); if (entry) { entry.count--; + + const fork_count = /** @type {number} */ (entry.forks.get(fork)) - 1; + if (fork_count === 0) { + entry.forks.delete(fork); + } else { + entry.forks.set(fork, fork_count); + } + void tick().then(() => { if (!entry.count && entry === query_map.get(cache_key)) { query_map.delete(cache_key); @@ -74,7 +114,8 @@ export function create_remote_function(id, create) { let resource = entry?.resource; if (!resource) { - resource = create(cache_key, payload); + const forks = new Map([[fork, 1]]); + resource = create(cache_key, payload, forks); Object.defineProperty(resource, '_key', { value: cache_key @@ -84,6 +125,7 @@ export function create_remote_function(id, create) { cache_key, (entry = { count: tracking ? 1 : 0, + forks, resource }) );