Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-crabs-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: allow `requested()` in a command that was called from other server code
8 changes: 7 additions & 1 deletion packages/kit/src/runtime/app/server/remote/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ 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,
{ ...state, is_in_remote_form_or_command: true },
true,
() => validate(arg),
fn
)
);

// @ts-expect-error
Expand Down
80 changes: 80 additions & 0 deletions packages/kit/src/runtime/app/server/remote/command.spec.js
Original file line number Diff line number Diff line change
@@ -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<string, any>} [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]);
});
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/app/server/remote/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
8 changes: 4 additions & 4 deletions packages/kit/src/runtime/app/server/remote/requested.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
15 changes: 3 additions & 12 deletions packages/kit/src/runtime/server/remote-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down
Loading