Skip to content
Draft
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1affeca
feat: add new remote function `query.batch`
dummdidumm Aug 19, 2025
b70a394
my god this high already
dummdidumm Aug 19, 2025
bd0c519
hhnngghhhh
dummdidumm Aug 19, 2025
60fb389
feat: add remote function `query.stream`
dummdidumm Aug 21, 2025
41fad93
make it treeshakeable on the client
dummdidumm Aug 22, 2025
4819f97
hydrate data
dummdidumm Aug 22, 2025
a955c16
validation
dummdidumm Aug 22, 2025
1b5f453
lint
dummdidumm Aug 22, 2025
5da377d
explain + simplify (no use in clearing a timeout for the next macrotask)
dummdidumm Aug 22, 2025
e036a27
fast-path for remote client calls
dummdidumm Aug 22, 2025
afdae97
validate
dummdidumm Aug 22, 2025
6434cf3
note in docs about output shape
dummdidumm Aug 22, 2025
5ed076e
Merge branch 'query-batch' into query-stream
dummdidumm Aug 22, 2025
3b54a4c
make it treeshakeable
dummdidumm Aug 22, 2025
96ea81c
oops
dummdidumm Aug 22, 2025
9656a7f
regenerate types
dummdidumm Aug 22, 2025
6665ed6
thanks for nothing service workers
dummdidumm Aug 22, 2025
28a3564
tweak readablestream implementation
dummdidumm Aug 22, 2025
531225c
fix
dummdidumm Aug 22, 2025
e630de7
adjust API
dummdidumm Aug 23, 2025
cbfa1cf
deduplicate
dummdidumm Aug 23, 2025
aa7cd56
test deduplication
dummdidumm Aug 23, 2025
910125c
fix
dummdidumm Aug 23, 2025
476ce72
Merge branch 'query-batch' into query-stream
dummdidumm Aug 23, 2025
2ca6eb1
oops
dummdidumm Aug 23, 2025
3447d75
Merge branch 'query-batch' into query-stream
dummdidumm Aug 23, 2025
dfa4cb4
omg lol
dummdidumm Aug 23, 2025
d51c20c
Merge branch 'query-batch' into query-stream
dummdidumm Aug 23, 2025
44f60b8
Update documentation/docs/20-core-concepts/60-remote-functions.md
dummdidumm Aug 23, 2025
d8d02bc
per-item error handling
dummdidumm Aug 23, 2025
40de6f1
Merge branch 'query-batch' into query-stream
dummdidumm Aug 23, 2025
a649cf0
don't share stream()
dummdidumm Aug 25, 2025
ac02bc1
Update documentation/docs/20-core-concepts/60-remote-functions.md
dummdidumm Aug 30, 2025
74d0587
Merge branch 'main' into query-stream
dummdidumm Sep 10, 2025
7254df5
Merge branch 'main' into query-stream
Rich-Harris Sep 10, 2025
cead45e
remove duplicated docs
Rich-Harris Sep 10, 2025
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/red-waves-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add new remote function `query.batch`
5 changes: 5 additions & 0 deletions .changeset/smart-nails-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add remote function `query.stream`
137 changes: 137 additions & 0 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,143 @@ Any query can be updated via its `refresh` method:

> [!NOTE] Queries are cached while they're on the page, meaning `getPosts() === getPosts()`. This means you don't need a reference like `const posts = getPosts()` in order to refresh the query.

## query.batch

`query.batch` works like `query` except that it batches requests that happen within the same macrotask. This solves the so-called n+1 problem: rather than each query resulting in a separate database call (for example), simultaneous queries are grouped together.

On the server, the callback receives an array of the arguments the function was called with. It must return a function of the form `(input: Input, index: number) => Output`. SvelteKit will then call this with each of the input arguments to resolve the individual calls with their results.

```js
/// file: weather.remote.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function sql(strings: TemplateStringsArray, ...values: any[]): Promise<any[]>;
}
// @filename: index.js
// ---cut---
import * as v from 'valibot';
import { query } from '$app/server';
import * as db from '$lib/server/database';

export const getWeather = query.batch(v.string(), async (cities) => {
const weather = await db.sql`
SELECT * FROM weather
WHERE city = ANY(${cities})
`;
const weatherMap = new Map(weather.map(w => [w.city, w]));

return (city) => weatherMap.get(city);
});
```

```svelte
<!--- file: Weather.svelte --->
<script>
import CityWeather from './CityWeather.svelte';
import { getWeather } from './weather.remote.js';

let { cities } = $props();
let limit = $state(5);
</script>

<h2>Weather</h2>

{#each cities.slice(0, limit) as city}
<h3>{city.name}</h3>
<CityWeather weather={await getWeather(city.id)} />
{/each}

{#if cities.length > limit}
<button onclick={() => limit += 5}>
Load more
</button>
{/if}
```

## query.stream

`query.stream` allows you to stream continuous data from the server to the client.

```js
/// file: src/routes/time.remote.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function sql(strings: TemplateStringsArray, ...values: any[]): Promise<any[]>;
}
// @filename: index.js
// ---cut---
import { query } from '$app/server';

export const time = query.stream(async function* () {
while (true) {
yield new Date();
await new Promise(r => setTimeout(r, 1000))
}
});
```

You can consume the stream like a promise or via the `current` property. In both cases, if it's used in a reactive context, it will automatically update to the latest version upon retrieving new data.

```svelte
<!--- file: src/routes/+page.svelte --->
<script>
import { time } from './time.remote.js';
</script>

<p>{await time()}</p>
<p>{time().current}</p>
```

Apart from that you can iterate over it like any other async iterable, including using `for await (...)`.

```svelte
<!--- file: src/routes/+page.svelte --->
<script>
import { time } from './time.remote.js';

let times = $state([]);

async function stream() {
times = []
let count = 0;

for await (const entry of time()) {
times.push(time);
count++;
if (count >= 5) {
break;
}
}
})
</script>

<button onclick={stream}>stream for five seconds</button>

{#each times as time}
<span>{time}</time>
{/each}
```

Unlike other `query` methods, stream requests to the same resource with the same payload are _not_ deduplicated. That means you can start the same stream multiple times in parallel and it will start from the beginning each time.

```svelte
<!--- file: src/routes/+page.svelte --->
<script>
import { oneToTen } from './count.remote.js';

const stream = oneToTen();
</script>

<!-- these are one single ReadableStream request since they share the same stream instance -->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReadableStream?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream - the comment is meant as "these are not two network requests, it's one because they share the same stream instance"

I guess I could've written that instead ... 😅

{#await stream()}
{#await stream()}

<!-- this is a separate instance and will create a new ReadableStream request to the backend -->
{await oneToTen()}
```

> [!NOTE] Be careful when using `query.stream` in combination with service workers. Specifically, make sure to never pass the promise of a `ReadableStream` (which `query.stream` uses) to `event.respondWith(...)`, as the promise never settles.

## form

The `form` function makes it easy to write data to the server. It takes a callback that receives the current [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)...
Expand Down
11 changes: 9 additions & 2 deletions packages/kit/src/exports/internal/remote-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ export function validate_remote_functions(module, file) {
}

for (const name in module) {
const type = module[name]?.__?.type;
const type = /** @type {import('types').RemoteInfo['type']} */ (module[name]?.__?.type);

if (type !== 'form' && type !== 'command' && type !== 'query' && type !== 'prerender') {
if (
type !== 'form' &&
type !== 'command' &&
type !== 'query' &&
type !== 'query_batch' &&
type !== 'query_stream' &&
type !== 'prerender'
) {
throw new Error(
`\`${name}\` exported from ${file} is invalid — all exports from this file must be remote functions`
);
Expand Down
10 changes: 10 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,16 @@ export interface RemoteQueryOverride {
release(): void;
}

/**
* The return value of a remote `query.stream` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation.
*/
export type RemoteQueryStream<T> = RemoteResource<T> & AsyncIterable<Awaited<T>>;

/**
* The return value of a remote `query.stream` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query-stream) for full documentation.
*/
export type RemoteQueryStreamFunction<Input, Output> = (arg: Input) => RemoteQueryStream<Output>;

/**
* The return value of a remote `prerender` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
*/
Expand Down
Loading
Loading