Skip to content
Merged
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/rude-islands-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: use validated args in batch resolver in both csr and ssr
4 changes: 3 additions & 1 deletion packages/kit/src/runtime/app/server/remote/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ 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 = () => {
Expand Down
3 changes: 1 addition & 2 deletions packages/kit/src/runtime/app/server/remote/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/app/server/remote/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
57 changes: 39 additions & 18 deletions packages/kit/src/runtime/app/server/remote/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -73,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<any> & Partial<RemoteQuery<any>>} */
const promise = get_response(__, arg, state, get_remote_function_result);
Expand Down Expand Up @@ -137,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<Input>} */
Expand All @@ -148,16 +150,34 @@ 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(
event,
state,
false,
args,
(array) => Promise.all(array.map(validate)),
fn
async () => Promise.all(args.map(validate)),
async (/** @type {any[]} */ input) => {
const get_result = await fn(input);

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
};
}
})
);
}
);
}
};
Expand Down Expand Up @@ -190,22 +210,23 @@ function batch(validate_or_fn, maybe_fn) {
batching = { args: [], resolvers: [] };

try {
const get_result = await run_remote_function(
return 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++) {
try {
batched.resolvers[i].resolve(get_result(batched.args[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);
Expand Down
9 changes: 4 additions & 5 deletions packages/kit/src/runtime/app/server/remote/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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));
}

/**
Expand Down
19 changes: 4 additions & 15 deletions packages/kit/src/runtime/server/remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,12 @@ 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 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 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(
/** @type {RemoteFunctionResponse} */ ({
type: 'result',
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any[]>;
}
| {
type: 'form';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import { reverse } from './batch.remote.js';
let phrase = $state('ecrof eht esu');
const words = $derived(phrase.split(' ').reverse());
</script>

<div id="phrase">
{#each words as word, i}
{await reverse(word)}{i === words.length - 1 ? '' : ' '}
{/each}
</div>
<button onclick={() => (phrase = 'rehtaf ruoy ma i')}>get dramatic</button>
Original file line number Diff line number Diff line change
@@ -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;
}
);
8 changes: 8 additions & 0 deletions packages/kit/test/apps/async/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,14 @@ test.describe('remote function mutations', () => {
expect(request_count).toBe(1); // only the command request
});

test('query.batch resolver function always receives validated arguments', async ({ page }) => {
await page.goto('/remote/batch-validation');

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
test('query works with transport', async ({ page }) => {
await page.goto('/remote/transport');
Expand Down
Loading