From 96fab14a8a1c8fed704672bbf07139415a78e986 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 19:41:31 -0400 Subject: [PATCH 01/11] fix: create separate cache entries for non-exported queries --- .changeset/soft-emus-fry.md | 5 +++++ .../kit/src/runtime/app/server/remote/form.js | 17 +++++++++++----- .../runtime/app/server/remote/prerender.js | 14 ++++++++++--- .../src/runtime/app/server/remote/query.js | 20 +++++++++++++++---- .../src/runtime/app/server/remote/shared.js | 18 +++++++++++------ .../kit/src/runtime/server/page/render.js | 14 +++++++++---- packages/kit/src/types/internal.d.ts | 1 + .../remote/query-non-exported/+page.svelte | 8 ++++++++ .../remote/query-non-exported/data.remote.ts | 8 ++++++++ packages/kit/test/apps/basics/test/test.js | 11 +++++++++- 10 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 .changeset/soft-emus-fry.md create mode 100644 packages/kit/test/apps/basics/src/routes/remote/query-non-exported/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/remote/query-non-exported/data.remote.ts diff --git a/.changeset/soft-emus-fry.md b/.changeset/soft-emus-fry.md new file mode 100644 index 000000000000..f276b263e5f6 --- /dev/null +++ b/.changeset/soft-emus-fry.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: create separate cache entries for non-exported queries diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 01e2c893df76..c45ff1d69adc 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -166,7 +166,14 @@ export function form(validate_or_fn, maybe_fn) { // We don't need to care about args or deduplicating calls, because uneval results are only relevant in full page reloads // where only one form submission is active at the same time if (!event.isRemoteRequest) { - (state.remote_data ??= {})[__.id] = output; + let cache = state.remote_cache?.get(__); + + if (cache === undefined) { + cache = {}; + (state.remote_cache ??= new Map()).set(__, cache); + } + + cache[''] = output; } return output; @@ -189,8 +196,8 @@ export function form(validate_or_fn, maybe_fn) { Object.defineProperty(instance, property, { get() { try { - const { remote_data } = get_request_store().state; - return remote_data?.[__.id]?.[property] ?? {}; + const { remote_cache } = get_request_store().state; + return remote_cache?.get(__)?.['']?.[property] ?? {}; } catch { return undefined; } @@ -201,8 +208,8 @@ export function form(validate_or_fn, maybe_fn) { Object.defineProperty(instance, 'result', { get() { try { - const { remote_data } = get_request_store().state; - return remote_data?.[__.id]?.result; + const { remote_cache } = get_request_store().state; + return remote_cache?.get(__)?.['']?.result; } catch { return undefined; } diff --git a/packages/kit/src/runtime/app/server/remote/prerender.js b/packages/kit/src/runtime/app/server/remote/prerender.js index c73fef4dbb4e..5046f4a96810 100644 --- a/packages/kit/src/runtime/app/server/remote/prerender.js +++ b/packages/kit/src/runtime/app/server/remote/prerender.js @@ -96,7 +96,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { if (!state.prerendering && !DEV && !event.isRemoteRequest) { try { - return await get_response(id, arg, state, async () => { + return await get_response(__, arg, state, async () => { // TODO adapters can provide prerendered data more efficiently than // fetching from the public internet const response = await fetch(new URL(url, event.url.origin).href); @@ -113,7 +113,15 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { // TODO can we redirect here? - (state.remote_data ??= {})[create_remote_cache_key(id, payload)] = prerendered.result; + let cache = state.remote_cache?.get(__); + + if (cache === undefined) { + cache = {}; + (state.remote_cache ??= new Map()).set(__, cache); + } + + cache[stringify_remote_arg(arg, state.transport)] = prerendered.result; + return parse_remote_response(prerendered.result, state.transport); }); } catch { @@ -125,7 +133,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { return /** @type {Promise} */ (state.prerendering.remote_responses.get(url)); } - const promise = get_response(id, arg, state, () => + const promise = get_response(__, arg, state, () => run_remote_function(event, state, false, arg, validate, fn) ); diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index bb36543f17fa..e6b7b72224a1 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -73,7 +73,7 @@ export function query(validate_or_fn, maybe_fn) { const { event, state } = get_request_store(); /** @type {Promise & Partial>} */ - const promise = get_response(__.id, arg, state, () => + const promise = get_response(__, arg, state, () => run_remote_function(event, state, false, arg, validate, fn) ); @@ -90,8 +90,20 @@ export function query(validate_or_fn, maybe_fn) { ); } - const cache_key = create_remote_cache_key(__.id, stringify_remote_arg(arg, state.transport)); - refreshes[cache_key] = (state.remote_data ??= {})[cache_key] = Promise.resolve(value); + console.log('setting', __.id, arg, value); + + let cache = state.remote_cache?.get(__); + + if (cache === undefined) { + cache = {}; + (state.remote_cache ??= new Map()).set(__, cache); + } + + const key = stringify_remote_arg(arg, state.transport); + + if (__.id) { + refreshes[__.id + '/' + key] = cache[key] = Promise.resolve(value); + } }; promise.refresh = () => { @@ -198,7 +210,7 @@ function batch(validate_or_fn, maybe_fn) { const { event, state } = get_request_store(); /** @type {Promise & Partial>} */ - const promise = get_response(__.id, arg, state, () => { + const promise = get_response(__, arg, state, () => { // Collect all the calls to the same query in the same macrotask, // then execute them as one backend request. return new Promise((resolve, reject) => { diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index c0c7c19a2be4..8a2c223e424c 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -1,9 +1,9 @@ /** @import { RequestEvent } from '@sveltejs/kit' */ -/** @import { ServerHooks, MaybePromise, RequestState } from 'types' */ +/** @import { ServerHooks, MaybePromise, RequestState, RemoteInfo } from 'types' */ import { parse } from 'devalue'; import { error } from '@sveltejs/kit'; import { with_request_store, get_request_store } from '@sveltejs/kit/internal/server'; -import { create_remote_cache_key, stringify_remote_arg } from '../../../shared.js'; +import { stringify_remote_arg } from '../../../shared.js'; /** * @param {any} validate_or_fn @@ -62,19 +62,25 @@ export function create_validator(validate_or_fn, maybe_fn) { * Also saves an uneval'ed version of the result for later HTML inlining for hydration. * * @template {MaybePromise} T - * @param {string} id + * @param {RemoteInfo} info * @param {any} arg * @param {RequestState} state * @param {() => Promise} get_result * @returns {Promise} */ -export async function get_response(id, arg, state, get_result) { +export async function get_response(info, arg, state, get_result) { // wait a beat, in case `myQuery().set(...)` is immediately called // eslint-disable-next-line @typescript-eslint/await-thenable await 0; - const cache_key = create_remote_cache_key(id, stringify_remote_arg(arg, state.transport)); - return ((state.remote_data ??= {})[cache_key] ??= get_result()); + let cache = state.remote_cache?.get(info); + + if (cache === undefined) { + cache = {}; + (state.remote_cache ??= new Map()).set(info, cache); + } + + return (cache[stringify_remote_arg(arg, state.transport)] ??= get_result()); } /** diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 90fe3aefe3b0..484c3e2e8a0c 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -467,16 +467,22 @@ export async function render_response({ args.push(`{\n${indent}\t${hydrate.join(`,\n${indent}\t`)}\n${indent}}`); } - const { remote_data } = event_state; + const { remote_cache } = event_state; let serialized_remote_data = ''; - if (remote_data) { + if (remote_cache) { /** @type {Record} */ const remote = {}; - for (const key in remote_data) { - remote[key] = await remote_data[key]; + for (const [info, cache] of remote_cache) { + // remote functions without an `id` aren't exported, and thus + // cannot be called from the client + if (!info.id) continue; + + for (const key in cache) { + remote[key ? info.id + '/' + key : info.id] = await cache[key]; + } } // TODO this is repeated in a few places — dedupe it diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index c1167bd79e8d..b64e93bb34b7 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -597,6 +597,7 @@ export interface RequestState { record_span: RecordSpan; }; form_instances?: Map; + remote_cache?: Map>>; remote_data?: Record>; refreshes?: Record>; is_endpoint_request?: boolean; diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-non-exported/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/query-non-exported/+page.svelte new file mode 100644 index 000000000000..5fc45d97d451 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/query-non-exported/+page.svelte @@ -0,0 +1,8 @@ + + + +{#await total() then t} +

{t}

+{/await} diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-non-exported/data.remote.ts b/packages/kit/test/apps/basics/src/routes/remote/query-non-exported/data.remote.ts new file mode 100644 index 000000000000..f444537b3892 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/query-non-exported/data.remote.ts @@ -0,0 +1,8 @@ +import { query } from '$app/server'; + +const one = query(() => 1); +const two = query(() => 2); + +export const total = query(async () => { + return (await one()) + (await two()); +}); diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 799220608522..1382d5a57dd5 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1600,7 +1600,7 @@ test.describe('getRequestEvent', () => { }); }); -test.describe('remote functions', () => { +test.describe.only('remote functions', () => { test('query returns correct data', async ({ page, javaScriptEnabled }) => { await page.goto('/remote'); await expect(page.locator('#echo-result')).toHaveText('Hello world'); @@ -1633,6 +1633,15 @@ test.describe('remote functions', () => { await expect(page.locator('#redirected')).toHaveText('redirected'); }); + test('non-exported queries do not clobber each other', async ({ page, javaScriptEnabled }) => { + // TODO remove once async SSR exists + if (!javaScriptEnabled) return; + + await page.goto('/remote/query-non-exported'); + + await expect(page.locator('h1')).toHaveText('3'); + }); + test('form works', async ({ page, javaScriptEnabled }) => { await page.goto('/remote/form'); From 29d4703c2d13cace37f458fcc68d77a66993d345 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 20:00:19 -0400 Subject: [PATCH 02/11] fix --- packages/kit/src/runtime/app/server/remote/form.js | 2 +- packages/kit/src/runtime/app/server/remote/query.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index c45ff1d69adc..99f6221d4f91 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -173,7 +173,7 @@ export function form(validate_or_fn, maybe_fn) { (state.remote_cache ??= new Map()).set(__, cache); } - cache[''] = output; + cache[''] ??= output; } return output; diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index e6b7b72224a1..c2556a034ed7 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -90,8 +90,6 @@ export function query(validate_or_fn, maybe_fn) { ); } - console.log('setting', __.id, arg, value); - let cache = state.remote_cache?.get(__); if (cache === undefined) { From d68689e3f8f2cd86e059d09df71087ed65d008c7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 20:01:52 -0400 Subject: [PATCH 03/11] remove --- packages/kit/src/types/internal.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index b64e93bb34b7..0e32d76783c9 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -598,7 +598,6 @@ export interface RequestState { }; form_instances?: Map; remote_cache?: Map>>; - remote_data?: Record>; refreshes?: Record>; is_endpoint_request?: boolean; } From ea1975cc223040ebd6bf2ada0357a4dbc9240bd1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 20:02:07 -0400 Subject: [PATCH 04/11] rename --- packages/kit/src/runtime/app/server/remote/form.js | 8 ++++---- packages/kit/src/runtime/app/server/remote/prerender.js | 4 ++-- packages/kit/src/runtime/app/server/remote/query.js | 4 ++-- packages/kit/src/runtime/app/server/remote/shared.js | 4 ++-- packages/kit/src/runtime/server/page/render.js | 2 +- packages/kit/src/types/internal.d.ts | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 99f6221d4f91..7cc72b249e53 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -166,11 +166,11 @@ export function form(validate_or_fn, maybe_fn) { // We don't need to care about args or deduplicating calls, because uneval results are only relevant in full page reloads // where only one form submission is active at the same time if (!event.isRemoteRequest) { - let cache = state.remote_cache?.get(__); + let cache = state.remote_data?.get(__); if (cache === undefined) { cache = {}; - (state.remote_cache ??= new Map()).set(__, cache); + (state.remote_data ??= new Map()).set(__, cache); } cache[''] ??= output; @@ -196,7 +196,7 @@ export function form(validate_or_fn, maybe_fn) { Object.defineProperty(instance, property, { get() { try { - const { remote_cache } = get_request_store().state; + const { remote_data: remote_cache } = get_request_store().state; return remote_cache?.get(__)?.['']?.[property] ?? {}; } catch { return undefined; @@ -208,7 +208,7 @@ export function form(validate_or_fn, maybe_fn) { Object.defineProperty(instance, 'result', { get() { try { - const { remote_cache } = get_request_store().state; + const { remote_data: remote_cache } = get_request_store().state; return remote_cache?.get(__)?.['']?.result; } catch { return undefined; diff --git a/packages/kit/src/runtime/app/server/remote/prerender.js b/packages/kit/src/runtime/app/server/remote/prerender.js index 5046f4a96810..6ea4932d13c9 100644 --- a/packages/kit/src/runtime/app/server/remote/prerender.js +++ b/packages/kit/src/runtime/app/server/remote/prerender.js @@ -113,11 +113,11 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { // TODO can we redirect here? - let cache = state.remote_cache?.get(__); + let cache = state.remote_data?.get(__); if (cache === undefined) { cache = {}; - (state.remote_cache ??= new Map()).set(__, cache); + (state.remote_data ??= new Map()).set(__, cache); } cache[stringify_remote_arg(arg, state.transport)] = prerendered.result; diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index c2556a034ed7..66a268b27e6d 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -90,11 +90,11 @@ export function query(validate_or_fn, maybe_fn) { ); } - let cache = state.remote_cache?.get(__); + let cache = state.remote_data?.get(__); if (cache === undefined) { cache = {}; - (state.remote_cache ??= new Map()).set(__, cache); + (state.remote_data ??= new Map()).set(__, cache); } const key = stringify_remote_arg(arg, state.transport); diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index 8a2c223e424c..c80366b7f89a 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -73,11 +73,11 @@ export async function get_response(info, arg, state, get_result) { // eslint-disable-next-line @typescript-eslint/await-thenable await 0; - let cache = state.remote_cache?.get(info); + let cache = state.remote_data?.get(info); if (cache === undefined) { cache = {}; - (state.remote_cache ??= new Map()).set(info, cache); + (state.remote_data ??= new Map()).set(info, cache); } return (cache[stringify_remote_arg(arg, state.transport)] ??= get_result()); diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 484c3e2e8a0c..bc46fcd12535 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -467,7 +467,7 @@ export async function render_response({ args.push(`{\n${indent}\t${hydrate.join(`,\n${indent}\t`)}\n${indent}}`); } - const { remote_cache } = event_state; + const { remote_data: remote_cache } = event_state; let serialized_remote_data = ''; diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 0e32d76783c9..a9203cf1f94b 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -597,7 +597,7 @@ export interface RequestState { record_span: RecordSpan; }; form_instances?: Map; - remote_cache?: Map>>; + remote_data?: Map>>; refreshes?: Record>; is_endpoint_request?: boolean; } From e08f3d4c0098db49f0c75fffbde10b7b0047c22d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 20:08:04 -0400 Subject: [PATCH 05/11] DRY out --- .../kit/src/runtime/app/server/remote/form.js | 11 ++--------- .../src/runtime/app/server/remote/prerender.js | 8 ++------ .../kit/src/runtime/app/server/remote/query.js | 9 ++------- .../kit/src/runtime/app/server/remote/shared.js | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 7cc72b249e53..b0cba37fd0bd 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -3,7 +3,7 @@ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ import { get_request_store } from '@sveltejs/kit/internal/server'; import { DEV } from 'esm-env'; -import { run_remote_function } from './shared.js'; +import { get_cache, run_remote_function } from './shared.js'; import { convert_formdata, flatten_issues } from '../../../utils.js'; /** @@ -166,14 +166,7 @@ export function form(validate_or_fn, maybe_fn) { // We don't need to care about args or deduplicating calls, because uneval results are only relevant in full page reloads // where only one form submission is active at the same time if (!event.isRemoteRequest) { - let cache = state.remote_data?.get(__); - - if (cache === undefined) { - cache = {}; - (state.remote_data ??= new Map()).set(__, cache); - } - - cache[''] ??= output; + get_cache(__, state)[''] ??= output; } return output; diff --git a/packages/kit/src/runtime/app/server/remote/prerender.js b/packages/kit/src/runtime/app/server/remote/prerender.js index 6ea4932d13c9..2593902c6e1e 100644 --- a/packages/kit/src/runtime/app/server/remote/prerender.js +++ b/packages/kit/src/runtime/app/server/remote/prerender.js @@ -8,6 +8,7 @@ import { create_remote_cache_key, stringify, stringify_remote_arg } from '../../ import { app_dir, base } from '__sveltekit/paths'; import { create_validator, + get_cache, get_response, parse_remote_response, run_remote_function @@ -113,12 +114,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { // TODO can we redirect here? - let cache = state.remote_data?.get(__); - - if (cache === undefined) { - cache = {}; - (state.remote_data ??= new Map()).set(__, cache); - } + const cache = get_cache(__, state); cache[stringify_remote_arg(arg, state.transport)] = prerendered.result; diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 66a268b27e6d..6454319807ba 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -4,7 +4,7 @@ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_cache_key, stringify_remote_arg } from '../../../shared.js'; import { prerendering } from '__sveltekit/environment'; -import { create_validator, get_response, run_remote_function } from './shared.js'; +import { create_validator, get_cache, get_response, run_remote_function } from './shared.js'; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. @@ -90,12 +90,7 @@ export function query(validate_or_fn, maybe_fn) { ); } - let cache = state.remote_data?.get(__); - - if (cache === undefined) { - cache = {}; - (state.remote_data ??= new Map()).set(__, cache); - } + const cache = get_cache(__, state); const key = stringify_remote_arg(arg, state.transport); diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index c80366b7f89a..eb83dc004917 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -147,3 +147,18 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali const validated = await with_request_store({ event: cleansed, state }, () => validate(arg)); return with_request_store({ event: cleansed, state }, () => fn(validated)); } + +/** + * @param {RemoteInfo} info + * @param {RequestState} state + */ +export function get_cache(info, state) { + let cache = state.remote_data?.get(info); + + if (cache === undefined) { + cache = {}; + (state.remote_data ??= new Map()).set(info, cache); + } + + return cache; +} From f62f7ca012ef199f092be90fc1196a87669c9bd1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 20:09:26 -0400 Subject: [PATCH 06/11] more --- packages/kit/src/runtime/app/server/remote/form.js | 6 ++---- packages/kit/src/runtime/app/server/remote/shared.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index b0cba37fd0bd..1fe3390b18c5 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -189,8 +189,7 @@ export function form(validate_or_fn, maybe_fn) { Object.defineProperty(instance, property, { get() { try { - const { remote_data: remote_cache } = get_request_store().state; - return remote_cache?.get(__)?.['']?.[property] ?? {}; + return get_cache(__)?.['']?.[property] ?? {}; } catch { return undefined; } @@ -201,8 +200,7 @@ export function form(validate_or_fn, maybe_fn) { Object.defineProperty(instance, 'result', { get() { try { - const { remote_data: remote_cache } = get_request_store().state; - return remote_cache?.get(__)?.['']?.result; + return get_cache(__)?.['']?.result; } catch { return undefined; } diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index eb83dc004917..2d4179934ddc 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -152,7 +152,7 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali * @param {RemoteInfo} info * @param {RequestState} state */ -export function get_cache(info, state) { +export function get_cache(info, state = get_request_store().state) { let cache = state.remote_data?.get(info); if (cache === undefined) { From e82d6c54fa39ac384ab68eb8eccae7bdc40c58ae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 20:16:23 -0400 Subject: [PATCH 07/11] tidy up --- .../runtime/app/server/remote/prerender.js | 31 ++++++++++--------- .../src/runtime/app/server/remote/shared.js | 7 +---- packages/kit/test/apps/basics/test/test.js | 2 +- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/prerender.js b/packages/kit/src/runtime/app/server/remote/prerender.js index 2593902c6e1e..8f2c6bcff931 100644 --- a/packages/kit/src/runtime/app/server/remote/prerender.js +++ b/packages/kit/src/runtime/app/server/remote/prerender.js @@ -98,27 +98,28 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { if (!state.prerendering && !DEV && !event.isRemoteRequest) { try { return await get_response(__, arg, state, async () => { + const key = stringify_remote_arg(arg, state.transport); + const cache = get_cache(__, state); + // TODO adapters can provide prerendered data more efficiently than // fetching from the public internet - const response = await fetch(new URL(url, event.url.origin).href); - - if (!response.ok) { - throw new Error('Prerendered response not found'); - } + const promise = (cache[key] ??= fetch(new URL(url, event.url.origin).href).then( + async (response) => { + if (!response.ok) { + throw new Error('Prerendered response not found'); + } - const prerendered = await response.json(); + const prerendered = await response.json(); - if (prerendered.type === 'error') { - error(prerendered.status, prerendered.error); - } - - // TODO can we redirect here? - - const cache = get_cache(__, state); + if (prerendered.type === 'error') { + error(prerendered.status, prerendered.error); + } - cache[stringify_remote_arg(arg, state.transport)] = prerendered.result; + return prerendered.result; + } + )); - return parse_remote_response(prerendered.result, state.transport); + return parse_remote_response(await promise, state.transport); }); } catch { // not available prerendered, fallback to normal function diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index 2d4179934ddc..48afb882b21b 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -73,12 +73,7 @@ export async function get_response(info, arg, state, get_result) { // eslint-disable-next-line @typescript-eslint/await-thenable await 0; - let cache = state.remote_data?.get(info); - - if (cache === undefined) { - cache = {}; - (state.remote_data ??= new Map()).set(info, cache); - } + const cache = get_cache(info, state); return (cache[stringify_remote_arg(arg, state.transport)] ??= get_result()); } diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 1382d5a57dd5..ff54734dff48 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1600,7 +1600,7 @@ test.describe('getRequestEvent', () => { }); }); -test.describe.only('remote functions', () => { +test.describe('remote functions', () => { test('query returns correct data', async ({ page, javaScriptEnabled }) => { await page.goto('/remote'); await expect(page.locator('#echo-result')).toHaveText('Hello world'); From 692dbcaba7d8460c114d631662748a4bb94fb25a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 20:20:10 -0400 Subject: [PATCH 08/11] unused --- packages/kit/src/runtime/app/server/remote/prerender.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/app/server/remote/prerender.js b/packages/kit/src/runtime/app/server/remote/prerender.js index 8f2c6bcff931..8c19732bc52c 100644 --- a/packages/kit/src/runtime/app/server/remote/prerender.js +++ b/packages/kit/src/runtime/app/server/remote/prerender.js @@ -4,7 +4,7 @@ import { error, json } from '@sveltejs/kit'; import { DEV } from 'esm-env'; import { get_request_store } from '@sveltejs/kit/internal/server'; -import { create_remote_cache_key, stringify, stringify_remote_arg } from '../../../shared.js'; +import { stringify, stringify_remote_arg } from '../../../shared.js'; import { app_dir, base } from '__sveltekit/paths'; import { create_validator, From b4ab406433dcf8ba867260e671145f74e7a2b18b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 21 Sep 2025 20:23:01 -0400 Subject: [PATCH 09/11] update docs to use a non-exported query instead of caching on locals --- .../docs/20-core-concepts/60-remote-functions.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index a1c63dc7b124..a0a1a47eaa86 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -995,13 +995,13 @@ export const getProfile = query(async () => { }; }); -// this function could be called from multiple places -function getUser() { - const { cookies, locals } = getRequestEvent(); +// this query could be called from multiple places, but +// the function will only run once per request +const getUser = query(() => { + const { cookies } = getRequestEvent(); - locals.userPromise ??= findUser(cookies.get('session_id')); - return await locals.userPromise; -} + return await findUser(cookies.get('session_id')); +}); ``` Note that some properties of `RequestEvent` are different inside remote functions. There are no `params` or `route.id`, and you cannot set headers (other than writing cookies, and then only inside `form` and `command` functions), and `url.pathname` is always `/` (since the path that’s actually being requested by the client is purely an implementation detail). From e96ef0fe451b74fa3727cf2be8cfbafae29111fc Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:22:28 +0200 Subject: [PATCH 10/11] Update .changeset/soft-emus-fry.md Co-authored-by: Tee Ming --- .changeset/soft-emus-fry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/soft-emus-fry.md b/.changeset/soft-emus-fry.md index f276b263e5f6..7a92ae52e5da 100644 --- a/.changeset/soft-emus-fry.md +++ b/.changeset/soft-emus-fry.md @@ -2,4 +2,4 @@ '@sveltejs/kit': patch --- -fix: create separate cache entries for non-exported queries +fix: create separate cache entries for non-exported remote function queries From 54444c36e165f9501728903a5e794d93643990da Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:24:36 +0200 Subject: [PATCH 11/11] Update packages/kit/src/runtime/app/server/remote/query.js --- packages/kit/src/runtime/app/server/remote/query.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 6454319807ba..2974f7228271 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -91,7 +91,6 @@ export function query(validate_or_fn, maybe_fn) { } const cache = get_cache(__, state); - const key = stringify_remote_arg(arg, state.transport); if (__.id) {