From b768d38b9d881f8ddffaf9fa0f8a828fba8c6c6f Mon Sep 17 00:00:00 2001 From: kdelay Date: Sat, 5 Sep 2026 11:24:49 +0900 Subject: [PATCH 1/2] fix: allow requested() in a command called from other server code --- .changeset/olive-crabs-listen.md | 5 ++ .../src/runtime/app/server/remote/command.js | 12 ++- .../runtime/app/server/remote/command.spec.js | 80 +++++++++++++++++++ .../runtime/app/server/remote/requested.js | 8 +- 4 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 .changeset/olive-crabs-listen.md create mode 100644 packages/kit/src/runtime/app/server/remote/command.spec.js diff --git a/.changeset/olive-crabs-listen.md b/.changeset/olive-crabs-listen.md new file mode 100644 index 000000000000..9398b239c8ab --- /dev/null +++ b/.changeset/olive-crabs-listen.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: allow `requested()` in a command that was called from other server code diff --git a/packages/kit/src/runtime/app/server/remote/command.js b/packages/kit/src/runtime/app/server/remote/command.js index ddc9fcb9be9e..6535b74c1104 100644 --- a/packages/kit/src/runtime/app/server/remote/command.js +++ b/packages/kit/src/runtime/app/server/remote/command.js @@ -83,7 +83,17 @@ export function command(validate_or_fn, maybe_fn) { } const promise = Promise.resolve( - run_remote_function(event, state, true, () => validate(arg), fn) + run_remote_function( + event, + // `requested(...)` and single-flight `refresh()` use this to detect that they + // are inside a command. The remote function endpoint sets it for the requests + // it handles, but a command can also be called from other server code, such as + // a `+server.js` handler, and it is still a command in that case + { ...state, is_in_remote_form_or_command: true }, + true, + () => validate(arg), + fn + ) ); // @ts-expect-error diff --git a/packages/kit/src/runtime/app/server/remote/command.spec.js b/packages/kit/src/runtime/app/server/remote/command.spec.js new file mode 100644 index 000000000000..9b0019bfbc6c --- /dev/null +++ b/packages/kit/src/runtime/app/server/remote/command.spec.js @@ -0,0 +1,80 @@ +/** @import { RequestEvent } from '@sveltejs/kit' */ +/** @import { RequestState } from 'types' */ +import { expect, test, vi } from 'vitest'; +import { with_request_store } from '@sveltejs/kit/internal/server'; +import { init_transport } from '#app/internal/transport'; + +vi.stubGlobal('__SVELTEKIT_DEV__', false); +vi.stubGlobal('__SVELTEKIT_APP_VERSION__', 'test'); + +init_transport({}); + +const { command } = await import('./command.js'); +const { query } = await import('./query.js'); +const { requested } = await import('./requested.js'); + +/** + * By default this mimics a request that did not come through the `/_app/remote/...` + * endpoint, e.g. a `POST` handler in a `+server.js` file that calls a command directly + * @param {Record} [state] + * @param {boolean} [is_remote_request] + */ +function setup(state, is_remote_request = false) { + const get_items = query(() => ['a', 'b']); + /** @type {any} */ (get_items).__.id = 'hash/get_items'; + + return { + get_items, + store: { + event: /** @type {RequestEvent} */ ( + /** @type {unknown} */ ({ + request: new Request('http://localhost/api/add', { method: 'POST' }), + isRemoteRequest: is_remote_request, + cookies: {} + }) + ), + state: /** @type {RequestState} */ ( + /** @type {unknown} */ ({ + remote: {}, + is_in_remote_form_or_command: false, + ...state + }) + ) + } + }; +} + +// https://github.com/sveltejs/kit/issues/17035 +test('requested() yields nothing when the command was not called via the remote endpoint', async () => { + const { get_items, store } = setup(); + + const add = command(async () => { + const entries = [...requested(get_items, 1)]; + await requested(get_items, 1).refreshAll(); + return entries.length; + }); + + await expect(with_request_store(store, () => add())).resolves.toBe(0); +}); + +test('requested() throws when called outside a command or form', () => { + const { get_items, store } = setup(); + + expect(() => with_request_store(store, () => requested(get_items, 1))).toThrow( + 'requested(...) can only be called in the context of a command/form remote function' + ); +}); + +test('requested() still yields the queries the client asked to refresh', async () => { + const { get_items, store } = setup( + { + is_in_remote_form_or_command: true, + remote: { requested: new Map([['hash/get_items', new Set([''])]]) } + }, + true + ); + + const add = command(() => [...requested(get_items, 1)].map(({ arg }) => arg)); + + await expect(with_request_store(store, () => add())).resolves.toEqual([undefined]); +}); diff --git a/packages/kit/src/runtime/app/server/remote/requested.js b/packages/kit/src/runtime/app/server/remote/requested.js index 29a1f3075b07..4605efd54037 100644 --- a/packages/kit/src/runtime/app/server/remote/requested.js +++ b/packages/kit/src/runtime/app/server/remote/requested.js @@ -134,10 +134,10 @@ export function requested(query, limit) { ignored.add(create_remote_key(__.id, payload)); }; - // note: don't initialize these maps here -- they will be initialized by the - // command/form wrapper when we enter them, and if we initialize them here - // we will enable requested(...) in contexts where it shouldn't be allowed, - // such as load functions or other server functions + // note: this flag is set by the command/form wrapper when we enter it, so that + // requested(...) is rejected in contexts where it makes no sense, such as load + // functions or other server functions. A command that was not called via the + // remote endpoint has nothing to refresh, so `payloads` is empty there if (!state.is_in_remote_form_or_command) { throw new Error( 'requested(...) can only be called in the context of a command/form remote function' From 62fa98f27dd77f0c4e92432cc085e75e1680d929 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Sep 2026 16:12:08 +0200 Subject: [PATCH 2/2] remove redundant child states --- .../kit/src/runtime/app/server/remote/command.js | 4 ---- .../kit/src/runtime/app/server/remote/form.js | 2 +- .../kit/src/runtime/server/remote-functions.js | 15 +++------------ 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/command.js b/packages/kit/src/runtime/app/server/remote/command.js index 6535b74c1104..fd913322e262 100644 --- a/packages/kit/src/runtime/app/server/remote/command.js +++ b/packages/kit/src/runtime/app/server/remote/command.js @@ -85,10 +85,6 @@ export function command(validate_or_fn, maybe_fn) { const promise = Promise.resolve( run_remote_function( event, - // `requested(...)` and single-flight `refresh()` use this to detect that they - // are inside a command. The remote function endpoint sets it for the requests - // it handles, but a command can also be called from other server code, such as - // a `+server.js` handler, and it is still a command in that case { ...state, is_in_remote_form_or_command: true }, true, () => validate(arg), diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index f7d7de6521e3..ce78033758d7 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -117,7 +117,7 @@ export function form(validate_or_fn, maybe_fn) { try { output.result = await run_remote_function( event, - state, + { ...state, is_in_remote_form_or_command: true }, true, () => data, (data) => (!maybe_fn ? fn() : fn(data, issue)) diff --git a/packages/kit/src/runtime/server/remote-functions.js b/packages/kit/src/runtime/server/remote-functions.js index 5c6eadee66ed..875114c60c56 100644 --- a/packages/kit/src/runtime/server/remote-functions.js +++ b/packages/kit/src/runtime/server/remote-functions.js @@ -257,10 +257,7 @@ async function handle_remote_call_internal(event, state, id) { } const fn = internals.fn; - data._ = await with_request_store( - { event, state: { ...state, is_in_remote_form_or_command: true } }, - () => fn(input, meta, form_data) - ); + data._ = await with_request_store({ event, state }, () => fn(input, meta, form_data)); if (data._.issues) { // special case — don't serialize refreshes/reconnects @@ -282,10 +279,7 @@ async function handle_remote_call_internal(event, state, id) { state.remote.requested = create_requested_map(refreshes); const arg = parse_remote_arg(payload); - data._ = await with_request_store( - { event, state: { ...state, is_in_remote_form_or_command: true } }, - () => fn(arg) - ); + data._ = await with_request_store({ event, state }, () => fn(arg)); break; } @@ -565,10 +559,7 @@ async function handle_remote_form_post_internal(event, state, id) { data.id = JSON.parse(decodeURIComponent(action_id)); } - await with_request_store( - { event, state: { ...state, is_in_remote_form_or_command: true } }, - () => __.fn(data, meta, form_data) - ); + await with_request_store({ event, state }, () => __.fn(data, meta, form_data)); // We don't want the data to appear on `let { form } = $props()`, which is why we're not returning it. // It is instead available on `myForm.result`, setting of which happens within the remote `form` function.