;
/** A map of query.batch ID to payloads requested for that batch within the same macrotask */
diff --git a/packages/kit/test/apps/async/src/routes/remote/nested-from/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/nested-from/+page.svelte
new file mode 100644
index 000000000000..5f368cc4dfbb
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/nested-from/+page.svelte
@@ -0,0 +1,35 @@
+
+
+Seeded nested query (from)
+
+
+ {(await parent).label}
+ {(await (await parent).child).value}
+ {(await client_seeded).value}
+
+ {#snippet failed(error)}
+ error: {/** @type {Error} */ (error).message}
+ {/snippet}
+
+
+
+
+{command_result}
diff --git a/packages/kit/test/apps/async/src/routes/remote/nested-live/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/nested-live/+page.svelte
new file mode 100644
index 000000000000..fe60013d5713
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/nested-live/+page.svelte
@@ -0,0 +1,21 @@
+
+
+Nested query in a live query
+
+
+ {@const first = await live}
+ {first.tick}
+
+ {live.current ? `${live.current.tick}:${(await live.current.child).value}` : 'pending'}
+
+
+ {#snippet failed(error)}
+ error: {/** @type {Error} */ (error).message}
+ {/snippet}
+
+
+
diff --git a/packages/kit/test/apps/async/src/routes/remote/nested-prerender/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/nested-prerender/+page.svelte
new file mode 100644
index 000000000000..0de3321580e1
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/nested-prerender/+page.svelte
@@ -0,0 +1,15 @@
+
+
+Nested prerender
+
+
+ {await (await pparent).child}
+
+ {#snippet failed(error)}
+ error: {/** @type {Error} */ (error).message}
+ {/snippet}
+
diff --git a/packages/kit/test/apps/async/src/routes/remote/nested/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/nested/+page.svelte
new file mode 100644
index 000000000000..5c5af272e575
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/nested/+page.svelte
@@ -0,0 +1,31 @@
+
+
+Nested remote functions
+
+
+ {(await parent).label}
+ {(await (await parent).child).value}
+
+ {#snippet failed(error)}
+ error: {/** @type {Error} */ (error).message}
+ {/snippet}
+
+
+
+
+{command_result}
diff --git a/packages/kit/test/apps/async/src/routes/remote/nested/nested.remote.js b/packages/kit/test/apps/async/src/routes/remote/nested/nested.remote.js
new file mode 100644
index 000000000000..962c7ea8fe2c
--- /dev/null
+++ b/packages/kit/test/apps/async/src/routes/remote/nested/nested.remote.js
@@ -0,0 +1,99 @@
+import { command, getRequestEvent, prerender, query } from '$app/server';
+
+let child_calls = 0;
+
+export const get_child = query('unchecked', (/** @type {string} */ id) => {
+ child_calls++;
+ return { id, value: `child:${id}` };
+});
+
+export const get_child_calls = query(() => child_calls);
+
+export const reset_child_calls = command(() => {
+ child_calls = 0;
+});
+
+// returns a nested query that IS awaited during render, so its value gets seeded
+export const get_parent = query('unchecked', (/** @type {string} */ id) => {
+ return { id, label: `parent:${id}`, child: get_child(id) };
+});
+
+// returns a nested query that is NOT used — only the pointer is serialized, and the client
+// fetches the value on use
+export const get_parent_unused = query('unchecked', (/** @type {string} */ id) => {
+ return { id, child: get_child(`${id}-unused`) };
+});
+
+// a command that returns a nested query whose value is seeded via the side-channel
+export const create_child = command('unchecked', async (/** @type {string} */ id) => {
+ const child = get_child(id);
+ await child; // mark as used so its value is seeded into the response
+ return { created: id, child };
+});
+
+// returns a nested query seeded via `.from(...)`: the query's own function is never invoked,
+// but the provided value still travels to the client so the pointer resolves without a fetch
+export const get_parent_from = query('unchecked', (/** @type {string} */ id) => {
+ return {
+ id,
+ label: `parent-from:${id}`,
+ child: get_child.from(id, { id, value: `seeded:${id}` })
+ };
+});
+
+// a command that returns a nested query created (and seeded) via `.from(...)`
+export const create_child_from = command('unchecked', (/** @type {string} */ id) => {
+ return { created: id, child: get_child.from(id, { id, value: `seeded:${id}` }) };
+});
+
+// a live query that yields values containing a nested query, seeded per stream message
+/** @type {Set<() => void>} */
+const live_listeners = new Set();
+let live_tick = 0;
+
+export const bump_live = command(() => {
+ live_tick += 1;
+ for (const listener of live_listeners) listener();
+ live_listeners.clear();
+});
+
+export const live_with_child = query.live(async function* () {
+ const signal = getRequestEvent().request.signal;
+
+ while (true) {
+ const tick = live_tick;
+ const child = get_child(`live-${tick}`);
+ await child; // mark used so its value is seeded into the stream message
+ yield { tick, child };
+
+ const changed = await new Promise((resolve) => {
+ const on_change = () => {
+ signal.removeEventListener('abort', on_abort);
+ resolve(true);
+ };
+ const on_abort = () => {
+ live_listeners.delete(on_change);
+ resolve(false);
+ };
+ live_listeners.add(on_change);
+ signal.addEventListener('abort', on_abort, { once: true });
+ });
+
+ if (!changed) return;
+ }
+});
+
+const prerender_child = prerender('unchecked', (/** @type {string} */ id) => `pchild:${id}`, {
+ dynamic: true
+});
+
+export { prerender_child };
+
+// a prerender that returns a nested prerender
+export const prerender_parent = prerender(
+ 'unchecked',
+ (/** @type {string} */ id) => {
+ return { id, child: prerender_child(id) };
+ },
+ { dynamic: true }
+);
diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js
index b10c7ed5123b..7f30b57b409a 100644
--- a/packages/kit/test/apps/async/test/client.test.js
+++ b/packages/kit/test/apps/async/test/client.test.js
@@ -809,6 +809,113 @@ test.describe('remote function mutations', () => {
// Should have refreshed
await expect(count).toHaveText('Count: 1');
});
+
+ test('a query returning a nested query seeds it without an extra request', async ({ page }) => {
+ let request_count = 0;
+ /** @param {import('@playwright/test').Request} r */
+ const handler = (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0);
+ page.on('request', handler);
+
+ await page.goto('/remote/nested');
+
+ await expect(page.locator('#parent')).toHaveText('parent:a');
+ // the nested query value was seeded during SSR, so it resolves without a request
+ await expect(page.locator('#child')).toHaveText('child:a');
+
+ await page.waitForTimeout(100);
+ expect(request_count).toBe(0);
+ });
+
+ test('a prerender can return a nested prerender', async ({ page }) => {
+ let nested_request_count = 0;
+ /** @param {import('@playwright/test').Request} r */
+ const handler = (r) => (nested_request_count += r.url().includes('prerender_child') ? 1 : 0);
+ page.on('request', handler);
+
+ await page.goto('/remote/nested-prerender');
+ await expect(page.locator('#prerender-child')).toHaveText('pchild:p');
+
+ await page.waitForTimeout(100);
+ // the nested prerender's value is seeded (server-side revival + the parent response's
+ // `queries` side-channel), so it never has to be fetched separately on the client
+ expect(nested_request_count).toBe(0);
+ });
+
+ test('a live query can return a nested query seeded per stream message', async ({ page }) => {
+ let nested_request_count = 0;
+ /** @param {import('@playwright/test').Request} r */
+ const handler = (r) => (nested_request_count += r.url().includes('get_child') ? 1 : 0);
+ page.on('request', handler);
+
+ await page.goto('/remote/nested-live');
+ await expect(page.locator('#live-child')).toHaveText('0:child:live-0');
+
+ // trigger a new stream value whose nested query value only arrives over the stream
+ await page.click('#bump');
+ await expect(page.locator('#live-child')).toHaveText('1:child:live-1');
+
+ await page.waitForTimeout(100);
+ // the nested query's value rides along in each stream message's `queries` side-channel,
+ // so it's never fetched separately
+ expect(nested_request_count).toBe(0);
+ });
+
+ test('a command can return a nested query seeded via the side-channel', async ({ page }) => {
+ await page.goto('/remote/nested');
+ await expect(page.locator('#parent')).toHaveText('parent:a');
+
+ let request_count = 0;
+ /** @param {import('@playwright/test').Request} r */
+ const handler = (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0);
+ page.on('request', handler);
+
+ await page.click('#create-child');
+
+ // the nested query returned by the command resolves to its seeded value
+ await expect(page.locator('#command-result')).toHaveText('z/child:z');
+
+ await page.waitForTimeout(100);
+ // only the command POST itself — the nested query was seeded, not fetched
+ expect(request_count).toBe(1);
+ });
+
+ test('a query can return a nested query seeded with `.from(...)`', async ({ page }) => {
+ let request_count = 0;
+ /** @param {import('@playwright/test').Request} r */
+ const handler = (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0);
+ page.on('request', handler);
+
+ await page.goto('/remote/nested-from');
+
+ await expect(page.locator('#parent')).toHaveText('parent-from:a');
+ // the child's value was provided via `.from(...)` (its function never ran), and seeded
+ // into the page — so it resolves to the seeded value without a request
+ await expect(page.locator('#child')).toHaveText('seeded:a');
+ // a query seeded purely on the client via `.from(...)` also resolves without a request
+ await expect(page.locator('#client-seeded')).toHaveText('client-seeded');
+
+ await page.waitForTimeout(100);
+ expect(request_count).toBe(0);
+ });
+
+ test('a command can return a nested query seeded with `.from(...)`', async ({ page }) => {
+ await page.goto('/remote/nested-from');
+ await expect(page.locator('#parent')).toHaveText('parent-from:a');
+
+ let request_count = 0;
+ /** @param {import('@playwright/test').Request} r */
+ const handler = (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0);
+ page.on('request', handler);
+
+ await page.click('#create-child');
+
+ // the nested query created by the command via `.from(...)` resolves to its seeded value
+ await expect(page.locator('#command-result')).toHaveText('z/seeded:z');
+
+ await page.waitForTimeout(100);
+ // only the command POST itself — the nested query was seeded, not fetched
+ expect(request_count).toBe(1);
+ });
});
test.describe('client error boundaries', () => {
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 1948fe5f4615..ff2578389bc5 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -2273,9 +2273,32 @@ declare module '@sveltejs/kit' {
* `Input = number` but `Validated = string`). For `'unchecked'` validators and queries
* without arguments it defaults to `Input`.
*/
- export type RemoteQueryFunction = (
+ export type RemoteQueryFunction = ((
arg: undefined extends Input ? Input | void : Input
- ) => RemoteQuery