From d6294f703670256b3a83175e53a816aafe9e43c7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 15 Sep 2025 13:02:33 -0400 Subject: [PATCH 1/6] fix: allow remote functions to return custom types serialized with `transport` hooks --- .changeset/deep-points-peel.md | 5 ++ .../kit/src/runtime/server/page/render.js | 54 ++++++++++--------- .../src/routes/remote/transport/+page.svelte | 8 +++ .../routes/remote/transport/data.remote.ts | 4 ++ .../kit/test/apps/basics/test/client.test.js | 7 +++ 5 files changed, 52 insertions(+), 26 deletions(-) create mode 100644 .changeset/deep-points-peel.md create mode 100644 packages/kit/test/apps/basics/src/routes/remote/transport/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/remote/transport/data.remote.ts diff --git a/.changeset/deep-points-peel.md b/.changeset/deep-points-peel.md new file mode 100644 index 000000000000..b93f6d332117 --- /dev/null +++ b/.changeset/deep-points-peel.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: allow remote functions to return custom types serialized with `transport` hooks diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 2ad4908508d0..b9a9ffc4669c 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -408,29 +408,6 @@ export async function render_response({ }`); } - const { remote_data } = event_state; - - if (remote_data) { - /** @type {Record} */ - const remote = {}; - - for (const key in remote_data) { - remote[key] = await remote_data[key]; - } - - // TODO this is repeated in a few places — dedupe it - const replacer = (/** @type {any} */ thing) => { - for (const key in options.hooks.transport) { - const encoded = options.hooks.transport[key].encode(thing); - if (encoded) { - return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`; - } - } - }; - - properties.push(`data: ${devalue.uneval(remote, replacer)}`); - } - // create this before declaring `data`, which may contain references to `${global}` blocks.push(`${global} = { ${properties.join(',\n\t\t\t\t\t\t')} @@ -482,20 +459,45 @@ export async function render_response({ args.push(`{\n${indent}\t${hydrate.join(`,\n${indent}\t`)}\n${indent}}`); } + const { remote_data } = event_state; + + let serialized_remote_data = ''; + + if (remote_data) { + /** @type {Record} */ + const remote = {}; + + for (const key in remote_data) { + remote[key] = await remote_data[key]; + } + + // TODO this is repeated in a few places — dedupe it + const replacer = (/** @type {any} */ thing) => { + for (const key in options.hooks.transport) { + const encoded = options.hooks.transport[key].encode(thing); + if (encoded) { + return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`; + } + } + }; + + serialized_remote_data = `${global}.data = ${devalue.uneval(remote, replacer)};\n\n\t\t\t\t\t\t`; + } + // `client.app` is a proxy for `bundleStrategy === 'split'` const boot = client.inline ? `${client.inline.script} - __sveltekit_${options.version_hash}.app.start(${args.join(', ')});` + ${serialized_remote_data}${global}.app.start(${args.join(', ')});` : client.app ? `Promise.all([ import(${s(prefixed(client.start))}), import(${s(prefixed(client.app))}) ]).then(([kit, app]) => { - kit.start(app, ${args.join(', ')}); + ${serialized_remote_data}kit.start(app, ${args.join(', ')}); });` : `import(${s(prefixed(client.start))}).then((app) => { - app.start(${args.join(', ')}) + ${serialized_remote_data}app.start(${args.join(', ')}) });`; if (load_env_eagerly) { diff --git a/packages/kit/test/apps/basics/src/routes/remote/transport/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/transport/+page.svelte new file mode 100644 index 000000000000..023c95be9909 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/transport/+page.svelte @@ -0,0 +1,8 @@ + + + +{#await greeting() then x} +

{x.bar()}

+{/await} diff --git a/packages/kit/test/apps/basics/src/routes/remote/transport/data.remote.ts b/packages/kit/test/apps/basics/src/routes/remote/transport/data.remote.ts new file mode 100644 index 000000000000..78261104279d --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/transport/data.remote.ts @@ -0,0 +1,4 @@ +import { query } from '$app/server'; +import { Foo } from '$lib'; + +export const greeting = query(() => new Foo('hello from remote function')); diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 0d1c52235818..2554c1bfa22e 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -2093,4 +2093,11 @@ test.describe('remote functions', () => { await page.waitForTimeout(100); // allow all requests to finish expect(request_count).toBe(1); }); + + // TODO ditto + test.only('query works with transport', async ({ page }) => { + await page.goto('/remote/transport'); + + await expect(page.locator('h1')).toHaveText('hello from remote function!'); + }); }); From ea230a0770e551aa3ce3e28aabd2fcf3972f4d1f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 15 Sep 2025 13:04:46 -0400 Subject: [PATCH 2/6] oops --- packages/kit/test/apps/basics/test/client.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 2554c1bfa22e..b5d9554e27ad 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -2095,7 +2095,7 @@ test.describe('remote functions', () => { }); // TODO ditto - test.only('query works with transport', async ({ page }) => { + test('query works with transport', async ({ page }) => { await page.goto('/remote/transport'); await expect(page.locator('h1')).toHaveText('hello from remote function!'); From a5636fefb3320c4d32028c1f7966aa7f3af8dc55 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 15 Sep 2025 13:06:40 -0400 Subject: [PATCH 3/6] eliminate any potential race conditions --- packages/kit/src/runtime/client/client.js | 6 ------ .../runtime/client/remote-functions/form.svelte.js | 11 ++--------- .../client/remote-functions/prerender.svelte.js | 4 ++-- .../runtime/client/remote-functions/query.svelte.js | 6 +++--- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 88424188f2dd..6e230d0bc14a 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -189,12 +189,6 @@ let target; /** @type {import('./types.js').SvelteKitApp} */ export let app; -/** @type {Record} */ -// we have to conditionally access the properties of `__SVELTEKIT_PAYLOAD__` -// because it will be `undefined` when users import the exports from this module. -// It's only defined when the server renders a page. -export const remote_responses = __SVELTEKIT_PAYLOAD__?.data ?? {}; - /** @type {Array<((url: URL) => boolean)>} */ const invalidated = []; diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index 3921ebc8efd3..cd3ddc13ec04 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -5,14 +5,7 @@ import { app_dir, base } from '__sveltekit/paths'; import * as devalue from 'devalue'; import { DEV } from 'esm-env'; import { HttpError } from '@sveltejs/kit/internal'; -import { - app, - remote_responses, - started, - _goto, - set_nearest_error_page, - invalidateAll -} from '../client.js'; +import { app, started, _goto, set_nearest_error_page, invalidateAll } from '../client.js'; import { tick } from 'svelte'; import { refresh_queries, release_overrides } from './shared.svelte.js'; @@ -32,7 +25,7 @@ export function form(id) { const action = '?/remote=' + encodeURIComponent(action_id); /** @type {any} */ - let result = $state.raw(started ? undefined : remote_responses[action_id]); + let result = $state.raw(started ? undefined : __SVELTEKIT_PAYLOAD__.data?.[action_id]); /** @type {number} */ let pending_count = $state(0); diff --git a/packages/kit/src/runtime/client/remote-functions/prerender.svelte.js b/packages/kit/src/runtime/client/remote-functions/prerender.svelte.js index 7a919f899da3..4ecfc280b8d3 100644 --- a/packages/kit/src/runtime/client/remote-functions/prerender.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/prerender.svelte.js @@ -3,7 +3,7 @@ import { app_dir, base } from '__sveltekit/paths'; import { version } from '__sveltekit/environment'; import * as devalue from 'devalue'; import { DEV } from 'esm-env'; -import { app, remote_responses, started } from '../client.js'; +import { app, started } from '../client.js'; import { create_remote_function, remote_request } from './shared.svelte.js'; // Initialize Cache API for prerender functions @@ -118,7 +118,7 @@ export function prerender(id) { return create_remote_function(id, (cache_key, payload) => { return new Prerender(async () => { if (!started) { - const result = remote_responses[cache_key]; + const result = __SVELTEKIT_PAYLOAD__.data?.[cache_key]; if (result) { return result; } 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 4cc9e9e156d4..60dde3fa4051 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -1,7 +1,7 @@ /** @import { RemoteQueryFunction } from '@sveltejs/kit' */ /** @import { RemoteFunctionResponse } from 'types' */ import { app_dir, base } from '__sveltekit/paths'; -import { app, goto, remote_responses, started } from '../client.js'; +import { app, goto, started } from '../client.js'; import { tick } from 'svelte'; import { create_remote_function, remote_request } from './shared.svelte.js'; import * as devalue from 'devalue'; @@ -15,7 +15,7 @@ export function query(id) { return create_remote_function(id, (cache_key, payload) => { return new Query(cache_key, async () => { if (!started) { - const result = remote_responses[cache_key]; + const result = __SVELTEKIT_PAYLOAD__.data?.[cache_key]; if (result) { return result; } @@ -39,7 +39,7 @@ export function query_batch(id) { return create_remote_function(id, (cache_key, payload) => { return new Query(cache_key, () => { if (!started) { - const result = remote_responses[cache_key]; + const result = __SVELTEKIT_PAYLOAD__.data?.[cache_key]; if (result) { return result; } From d1e5cf7d0ca1b8b48991d5644d8f2d6aa257efe9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 15 Sep 2025 17:32:00 -0400 Subject: [PATCH 4/6] Revert "eliminate any potential race conditions" This reverts commit a5636fefb3320c4d32028c1f7966aa7f3af8dc55. --- packages/kit/src/runtime/client/client.js | 6 ++++++ .../runtime/client/remote-functions/form.svelte.js | 11 +++++++++-- .../client/remote-functions/prerender.svelte.js | 4 ++-- .../runtime/client/remote-functions/query.svelte.js | 6 +++--- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 6e230d0bc14a..88424188f2dd 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -189,6 +189,12 @@ let target; /** @type {import('./types.js').SvelteKitApp} */ export let app; +/** @type {Record} */ +// we have to conditionally access the properties of `__SVELTEKIT_PAYLOAD__` +// because it will be `undefined` when users import the exports from this module. +// It's only defined when the server renders a page. +export const remote_responses = __SVELTEKIT_PAYLOAD__?.data ?? {}; + /** @type {Array<((url: URL) => boolean)>} */ const invalidated = []; diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index cd3ddc13ec04..3921ebc8efd3 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -5,7 +5,14 @@ import { app_dir, base } from '__sveltekit/paths'; import * as devalue from 'devalue'; import { DEV } from 'esm-env'; import { HttpError } from '@sveltejs/kit/internal'; -import { app, started, _goto, set_nearest_error_page, invalidateAll } from '../client.js'; +import { + app, + remote_responses, + started, + _goto, + set_nearest_error_page, + invalidateAll +} from '../client.js'; import { tick } from 'svelte'; import { refresh_queries, release_overrides } from './shared.svelte.js'; @@ -25,7 +32,7 @@ export function form(id) { const action = '?/remote=' + encodeURIComponent(action_id); /** @type {any} */ - let result = $state.raw(started ? undefined : __SVELTEKIT_PAYLOAD__.data?.[action_id]); + let result = $state.raw(started ? undefined : remote_responses[action_id]); /** @type {number} */ let pending_count = $state(0); diff --git a/packages/kit/src/runtime/client/remote-functions/prerender.svelte.js b/packages/kit/src/runtime/client/remote-functions/prerender.svelte.js index 4ecfc280b8d3..7a919f899da3 100644 --- a/packages/kit/src/runtime/client/remote-functions/prerender.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/prerender.svelte.js @@ -3,7 +3,7 @@ import { app_dir, base } from '__sveltekit/paths'; import { version } from '__sveltekit/environment'; import * as devalue from 'devalue'; import { DEV } from 'esm-env'; -import { app, started } from '../client.js'; +import { app, remote_responses, started } from '../client.js'; import { create_remote_function, remote_request } from './shared.svelte.js'; // Initialize Cache API for prerender functions @@ -118,7 +118,7 @@ export function prerender(id) { return create_remote_function(id, (cache_key, payload) => { return new Prerender(async () => { if (!started) { - const result = __SVELTEKIT_PAYLOAD__.data?.[cache_key]; + const result = remote_responses[cache_key]; if (result) { return result; } 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 60dde3fa4051..4cc9e9e156d4 100644 --- a/packages/kit/src/runtime/client/remote-functions/query.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/query.svelte.js @@ -1,7 +1,7 @@ /** @import { RemoteQueryFunction } from '@sveltejs/kit' */ /** @import { RemoteFunctionResponse } from 'types' */ import { app_dir, base } from '__sveltekit/paths'; -import { app, goto, started } from '../client.js'; +import { app, goto, remote_responses, started } from '../client.js'; import { tick } from 'svelte'; import { create_remote_function, remote_request } from './shared.svelte.js'; import * as devalue from 'devalue'; @@ -15,7 +15,7 @@ export function query(id) { return create_remote_function(id, (cache_key, payload) => { return new Query(cache_key, async () => { if (!started) { - const result = __SVELTEKIT_PAYLOAD__.data?.[cache_key]; + const result = remote_responses[cache_key]; if (result) { return result; } @@ -39,7 +39,7 @@ export function query_batch(id) { return create_remote_function(id, (cache_key, payload) => { return new Query(cache_key, () => { if (!started) { - const result = __SVELTEKIT_PAYLOAD__.data?.[cache_key]; + const result = remote_responses[cache_key]; if (result) { return result; } From 2078bf4653e1ca22c3a9696de3db91400694969d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 15 Sep 2025 17:34:30 -0400 Subject: [PATCH 5/6] set remote_responses lazily --- packages/kit/src/runtime/client/client.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 88424188f2dd..17c5232b793f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -189,11 +189,11 @@ let target; /** @type {import('./types.js').SvelteKitApp} */ export let app; -/** @type {Record} */ // we have to conditionally access the properties of `__SVELTEKIT_PAYLOAD__` // because it will be `undefined` when users import the exports from this module. // It's only defined when the server renders a page. -export const remote_responses = __SVELTEKIT_PAYLOAD__?.data ?? {}; +/** @type {Record} */ +export let remote_responses = {}; /** @type {Array<((url: URL) => boolean)>} */ const invalidated = []; @@ -294,6 +294,10 @@ export async function start(_app, _target, hydrate) { ); } + if (__SVELTEKIT_PAYLOAD__.data) { + remote_responses = __SVELTEKIT_PAYLOAD__?.data; + } + // detect basic auth credentials in the current URL // https://github.com/sveltejs/kit/pull/11179 // if so, refresh the page without credentials From 1a0c9639bf18107c8af5bfe1c64c3ce23db2548b Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 15 Sep 2025 23:41:39 +0200 Subject: [PATCH 6/6] remove obsolete comment Remove comments regarding conditional access to __SVELTEKIT_PAYLOAD__. --- packages/kit/src/runtime/client/client.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 17c5232b793f..f655bef49ecd 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -189,9 +189,6 @@ let target; /** @type {import('./types.js').SvelteKitApp} */ export let app; -// we have to conditionally access the properties of `__SVELTEKIT_PAYLOAD__` -// because it will be `undefined` when users import the exports from this module. -// It's only defined when the server renders a page. /** @type {Record} */ export let remote_responses = {};