From 8c2013da0aaa1c57011deb2a2fe77e77782abb0f Mon Sep 17 00:00:00 2001 From: Rishab49 <25582966+Rishab49@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:37:52 +0530 Subject: [PATCH 01/15] fix: use validated args in batch resolver --- .../src/runtime/app/server/remote/query.js | 12 +++- .../src/runtime/app/server/remote/shared.js | 58 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index d8bb50ba7392..e8527b2a0c0a 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -4,7 +4,13 @@ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_key, stringify_remote_arg } from '../../../shared.js'; import { prerendering } from '__sveltekit/environment'; -import { create_validator, get_cache, get_response, run_remote_function } from './shared.js'; +import { + create_validator, + get_cache, + get_response, + run_remote_batch_function, + 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. @@ -190,7 +196,7 @@ function batch(validate_or_fn, maybe_fn) { batching = { args: [], resolvers: [] }; try { - const get_result = await run_remote_function( + const result = await run_remote_batch_function( event, state, false, @@ -201,7 +207,7 @@ function batch(validate_or_fn, maybe_fn) { for (let i = 0; i < batched.resolvers.length; i++) { try { - batched.resolvers[i].resolve(get_result(batched.args[i], i)); + batched.resolvers[i].resolve(result.resolver(result.validated_args[i], i)); } catch (error) { batched.resolvers[i].reject(error); } diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index 43698b76244d..cf6998e18d1b 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -146,6 +146,64 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali return with_request_store(store, () => fn(validated)); } +/** + * Like `run_remote_function` but returns validated args along with resolver function. + * @template T + * @param {RequestEvent} event + * @param {RequestState} state + * @param {boolean} allow_cookies + * @param {any} arg + * @param {(arg: any) => any} validate + * @param {(arg?: any) => T} fn + */ +export async function run_remote_batch_function(event, state, allow_cookies, arg, validate, fn) { + /** @type {RequestStore} */ + const store = { + event: { + ...event, + setHeaders: () => { + throw new Error('setHeaders is not allowed in remote functions'); + }, + cookies: { + ...event.cookies, + set: (name, value, opts) => { + if (!allow_cookies) { + throw new Error('Cannot set cookies in `query` or `prerender` functions'); + } + + if (opts.path && !opts.path.startsWith('/')) { + throw new Error('Cookies set in remote functions must have an absolute path'); + } + + return event.cookies.set(name, value, opts); + }, + delete: (name, opts) => { + if (!allow_cookies) { + throw new Error('Cannot delete cookies in `query` or `prerender` functions'); + } + + if (opts.path && !opts.path.startsWith('/')) { + throw new Error('Cookies deleted in remote functions must have an absolute path'); + } + + return event.cookies.delete(name, opts); + } + } + }, + state: { + ...state, + is_in_remote_function: true + } + }; + + // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function + const validated = await with_request_store(store, () => validate(arg)); + return { + resolver: with_request_store(store, () => fn(validated)), + validated_args: validated + }; +} + /** * @param {RemoteInfo} info * @param {RequestState} state From 53335f6e9621ab31c1ad0db9ceec030b52b9f6fc Mon Sep 17 00:00:00 2001 From: Rishab49 <25582966+Rishab49@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:52:16 +0530 Subject: [PATCH 02/15] fix: use validated args in batch resolver in both csr and ssr --- .changeset/rude-islands-flow.md | 5 ++++ .../src/runtime/app/server/remote/query.js | 3 +- packages/kit/src/runtime/server/remote.js | 4 +-- packages/kit/src/types/internal.d.ts | 4 ++- .../src/routes/remote/batch_csr/+page.js | 1 + .../src/routes/remote/batch_csr/+page.svelte | 11 +++++++ .../routes/remote/batch_csr/batch.remote.js | 6 ++++ .../src/routes/remote/batch_ssr/+page.js | 1 + .../src/routes/remote/batch_ssr/+page.svelte | 13 +++++++++ .../routes/remote/batch_ssr/batch.remote.js | 6 ++++ .../kit/test/apps/async/test/client.test.js | 29 +++++++++++++++++++ 11 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 .changeset/rude-islands-flow.md create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js diff --git a/.changeset/rude-islands-flow.md b/.changeset/rude-islands-flow.md new file mode 100644 index 000000000000..8ba9a1d6730c --- /dev/null +++ b/.changeset/rude-islands-flow.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +fix: use validated args in batch resolver in both csr and ssr diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index e8527b2a0c0a..4efc259d96ca 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -156,8 +156,7 @@ function batch(validate_or_fn, maybe_fn) { name: '', run: (args) => { const { event, state } = get_request_store(); - - return run_remote_function( + return run_remote_batch_function( event, state, false, diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index d52b4be8d87b..3bf7eb9f7323 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -76,9 +76,9 @@ async function handle_remote_call_internal(event, state, options, manifest, id) const args = payloads.map((payload) => parse_remote_arg(payload, transport)); const get_result = await with_request_store({ event, state }, () => info.run(args)); const results = await Promise.all( - args.map(async (arg, i) => { + get_result.validated_args.map(async (arg, i) => { try { - return { type: 'result', data: get_result(arg, i) }; + return { type: 'result', data: get_result.resolver(arg, i) }; } catch (error) { return { type: 'error', diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 6384201af551..3e3b79527a64 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -571,7 +571,9 @@ export type RemoteInfo = id: string; name: string; /** Direct access to the function without batching etc logic, for remote functions called from the client */ - run: (args: any[]) => Promise<(arg: any, idx: number) => any>; + run: ( + args: any[] + ) => Promise<{ resolver: (arg: any, idx: number) => any; validated_args: any[] }>; } | { type: 'form'; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js new file mode 100644 index 000000000000..a3d15781a772 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js @@ -0,0 +1 @@ +export const ssr = false; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte new file mode 100644 index 000000000000..40a506854d81 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte @@ -0,0 +1,11 @@ + + +
+ {#each ['2026-01-01', '2025-12-01', '2025-11-01'] as d} +
+ {await getData(d)} +
+ {/each} +
diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js new file mode 100644 index 000000000000..96e5ea440530 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js @@ -0,0 +1,6 @@ +import { query } from '$app/server'; +import * as v from 'valibot'; + +export const getData = query.batch(v.pipe(v.string(), v.toDate()), (dates) => { + return (x) => typeof x; +}); diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js new file mode 100644 index 000000000000..26bb5688768d --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js @@ -0,0 +1 @@ +export const csr = false; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte new file mode 100644 index 000000000000..8daff4bff661 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte @@ -0,0 +1,13 @@ + + + +
+ {#each ['2026-01-01', '2025-12-01', '2025-11-01'] as d} +
+ {await getData(d)} +
+ {/each} +
+ diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js new file mode 100644 index 000000000000..96e5ea440530 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js @@ -0,0 +1,6 @@ +import { query } from '$app/server'; +import * as v from 'valibot'; + +export const getData = query.batch(v.pipe(v.string(), v.toDate()), (dates) => { + return (x) => typeof x; +}); diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index 8266b5ac8338..1246d9522f8b 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -301,6 +301,35 @@ test.describe('remote function mutations', () => { expect(request_count).toBe(1); // only the command request }); + test('query.batch ssr resolver function has first argument of type Output', async ({ page }) => { + await page.goto('/remote/batch_ssr', { + waitUntil: 'domcontentloaded' + }); + + const items = page.locator('#ssr_batch div'); + let count = await items.count(); + expect(count).toBe(3); + + let textContent = await items.allTextContents(); + + expect(textContent).toEqual(['object', 'object', 'object']); + }); + + test('query.batch csr resolver function has first argument of type Output', async ({ page }) => { + await page.goto('/remote/batch_csr', { + waitUntil: 'domcontentloaded' + }); + + await page.locator('body.started').waitFor({ state: 'visible' }); + const items = page.locator('#csr_batch div'); + let count = await items.count(); + expect(count).toBe(3); + + let textContent = await items.allTextContents(); + + expect(textContent).toEqual(['object', 'object', 'object']); + }); + // TODO ditto test('query works with transport', async ({ page }) => { await page.goto('/remote/transport'); From c830c38f3a903651f48455587e8c33449026bfaf Mon Sep 17 00:00:00 2001 From: Rishab49 <25582966+Rishab49@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:52:27 +0530 Subject: [PATCH 03/15] fixing linting issues --- .../async/src/routes/remote/batch_csr/batch.remote.js | 2 +- .../async/src/routes/remote/batch_ssr/batch.remote.js | 2 +- packages/kit/test/apps/async/test/client.test.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js index 96e5ea440530..a722fb9d01b8 100644 --- a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js +++ b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js @@ -1,6 +1,6 @@ import { query } from '$app/server'; import * as v from 'valibot'; -export const getData = query.batch(v.pipe(v.string(), v.toDate()), (dates) => { +export const getData = query.batch(v.pipe(v.string(), v.toDate()), () => { return (x) => typeof x; }); diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js index 96e5ea440530..a722fb9d01b8 100644 --- a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js +++ b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js @@ -1,6 +1,6 @@ import { query } from '$app/server'; import * as v from 'valibot'; -export const getData = query.batch(v.pipe(v.string(), v.toDate()), (dates) => { +export const getData = query.batch(v.pipe(v.string(), v.toDate()), () => { return (x) => typeof x; }); diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index 1246d9522f8b..a0889034a9df 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -307,10 +307,10 @@ test.describe('remote function mutations', () => { }); const items = page.locator('#ssr_batch div'); - let count = await items.count(); + const count = await items.count(); expect(count).toBe(3); - let textContent = await items.allTextContents(); + const textContent = await items.allTextContents(); expect(textContent).toEqual(['object', 'object', 'object']); }); @@ -322,10 +322,10 @@ test.describe('remote function mutations', () => { await page.locator('body.started').waitFor({ state: 'visible' }); const items = page.locator('#csr_batch div'); - let count = await items.count(); + const count = await items.count(); expect(count).toBe(3); - let textContent = await items.allTextContents(); + const textContent = await items.allTextContents(); expect(textContent).toEqual(['object', 'object', 'object']); }); From a4673254d1ff86b1467e28e75405833cf583ca39 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Mon, 26 Jan 2026 18:03:32 -0700 Subject: [PATCH 04/15] chore: rework/tidy --- .../src/runtime/app/server/remote/query.js | 20 ++--- .../src/runtime/app/server/remote/shared.js | 80 ++++++------------- packages/kit/src/runtime/server/remote.js | 56 +++++++------ packages/kit/src/types/internal.d.ts | 4 +- 4 files changed, 67 insertions(+), 93 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 4efc259d96ca..25f36a99207f 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -178,7 +178,7 @@ function batch(validate_or_fn, maybe_fn) { ); } - const { event, state } = get_request_store(); + const { state } = get_request_store(); const get_remote_function_result = () => { // Collect all the calls to the same query in the same macrotask, @@ -195,20 +195,14 @@ function batch(validate_or_fn, maybe_fn) { batching = { args: [], resolvers: [] }; try { - const result = await run_remote_batch_function( - event, - state, - false, - batched.args, - (array) => Promise.all(array.map(validate)), - fn - ); + const results = await __.run(batched.args); for (let i = 0; i < batched.resolvers.length; i++) { - try { - batched.resolvers[i].resolve(result.resolver(result.validated_args[i], i)); - } catch (error) { - batched.resolvers[i].reject(error); + const result = results[i]; + if (result.status === 'fulfilled') { + batched.resolvers[i].resolve(result.value); + } else { + batched.resolvers[i].reject(result.reason); } } } catch (error) { diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index cf6998e18d1b..a68a641261e2 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -92,18 +92,13 @@ export function parse_remote_response(data, transport) { } /** - * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`. - * @template T * @param {RequestEvent} event * @param {RequestState} state * @param {boolean} allow_cookies - * @param {any} arg - * @param {(arg: any) => any} validate - * @param {(arg?: any) => T} fn + * @returns {RequestStore} */ -export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) { - /** @type {RequestStore} */ - const store = { +function sanitize_event_for_remote_function(event, state, allow_cookies) { + return { event: { ...event, setHeaders: () => { @@ -140,68 +135,45 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali is_in_remote_function: true } }; +} +/** + * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`. + * @template T + * @param {RequestEvent} event + * @param {RequestState} state + * @param {boolean} allow_cookies + * @param {any} arg + * @param {(arg: any) => any} validate + * @param {(arg?: any) => T} fn + */ +export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) { + const store = sanitize_event_for_remote_function(event, state, allow_cookies); // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function const validated = await with_request_store(store, () => validate(arg)); return with_request_store(store, () => fn(validated)); } /** - * Like `run_remote_function` but returns validated args along with resolver function. + * Additionally-constrained version of `run_remote_function` that handles the array/validation dance of batching. + * Uses `Promise.allSettled` so that individual item errors can be captured and returned separately. * @template T * @param {RequestEvent} event * @param {RequestState} state * @param {boolean} allow_cookies * @param {any} arg - * @param {(arg: any) => any} validate - * @param {(arg?: any) => T} fn + * @param {(arg: any[]) => MaybePromise} validate + * @param {(arg?: any[]) => MaybePromise<(arg: any, idx: number) => T>} fn + * @returns {Promise[]>} */ export async function run_remote_batch_function(event, state, allow_cookies, arg, validate, fn) { - /** @type {RequestStore} */ - const store = { - event: { - ...event, - setHeaders: () => { - throw new Error('setHeaders is not allowed in remote functions'); - }, - cookies: { - ...event.cookies, - set: (name, value, opts) => { - if (!allow_cookies) { - throw new Error('Cannot set cookies in `query` or `prerender` functions'); - } - - if (opts.path && !opts.path.startsWith('/')) { - throw new Error('Cookies set in remote functions must have an absolute path'); - } - - return event.cookies.set(name, value, opts); - }, - delete: (name, opts) => { - if (!allow_cookies) { - throw new Error('Cannot delete cookies in `query` or `prerender` functions'); - } - - if (opts.path && !opts.path.startsWith('/')) { - throw new Error('Cookies deleted in remote functions must have an absolute path'); - } - - return event.cookies.delete(name, opts); - } - } - }, - state: { - ...state, - is_in_remote_function: true - } - }; - + const store = sanitize_event_for_remote_function(event, state, allow_cookies); // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function const validated = await with_request_store(store, () => validate(arg)); - return { - resolver: with_request_store(store, () => fn(validated)), - validated_args: validated - }; + const resolver = await with_request_store(store, () => fn(validated)); + return Promise.allSettled( + validated.map(async (value, index) => Promise.resolve(resolver(value, index))) + ); } /** diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 3bf7eb9f7323..523077cd494b 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -1,4 +1,4 @@ -/** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest } from '@sveltejs/kit' */ +/** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest, Transport } from '@sveltejs/kit' */ /** @import { RemoteFunctionResponse, RemoteInfo, RequestState, SSROptions } from 'types' */ import { json, error } from '@sveltejs/kit'; @@ -74,28 +74,8 @@ async function handle_remote_call_internal(event, state, options, manifest, id) const { payloads } = await event.request.json(); const args = payloads.map((payload) => parse_remote_arg(payload, transport)); - const get_result = await with_request_store({ event, state }, () => info.run(args)); - const results = await Promise.all( - get_result.validated_args.map(async (arg, i) => { - try { - return { type: 'result', data: get_result.resolver(arg, i) }; - } catch (error) { - return { - type: 'error', - error: await handle_error_and_jsonify(event, state, options, error), - status: - error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 - }; - } - }) - ); - - return json( - /** @type {RemoteFunctionResponse} */ ({ - type: 'result', - result: stringify(results, transport) - }) - ); + const results = await info.run(args); + return batch_to_response(results, transport, event, state, options); } if (info.type === 'form') { @@ -340,3 +320,33 @@ export function get_remote_id(url) { export function get_remote_action(url) { return url.searchParams.get('/remote'); } + +/** + * @param {PromiseSettledResult[]} results + * @param {Transport} transport + * @param {RequestEvent} event + * @param {RequestState} state + * @param {SSROptions} options + * @returns {Promise} + */ +async function batch_to_response(results, transport, event, state, options) { + const data = await Promise.all( + results.map(async (result) => { + if (result.status === 'fulfilled') { + return { type: 'result', data: result.value }; + } else { + const err = result.reason; + return { + type: 'error', + error: await handle_error_and_jsonify(event, state, options, err), + status: err instanceof HttpError || err instanceof SvelteKitError ? err.status : 500 + }; + } + }) + ); + + return json({ + type: 'result', + result: stringify(data, transport) + }); +} diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 3e3b79527a64..1d305b044c4d 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -571,9 +571,7 @@ export type RemoteInfo = id: string; name: string; /** Direct access to the function without batching etc logic, for remote functions called from the client */ - run: ( - args: any[] - ) => Promise<{ resolver: (arg: any, idx: number) => any; validated_args: any[] }>; + run: (args: any[]) => Promise>>; } | { type: 'form'; From 43c0f8bdf7517a126869c72ea19a008a618c31e8 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 27 Jan 2026 10:34:08 -0700 Subject: [PATCH 05/15] fix: error handling --- packages/kit/src/runtime/app/server/remote/shared.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index a68a641261e2..9f506183879f 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -156,7 +156,6 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali /** * Additionally-constrained version of `run_remote_function` that handles the array/validation dance of batching. - * Uses `Promise.allSettled` so that individual item errors can be captured and returned separately. * @template T * @param {RequestEvent} event * @param {RequestState} state @@ -171,9 +170,13 @@ export async function run_remote_batch_function(event, state, allow_cookies, arg // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function const validated = await with_request_store(store, () => validate(arg)); const resolver = await with_request_store(store, () => fn(validated)); - return Promise.allSettled( - validated.map(async (value, index) => Promise.resolve(resolver(value, index))) - ); + return validated.map((value, index) => { + try { + return { status: 'fulfilled', value: resolver(value, index) }; + } catch (e) { + return { status: 'rejected', reason: e }; + } + }); } /** From 1177e0d67bf2e7fdda5186a888875adebc2bdc2b Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 27 Jan 2026 10:35:34 -0700 Subject: [PATCH 06/15] changeset --- .changeset/rude-islands-flow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rude-islands-flow.md b/.changeset/rude-islands-flow.md index 8ba9a1d6730c..3e7813c69467 100644 --- a/.changeset/rude-islands-flow.md +++ b/.changeset/rude-islands-flow.md @@ -1,5 +1,5 @@ --- -'@sveltejs/kit': major +'@sveltejs/kit': patch --- fix: use validated args in batch resolver in both csr and ssr From 2b5a39896f949f01fe0092e07b836973e57d5b21 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 27 Jan 2026 11:05:32 -0700 Subject: [PATCH 07/15] overhaul tests --- .../remote/batch-validation/+page.svelte | 13 ++++++++ .../remote/batch-validation/batch.remote.js | 12 +++++++ .../src/routes/remote/batch_csr/+page.js | 1 - .../src/routes/remote/batch_csr/+page.svelte | 11 ------- .../routes/remote/batch_csr/batch.remote.js | 6 ---- .../src/routes/remote/batch_ssr/+page.js | 1 - .../src/routes/remote/batch_ssr/+page.svelte | 13 -------- .../routes/remote/batch_ssr/batch.remote.js | 6 ---- .../kit/test/apps/async/test/client.test.js | 31 +++---------------- 9 files changed, 30 insertions(+), 64 deletions(-) create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte delete mode 100644 packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js diff --git a/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte new file mode 100644 index 000000000000..710c4f806c6a --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte @@ -0,0 +1,13 @@ + + +
+ {#each words as word, i} + {await reverse(word)}{i === words.length - 1 ? '' : ' '} + {/each} +
+ diff --git a/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js new file mode 100644 index 000000000000..95ff1e550aa8 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js @@ -0,0 +1,12 @@ +import { query } from '$app/server'; +import * as v from 'valibot'; + +export const reverse = query.batch( + v.pipe( + v.string(), + v.transform((val) => val.split('').reverse().join('')) + ), + () => { + return (x) => x; + } +); diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js deleted file mode 100644 index a3d15781a772..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.js +++ /dev/null @@ -1 +0,0 @@ -export const ssr = false; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte deleted file mode 100644 index 40a506854d81..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_csr/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -
- {#each ['2026-01-01', '2025-12-01', '2025-11-01'] as d} -
- {await getData(d)} -
- {/each} -
diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js deleted file mode 100644 index a722fb9d01b8..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_csr/batch.remote.js +++ /dev/null @@ -1,6 +0,0 @@ -import { query } from '$app/server'; -import * as v from 'valibot'; - -export const getData = query.batch(v.pipe(v.string(), v.toDate()), () => { - return (x) => typeof x; -}); diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js deleted file mode 100644 index 26bb5688768d..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.js +++ /dev/null @@ -1 +0,0 @@ -export const csr = false; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte deleted file mode 100644 index 8daff4bff661..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/+page.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - -
- {#each ['2026-01-01', '2025-12-01', '2025-11-01'] as d} -
- {await getData(d)} -
- {/each} -
- diff --git a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js deleted file mode 100644 index a722fb9d01b8..000000000000 --- a/packages/kit/test/apps/async/src/routes/remote/batch_ssr/batch.remote.js +++ /dev/null @@ -1,6 +0,0 @@ -import { query } from '$app/server'; -import * as v from 'valibot'; - -export const getData = query.batch(v.pipe(v.string(), v.toDate()), () => { - return (x) => typeof x; -}); diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index a0889034a9df..d6d4a4da9717 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -301,33 +301,12 @@ test.describe('remote function mutations', () => { expect(request_count).toBe(1); // only the command request }); - test('query.batch ssr resolver function has first argument of type Output', async ({ page }) => { - await page.goto('/remote/batch_ssr', { - waitUntil: 'domcontentloaded' - }); + test('query.batch resolver function always receives validated arguments', async ({ page }) => { + await page.goto('/remote/batch-validation'); - const items = page.locator('#ssr_batch div'); - const count = await items.count(); - expect(count).toBe(3); - - const textContent = await items.allTextContents(); - - expect(textContent).toEqual(['object', 'object', 'object']); - }); - - test('query.batch csr resolver function has first argument of type Output', async ({ page }) => { - await page.goto('/remote/batch_csr', { - waitUntil: 'domcontentloaded' - }); - - await page.locator('body.started').waitFor({ state: 'visible' }); - const items = page.locator('#csr_batch div'); - const count = await items.count(); - expect(count).toBe(3); - - const textContent = await items.allTextContents(); - - expect(textContent).toEqual(['object', 'object', 'object']); + await expect(page.locator('#phrase')).toHaveText('use the force'); + await page.locator('button').click(); + await expect(page.locator('#phrase')).toHaveText('i am your father'); }); // TODO ditto From 562a5e13b6d03ace06d67b79b76ee70b6cac9892 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 Jan 2026 14:04:54 -0500 Subject: [PATCH 08/15] revert --- .../src/runtime/app/server/remote/query.js | 31 +++++----- .../src/runtime/app/server/remote/shared.js | 49 +++------------- packages/kit/src/runtime/server/remote.js | 56 ++++++++----------- packages/kit/src/types/internal.d.ts | 2 +- 4 files changed, 48 insertions(+), 90 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 25f36a99207f..d8bb50ba7392 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -4,13 +4,7 @@ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_key, stringify_remote_arg } from '../../../shared.js'; import { prerendering } from '__sveltekit/environment'; -import { - create_validator, - get_cache, - get_response, - run_remote_batch_function, - 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. @@ -156,7 +150,8 @@ function batch(validate_or_fn, maybe_fn) { name: '', run: (args) => { const { event, state } = get_request_store(); - return run_remote_batch_function( + + return run_remote_function( event, state, false, @@ -178,7 +173,7 @@ function batch(validate_or_fn, maybe_fn) { ); } - const { state } = get_request_store(); + const { event, state } = get_request_store(); const get_remote_function_result = () => { // Collect all the calls to the same query in the same macrotask, @@ -195,14 +190,20 @@ function batch(validate_or_fn, maybe_fn) { batching = { args: [], resolvers: [] }; try { - const results = await __.run(batched.args); + const get_result = await run_remote_function( + event, + state, + false, + batched.args, + (array) => Promise.all(array.map(validate)), + fn + ); for (let i = 0; i < batched.resolvers.length; i++) { - const result = results[i]; - if (result.status === 'fulfilled') { - batched.resolvers[i].resolve(result.value); - } else { - batched.resolvers[i].reject(result.reason); + try { + batched.resolvers[i].resolve(get_result(batched.args[i], i)); + } catch (error) { + batched.resolvers[i].reject(error); } } } catch (error) { diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index 9f506183879f..43698b76244d 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -92,13 +92,18 @@ export function parse_remote_response(data, transport) { } /** + * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`. + * @template T * @param {RequestEvent} event * @param {RequestState} state * @param {boolean} allow_cookies - * @returns {RequestStore} + * @param {any} arg + * @param {(arg: any) => any} validate + * @param {(arg?: any) => T} fn */ -function sanitize_event_for_remote_function(event, state, allow_cookies) { - return { +export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) { + /** @type {RequestStore} */ + const store = { event: { ...event, setHeaders: () => { @@ -135,50 +140,12 @@ function sanitize_event_for_remote_function(event, state, allow_cookies) { is_in_remote_function: true } }; -} -/** - * Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`. - * @template T - * @param {RequestEvent} event - * @param {RequestState} state - * @param {boolean} allow_cookies - * @param {any} arg - * @param {(arg: any) => any} validate - * @param {(arg?: any) => T} fn - */ -export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) { - const store = sanitize_event_for_remote_function(event, state, allow_cookies); // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function const validated = await with_request_store(store, () => validate(arg)); return with_request_store(store, () => fn(validated)); } -/** - * Additionally-constrained version of `run_remote_function` that handles the array/validation dance of batching. - * @template T - * @param {RequestEvent} event - * @param {RequestState} state - * @param {boolean} allow_cookies - * @param {any} arg - * @param {(arg: any[]) => MaybePromise} validate - * @param {(arg?: any[]) => MaybePromise<(arg: any, idx: number) => T>} fn - * @returns {Promise[]>} - */ -export async function run_remote_batch_function(event, state, allow_cookies, arg, validate, fn) { - const store = sanitize_event_for_remote_function(event, state, allow_cookies); - // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function - const validated = await with_request_store(store, () => validate(arg)); - const resolver = await with_request_store(store, () => fn(validated)); - return validated.map((value, index) => { - try { - return { status: 'fulfilled', value: resolver(value, index) }; - } catch (e) { - return { status: 'rejected', reason: e }; - } - }); -} - /** * @param {RemoteInfo} info * @param {RequestState} state diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 568cf4f9188e..27fd8c5a2515 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -1,4 +1,4 @@ -/** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest, Transport } from '@sveltejs/kit' */ +/** @import { ActionResult, RemoteForm, RequestEvent, SSRManifest } from '@sveltejs/kit' */ /** @import { RemoteFunctionResponse, RemoteInfo, RequestState, SSROptions } from 'types' */ import { json, error } from '@sveltejs/kit'; @@ -74,8 +74,28 @@ async function handle_remote_call_internal(event, state, options, manifest, id) const { payloads } = await event.request.json(); const args = payloads.map((payload) => parse_remote_arg(payload, transport)); - const results = await info.run(args); - return batch_to_response(results, transport, event, state, options); + const get_result = await with_request_store({ event, state }, () => info.run(args)); + const results = await Promise.all( + args.map(async (arg, i) => { + try { + return { type: 'result', data: get_result(arg, i) }; + } catch (error) { + return { + type: 'error', + error: await handle_error_and_jsonify(event, state, options, error), + status: + error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 + }; + } + }) + ); + + return json( + /** @type {RemoteFunctionResponse} */ ({ + type: 'result', + result: stringify(results, transport) + }) + ); } if (info.type === 'form') { @@ -321,33 +341,3 @@ export function get_remote_id(url) { export function get_remote_action(url) { return url.searchParams.get('/remote'); } - -/** - * @param {PromiseSettledResult[]} results - * @param {Transport} transport - * @param {RequestEvent} event - * @param {RequestState} state - * @param {SSROptions} options - * @returns {Promise} - */ -async function batch_to_response(results, transport, event, state, options) { - const data = await Promise.all( - results.map(async (result) => { - if (result.status === 'fulfilled') { - return { type: 'result', data: result.value }; - } else { - const err = result.reason; - return { - type: 'error', - error: await handle_error_and_jsonify(event, state, options, err), - status: err instanceof HttpError || err instanceof SvelteKitError ? err.status : 500 - }; - } - }) - ); - - return json({ - type: 'result', - result: stringify(data, transport) - }); -} diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 1d305b044c4d..6384201af551 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -571,7 +571,7 @@ export type RemoteInfo = id: string; name: string; /** Direct access to the function without batching etc logic, for remote functions called from the client */ - run: (args: any[]) => Promise>>; + run: (args: any[]) => Promise<(arg: any, idx: number) => any>; } | { type: 'form'; From 058e2198335e2521252b73058c90e8d25a9d68a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 Jan 2026 14:05:03 -0500 Subject: [PATCH 09/15] unstash --- packages/kit/src/runtime/app/server/remote/query.js | 9 +++++---- packages/kit/src/runtime/server/remote.js | 2 +- packages/kit/src/types/internal.d.ts | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index d8bb50ba7392..c4298e0a7794 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -156,10 +156,11 @@ function batch(validate_or_fn, maybe_fn) { state, false, args, - (array) => Promise.all(array.map(validate)), + (array) => array, fn ); - } + }, + validate }; /** @type {{ args: any[], resolvers: Array<{resolve: (value: any) => void, reject: (error: any) => void}> }} */ @@ -180,7 +181,7 @@ function batch(validate_or_fn, maybe_fn) { // then execute them as one backend request. return new Promise((resolve, reject) => { // We don't need to deduplicate args here, because get_response already caches/reuses identical calls - batching.args.push(arg); + batching.args.push(validate(arg)); batching.resolvers.push({ resolve, reject }); if (batching.args.length > 1) return; @@ -195,7 +196,7 @@ function batch(validate_or_fn, maybe_fn) { state, false, batched.args, - (array) => Promise.all(array.map(validate)), + (array) => Promise.all(array), fn ); diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 27fd8c5a2515..997450fe3fbc 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -73,7 +73,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id) /** @type {{ payloads: string[] }} */ const { payloads } = await event.request.json(); - const args = payloads.map((payload) => parse_remote_arg(payload, transport)); + const args = await Promise.all((payloads.map((payload) => info.validate(parse_remote_arg(payload, transport))))); const get_result = await with_request_store({ event, state }, () => info.run(args)); const results = await Promise.all( args.map(async (arg, i) => { diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 6384201af551..583437edb612 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -572,6 +572,7 @@ export type RemoteInfo = name: string; /** Direct access to the function without batching etc logic, for remote functions called from the client */ run: (args: any[]) => Promise<(arg: any, idx: number) => any>; + validate: (arg: any) => any; } | { type: 'form'; From 534cff5a7f9e038fb3075f0b665285a62a1d04fc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 Jan 2026 15:44:24 -0500 Subject: [PATCH 10/15] move some logic --- .../src/runtime/app/server/remote/query.js | 21 +++++++++++++++++-- packages/kit/src/runtime/server/remote.js | 16 +------------- packages/kit/src/types/internal.d.ts | 4 ++-- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index c4298e0a7794..3635c075961a 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -5,6 +5,8 @@ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_key, stringify_remote_arg } from '../../../shared.js'; import { prerendering } from '__sveltekit/environment'; import { create_validator, get_cache, get_response, run_remote_function } from './shared.js'; +import { handle_error_and_jsonify } from '../../../server/utils.js'; +import { HttpError, SvelteKitError } from '@sveltejs/kit/internal'; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. @@ -148,10 +150,10 @@ function batch(validate_or_fn, maybe_fn) { type: 'query_batch', id: '', name: '', - run: (args) => { + run: async (args, options) => { const { event, state } = get_request_store(); - return run_remote_function( + const get_result = await run_remote_function( event, state, false, @@ -159,6 +161,21 @@ function batch(validate_or_fn, maybe_fn) { (array) => array, fn ); + + return Promise.all( + args.map(async (arg, i) => { + try { + return { type: 'result', data: get_result(arg, i) }; + } catch (error) { + return { + type: 'error', + error: await handle_error_and_jsonify(event, state, options, error), + status: + error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 + }; + } + }) + ); }, validate }; diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 997450fe3fbc..31aad0e0561d 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -74,21 +74,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id) const { payloads } = await event.request.json(); const args = await Promise.all((payloads.map((payload) => info.validate(parse_remote_arg(payload, transport))))); - const get_result = await with_request_store({ event, state }, () => info.run(args)); - const results = await Promise.all( - args.map(async (arg, i) => { - try { - return { type: 'result', data: get_result(arg, i) }; - } catch (error) { - return { - type: 'error', - error: await handle_error_and_jsonify(event, state, options, error), - status: - error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 - }; - } - }) - ); + const results = await with_request_store({ event, state }, () => info.run(args, options)); return json( /** @type {RemoteFunctionResponse} */ ({ diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 583437edb612..3d08f4c13e9a 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -570,8 +570,8 @@ export type RemoteInfo = type: 'query_batch'; id: string; name: string; - /** Direct access to the function without batching etc logic, for remote functions called from the client */ - run: (args: any[]) => Promise<(arg: any, idx: number) => any>; + /** Direct access to the function, for remote functions called from the client */ + run: (args: any[], options: SSROptions) => Promise; validate: (arg: any) => any; } | { From 7a5d04b4cd770be5a3cd57874d9e4888a98f4f16 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 Jan 2026 16:13:38 -0500 Subject: [PATCH 11/15] tweak --- .../src/runtime/app/server/remote/query.js | 19 ++++++++++++------- packages/kit/src/runtime/server/remote.js | 2 +- packages/kit/src/types/internal.d.ts | 1 - 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 3635c075961a..7f16cb1ca132 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -153,17 +153,20 @@ function batch(validate_or_fn, maybe_fn) { run: async (args, options) => { const { event, state } = get_request_store(); + /** @type {any[]} */ + let validated = []; + const get_result = await run_remote_function( event, state, false, args, - (array) => array, + async (array) => (validated = await Promise.all(array.map(validate))), fn ); return Promise.all( - args.map(async (arg, i) => { + validated.map(async (arg, i) => { try { return { type: 'result', data: get_result(arg, i) }; } catch (error) { @@ -176,8 +179,7 @@ function batch(validate_or_fn, maybe_fn) { } }) ); - }, - validate + } }; /** @type {{ args: any[], resolvers: Array<{resolve: (value: any) => void, reject: (error: any) => void}> }} */ @@ -198,7 +200,7 @@ function batch(validate_or_fn, maybe_fn) { // then execute them as one backend request. return new Promise((resolve, reject) => { // We don't need to deduplicate args here, because get_response already caches/reuses identical calls - batching.args.push(validate(arg)); + batching.args.push(arg); batching.resolvers.push({ resolve, reject }); if (batching.args.length > 1) return; @@ -208,18 +210,21 @@ function batch(validate_or_fn, maybe_fn) { batching = { args: [], resolvers: [] }; try { + /** @type {any[]} */ + let validated = []; + const get_result = await run_remote_function( event, state, false, batched.args, - (array) => Promise.all(array), + async (array) => (validated = await Promise.all(array.map(validate))), fn ); for (let i = 0; i < batched.resolvers.length; i++) { try { - batched.resolvers[i].resolve(get_result(batched.args[i], i)); + batched.resolvers[i].resolve(get_result(validated[i], i)); } catch (error) { batched.resolvers[i].reject(error); } diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 31aad0e0561d..9937078c4b3b 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -73,7 +73,7 @@ async function handle_remote_call_internal(event, state, options, manifest, id) /** @type {{ payloads: string[] }} */ const { payloads } = await event.request.json(); - const args = await Promise.all((payloads.map((payload) => info.validate(parse_remote_arg(payload, transport))))); + const args = await Promise.all((payloads.map((payload) => parse_remote_arg(payload, transport)))); const results = await with_request_store({ event, state }, () => info.run(args, options)); return json( diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 3d08f4c13e9a..2e736bfc9dad 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -572,7 +572,6 @@ export type RemoteInfo = name: string; /** Direct access to the function, for remote functions called from the client */ run: (args: any[], options: SSROptions) => Promise; - validate: (arg: any) => any; } | { type: 'form'; From 206d7b32f01d830bbd47b14fb27c743185d19c52 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 Jan 2026 16:17:23 -0500 Subject: [PATCH 12/15] simplify --- packages/kit/src/runtime/app/server/remote/command.js | 2 +- packages/kit/src/runtime/app/server/remote/form.js | 3 +-- packages/kit/src/runtime/app/server/remote/prerender.js | 2 +- packages/kit/src/runtime/app/server/remote/query.js | 8 +++----- packages/kit/src/runtime/app/server/remote/shared.js | 9 ++++----- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/command.js b/packages/kit/src/runtime/app/server/remote/command.js index 2ea88bfb02f0..e9b171746afd 100644 --- a/packages/kit/src/runtime/app/server/remote/command.js +++ b/packages/kit/src/runtime/app/server/remote/command.js @@ -78,7 +78,7 @@ export function command(validate_or_fn, maybe_fn) { state.refreshes ??= {}; - const promise = Promise.resolve(run_remote_function(event, state, true, arg, validate, fn)); + const promise = Promise.resolve(run_remote_function(event, state, true, () => validate(arg), fn)); // @ts-expect-error promise.updates = () => { diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 9f71d6e7e126..0c5824e19190 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -145,8 +145,7 @@ export function form(validate_or_fn, maybe_fn) { event, state, true, - data, - (d) => d, + () => data, (data) => (!maybe_fn ? fn() : fn(data, issue)) ); } catch (e) { diff --git a/packages/kit/src/runtime/app/server/remote/prerender.js b/packages/kit/src/runtime/app/server/remote/prerender.js index 8e9762463063..f9729f7c7752 100644 --- a/packages/kit/src/runtime/app/server/remote/prerender.js +++ b/packages/kit/src/runtime/app/server/remote/prerender.js @@ -131,7 +131,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { } const promise = get_response(__, arg, state, () => - run_remote_function(event, state, false, arg, validate, fn) + run_remote_function(event, state, false, () => validate(arg), fn) ); if (state.prerendering) { diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 7f16cb1ca132..c07cbaddc94d 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -75,7 +75,7 @@ export function query(validate_or_fn, maybe_fn) { const { event, state } = get_request_store(); const get_remote_function_result = () => - run_remote_function(event, state, false, arg, validate, fn); + run_remote_function(event, state, false, () => validate(arg), fn); /** @type {Promise & Partial>} */ const promise = get_response(__, arg, state, get_remote_function_result); @@ -160,8 +160,7 @@ function batch(validate_or_fn, maybe_fn) { event, state, false, - args, - async (array) => (validated = await Promise.all(array.map(validate))), + async () => (validated = await Promise.all(args.map(validate))), fn ); @@ -217,8 +216,7 @@ function batch(validate_or_fn, maybe_fn) { event, state, false, - batched.args, - async (array) => (validated = await Promise.all(array.map(validate))), + async () => (validated = await Promise.all(batched.args.map(validate))), fn ); diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index 43698b76244d..a2e310649d1d 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -97,11 +97,10 @@ export function parse_remote_response(data, transport) { * @param {RequestEvent} event * @param {RequestState} state * @param {boolean} allow_cookies - * @param {any} arg - * @param {(arg: any) => any} validate + * @param {() => any} get_input * @param {(arg?: any) => T} fn */ -export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) { +export async function run_remote_function(event, state, allow_cookies, get_input, fn) { /** @type {RequestStore} */ const store = { event: { @@ -142,8 +141,8 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali }; // In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function - const validated = await with_request_store(store, () => validate(arg)); - return with_request_store(store, () => fn(validated)); + const input = await with_request_store(store, get_input); + return with_request_store(store, () => fn(input)); } /** From 7477105a5315c82d04cb2e627f17a545ba151b7e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 Jan 2026 16:25:42 -0500 Subject: [PATCH 13/15] prettier --- packages/kit/src/runtime/app/server/remote/command.js | 4 +++- packages/kit/src/runtime/server/remote.js | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/command.js b/packages/kit/src/runtime/app/server/remote/command.js index e9b171746afd..0ce1575c08f1 100644 --- a/packages/kit/src/runtime/app/server/remote/command.js +++ b/packages/kit/src/runtime/app/server/remote/command.js @@ -78,7 +78,9 @@ export function command(validate_or_fn, maybe_fn) { state.refreshes ??= {}; - const promise = Promise.resolve(run_remote_function(event, state, true, () => validate(arg), fn)); + const promise = Promise.resolve( + run_remote_function(event, state, true, () => validate(arg), fn) + ); // @ts-expect-error promise.updates = () => { diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 9937078c4b3b..b47a48210de4 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -73,7 +73,10 @@ async function handle_remote_call_internal(event, state, options, manifest, id) /** @type {{ payloads: string[] }} */ const { payloads } = await event.request.json(); - const args = await Promise.all((payloads.map((payload) => parse_remote_arg(payload, transport)))); + const args = await Promise.all( + payloads.map((payload) => parse_remote_arg(payload, transport)) + ); + const results = await with_request_store({ event, state }, () => info.run(args, options)); return json( From 1ac59ad54d25489b5283ec96812644ee9d39c544 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 Jan 2026 17:22:09 -0500 Subject: [PATCH 14/15] do it without the weird side-effecty validator --- .../src/runtime/app/server/remote/query.js | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index c07cbaddc94d..3f1dcb3608a0 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -139,7 +139,7 @@ export function query(validate_or_fn, maybe_fn) { */ /*@__NO_SIDE_EFFECTS__*/ function batch(validate_or_fn, maybe_fn) { - /** @type {(args?: Input[]) => (arg: Input, idx: number) => Output} */ + /** @type {(args?: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>} */ const fn = maybe_fn ?? validate_or_fn; /** @type {(arg?: any) => MaybePromise} */ @@ -153,30 +153,29 @@ function batch(validate_or_fn, maybe_fn) { run: async (args, options) => { const { event, state } = get_request_store(); - /** @type {any[]} */ - let validated = []; - - const get_result = await run_remote_function( + return run_remote_function( event, state, false, - async () => (validated = await Promise.all(args.map(validate))), - fn - ); + async () => (Promise.all(args.map(validate))), + async (/** @type {any[]} */ input) => { + const get_result = await fn(input); - return Promise.all( - validated.map(async (arg, i) => { - try { - return { type: 'result', data: get_result(arg, i) }; - } catch (error) { - return { - type: 'error', - error: await handle_error_and_jsonify(event, state, options, error), - status: - error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 - }; - } - }) + return Promise.all( + input.map(async (arg, i) => { + try { + return { type: 'result', data: get_result(arg, i) }; + } catch (error) { + return { + type: 'error', + error: await handle_error_and_jsonify(event, state, options, error), + status: + error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 + }; + } + }) + ); + } ); } }; @@ -209,24 +208,23 @@ function batch(validate_or_fn, maybe_fn) { batching = { args: [], resolvers: [] }; try { - /** @type {any[]} */ - let validated = []; - - const get_result = await run_remote_function( + return await run_remote_function( event, state, false, - async () => (validated = await Promise.all(batched.args.map(validate))), - fn - ); - - for (let i = 0; i < batched.resolvers.length; i++) { - try { - batched.resolvers[i].resolve(get_result(validated[i], i)); - } catch (error) { - batched.resolvers[i].reject(error); + async () => (Promise.all(batched.args.map(validate))), + async (input) => { + const get_result = await fn(input); + + for (let i = 0; i < batched.resolvers.length; i++) { + try { + batched.resolvers[i].resolve(get_result(input[i], i)); + } catch (error) { + batched.resolvers[i].reject(error); + } + } } - } + ); } catch (error) { for (const resolver of batched.resolvers) { resolver.reject(error); From 4333943de397367684414c78fbacddb7df9bf5a0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 27 Jan 2026 17:23:11 -0500 Subject: [PATCH 15/15] prettier --- packages/kit/src/runtime/app/server/remote/query.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index 3f1dcb3608a0..4b21603ae132 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -157,7 +157,7 @@ function batch(validate_or_fn, maybe_fn) { event, state, false, - async () => (Promise.all(args.map(validate))), + async () => Promise.all(args.map(validate)), async (/** @type {any[]} */ input) => { const get_result = await fn(input); @@ -170,7 +170,9 @@ function batch(validate_or_fn, maybe_fn) { type: 'error', error: await handle_error_and_jsonify(event, state, options, error), status: - error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 + error instanceof HttpError || error instanceof SvelteKitError + ? error.status + : 500 }; } }) @@ -212,7 +214,7 @@ function batch(validate_or_fn, maybe_fn) { event, state, false, - async () => (Promise.all(batched.args.map(validate))), + async () => Promise.all(batched.args.map(validate)), async (input) => { const get_result = await fn(input);