From 20d783c9ce93326618ea51bd8bc4aa1b7ded2cec Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 22 Nov 2025 00:00:31 +0100 Subject: [PATCH] fix: don't redirect in forks This prevents a redirect that a remote function could do from doing a navigation in a fork. Leverages Svelte's new forking methods from https://github.com/sveltejs/svelte/pull/17217 It's implemented by putting the context into fork, pulling it out on remote function invocation to check if we're in a fork (TODO what if you have it invoked elsewhere and you just await it in a fork context?), put that into a map, and pull it out of there when a redirect occurs to check if the remote function is only called in context of that fork, and if so instead of redirecting we tell the SvelteKit router that this new route will redirect elsewhere. In the future we could follow that redirect and run it in the fork, but this is good enough for now and ties nicely into the current SvelteKit router. Fixes #14935 --- .changeset/orange-mice-bet.md | 5 ++ packages/kit/src/core/sync/write_root.js | 5 +- packages/kit/src/runtime/client/client.js | 32 +++++++++-- .../client/remote-functions/query.svelte.js | 4 +- .../client/remote-functions/shared.svelte.js | 56 ++++++++++++++++--- 5 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 .changeset/orange-mice-bet.md 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 }) );