Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
aaebc03
feat: add query.live remote function support
Rich-Harris Mar 17, 2026
af10f8b
fix: make query.live await blocks update with stream values
Rich-Harris Mar 17, 2026
9fd9c25
fix: cancel live query streams on disconnect and reload
Rich-Harris Mar 17, 2026
92afb7d
regenerate types
Rich-Harris Mar 18, 2026
29d67e9
tweak
Rich-Harris Mar 19, 2026
704eec4
remove plan doc
Rich-Harris Mar 19, 2026
c69d5ee
tidy up
Rich-Harris Mar 19, 2026
1a6ba2f
fix test
Rich-Harris Mar 19, 2026
1b8991b
simplify
Rich-Harris Mar 19, 2026
872710b
changeset
Rich-Harris Mar 19, 2026
1d0fff3
revert
Rich-Harris Mar 19, 2026
1545da9
fix types
Rich-Harris Mar 19, 2026
54ad84e
tighten up types a bit
Rich-Harris Mar 19, 2026
dd9db79
merge
Rich-Harris Mar 19, 2026
da1e41c
fix typechecking error
Rich-Harris Mar 19, 2026
f0dadad
better naming
Rich-Harris Mar 19, 2026
19f5e96
tweak docs
Rich-Harris Mar 19, 2026
84d4d78
tweak docs
Rich-Harris Mar 19, 2026
fa1fb0b
merge main
Rich-Harris Mar 19, 2026
6cc8478
get rid of second argument, we dont need it
Rich-Harris Mar 19, 2026
2fb9c67
unnecessary
Rich-Harris Mar 19, 2026
af19c4b
unnecessary
Rich-Harris Mar 19, 2026
165e514
revert
Rich-Harris Mar 19, 2026
cf5c566
revert
Rich-Harris Mar 19, 2026
31e80f2
tidy
Rich-Harris Mar 19, 2026
a06f3cf
tweak
Rich-Harris Mar 19, 2026
e271658
not sure what this is for
Rich-Harris Mar 19, 2026
7fb2d8e
tidy up
Rich-Harris Mar 20, 2026
b579163
goddammit codex was right
Rich-Harris Mar 20, 2026
c228920
don't attempt to reconnect to a finished live query
Rich-Harris Mar 20, 2026
6fe92bf
fix
Rich-Harris Mar 20, 2026
c036ff7
lint
Rich-Harris Mar 22, 2026
dd63c1c
allow server to trigger a reconnect
Rich-Harris Mar 23, 2026
c380c4b
reconnect to finished live query
Rich-Harris Mar 23, 2026
3372198
hopefully fix preview deploy
Rich-Harris Mar 23, 2026
53815f5
dedupe
Rich-Harris Mar 23, 2026
3a63d78
tweak
Rich-Harris Mar 23, 2026
ff17ca9
slightly better error handling
Rich-Harris Mar 23, 2026
ff80288
Merge remote-tracking branch 'origin/main' into query-live
elliott-with-the-longest-name-on-github Apr 6, 2026
7f1661c
finished => completed... maybe not happy yet
elliott-with-the-longest-name-on-github Apr 6, 2026
4b06b4f
jsdoc
elliott-with-the-longest-name-on-github Apr 6, 2026
58b9b8b
docs
elliott-with-the-longest-name-on-github Apr 6, 2026
b41ba93
invalidateAll hooks
elliott-with-the-longest-name-on-github Apr 7, 2026
1a738df
requested support
elliott-with-the-longest-name-on-github Apr 8, 2026
25214c9
meh
elliott-with-the-longest-name-on-github Apr 8, 2026
9f08de3
requested and stuff
elliott-with-the-longest-name-on-github Apr 9, 2026
6038cf6
done
elliott-with-the-longest-name-on-github Apr 9, 2026
1315e09
remove destroy
elliott-with-the-longest-name-on-github Apr 9, 2026
58cdccb
server cleanup
elliott-with-the-longest-name-on-github Apr 10, 2026
2c583ff
text decoder bug fixes
elliott-with-the-longest-name-on-github Apr 10, 2026
8165a36
oops types
elliott-with-the-longest-name-on-github Apr 10, 2026
5a01a1c
Merge remote-tracking branch 'origin/main' into query-live
elliott-with-the-longest-name-on-github Apr 10, 2026
6e197e5
more
elliott-with-the-longest-name-on-github Apr 11, 2026
91d0f24
ho boy
elliott-with-the-longest-name-on-github Apr 11, 2026
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/cuddly-rats-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

breaking: on the server, make the promise returned from `refresh` represent adding the refresh to the map, not the time it takes to run the remote function
5 changes: 5 additions & 0 deletions .changeset/wet-rings-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: experimental `query.live` function
62 changes: 62 additions & 0 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,45 @@ export const getWeather = query.batch(v.string(), async (cityIds) => {
{/if}
```

## query.live

`query.live` is for accessing real-time data from the server. It behaves similarly to `query`, but the callback — typically an async [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function*) — returns an `AsyncIterable`:

```js
import { query } from '$app/server';

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

During server-side rendering, `await getTime()` returns the first yielded value then closes the iterator. This initial value is serialized and reused during hydration.

On the client, the query stays connected while it's actively used in a component. Multiple instances share a connection. When there are no active uses left, the stream disconnects and server-side iteration is stopped.

Live queries expose a `connected` property and `reconnect()` method:

```svelte
<script>
import { getTime } from './time.remote.js';

const time = getTime();
</script>

<p>{await time}</p>
<p>connected: {time.connected}</p>
<button onclick={() => time.reconnect()}>Reconnect</button>
```

If the connection drops, `connected` becomes `false`. SvelteKit will attempt to reconnect passively, with exponential backoff, and actively if `navigator.onLine` goes from `false` to `true`.

Unlike `query`, live queries do not have a `refresh()` method, as they are self-updating.

As with `query` and `query.batch`, call `.run()` outside render when you need imperative access. For live queries, `run()` returns a `Promise<AsyncIterator<T>>`.

## form

The `form` function makes it easy to write data to the server. It takes a callback that receives `data` constructed from the submitted [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)...
Expand Down Expand Up @@ -974,6 +1013,29 @@ export const updatePost = form(

Because queries are keyed based on their arguments, `await getPost(post.id).set(result)` on the server knows to look up the matching `getPost(id)` on the client to update it. The same goes for `getPosts().refresh()` -- it knows to look up `getPosts()` with no argument on the client.

### Reconnecting live queries in mutations

Single-flight mutations can also reconnect `query.live` instances. In a `form`/`command` handler, call `.reconnect()` on the live query resource you want to reconnect:

```js
import * as v from 'valibot';
import { form, query } from '$app/server';

export const getNotifications = query.live(v.string(), async function* (userId) {
while (true) {
yield await db.notifications(userId);
await wait(1000);
}
});

export const markAllRead = form(v.object({ userId: v.string() }), async ({ userId }) => {
// mutation logic...
+++getNotifications(userId).reconnect();+++
});
```

This schedules a reconnect for the matching active client instances and applies it as part of the mutation response (i.e. in the same flight as the form/command result).

### Client-requested refreshes

Unfortunately, life isn't always as simple as the preceding example. The server always knows which query _functions_ to update, but it may not know which specific query _instances_ to update. For example, if `getPosts({ filter: 'author:santa' })` is rendered on the client, calling `getPosts().refresh()` in the server handler won't update it. You'd need to call `getPosts({ filter: 'author:santa' }).refresh()` instead — but how could you know which specific combinations of filters are currently rendered on the client, especially if your query argument is more complicated than an object with just one key?
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/exports/internal/remote-functions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @import { RemoteInternals } from 'types' */

/** @type {RemoteInternals['type'][]} */
const types = ['command', 'form', 'prerender', 'query', 'query_batch'];
const types = ['command', 'form', 'prerender', 'query', 'query_batch', 'query_live'];

/**
* @param {Record<string, any>} module
Expand Down
52 changes: 47 additions & 5 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1452,7 +1452,9 @@ export interface Page<
*/
export type ParamMatcher = (param: string) => boolean;

export type RequestedResult<T> = Iterable<T> &
export type RequestedResult<T> = QueryRequestedResult<T> | LiveQueryRequestedResult<T>;

export type QueryRequestedResult<T> = Iterable<T> &
AsyncIterable<T> & {
/**
* Call `refresh` on all queries selected by this `requested` invocation.
Expand All @@ -1468,6 +1470,22 @@ export type RequestedResult<T> = Iterable<T> &
refreshAll: () => Promise<void>;
};

export type LiveQueryRequestedResult<T> = Iterable<T> &
AsyncIterable<T> & {
/**
* Call `reconnect` on all live queries selected by this `requested` invocation.
* This is identical to:
* ```ts
* import { requested } from '$app/server';
*
* for await (const arg of requested(query, ...)) {
* query(arg).reconnect();
* }
* ```
*/
reconnectAll: () => Promise<void>;
};

export interface RequestEvent<
Params extends AppLayoutParams<'/'> = AppLayoutParams<'/'>,
RouteId extends AppRouteId | null = AppRouteId | null
Expand Down Expand Up @@ -2079,7 +2097,7 @@ export interface ValidationError {
}

/**
* The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
* The type of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
*/
export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
/** Attachment that sets up an event handler that intercepts the form submission on the client to prevent a full page reload */
Expand Down Expand Up @@ -2134,7 +2152,7 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
};

/**
* The return value of a remote `command` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#command) for full documentation.
* The type of a remote `command` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#command) for full documentation.
*/
export type RemoteCommand<Input, Output> = {
(arg: undefined extends Input ? Input | void : Input): Promise<Output> & {
Expand All @@ -2146,7 +2164,9 @@ export type RemoteCommand<Input, Output> = {

export type RemoteQueryUpdate =
| RemoteQuery<any>
| RemoteLiveQuery<any>
| RemoteQueryFunction<any, any>
| RemoteLiveQueryFunction<any, any>
| RemoteQueryOverride;

export type RemoteResource<T> = Promise<T> & {
Expand Down Expand Up @@ -2210,20 +2230,42 @@ export type RemoteQuery<T> = RemoteResource<T> & {
withOverride(update: (current: T) => T): RemoteQueryOverride;
};

export type RemoteLiveQuery<T> = RemoteResource<T> & {
/**
* Returns an async iterator with live updates.
* Unlike awaiting the resource directly, this can only be used _outside_ render
* (i.e. in load functions, event handlers and so on)
*/
run(): AsyncGenerator<T>;
/** `true` if the live stream is currently connected. */
readonly connected: boolean;
/** `true` once the current live stream iterator is done. */
readonly done: boolean;
/** Reconnects the live stream immediately. */
reconnect(): Promise<void>;
};

export type RemoteQueryOverride = () => void;

/**
* The return value of a remote `prerender` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
* The type of a remote `prerender` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
*/
export type RemotePrerenderFunction<Input, Output> = (
arg: undefined extends Input ? Input | void : Input
) => RemoteResource<Output>;

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

/**
* The type of a remote `query.live` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.live) for full documentation.
*/
export type RemoteLiveQueryFunction<Input, Output> = (
arg: undefined extends Input ? Input | void : Input
) => RemoteLiveQuery<Output>;

export * from './index.js';
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/app/server/remote/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function command(validate_or_fn, maybe_fn) {
const fn = maybe_fn ?? validate_or_fn;

/** @type {(arg?: any) => MaybePromise<Input>} */
const validate = create_validator(validate_or_fn, maybe_fn);
const validate = create_validator(() => __, validate_or_fn, maybe_fn);

/** @type {RemoteCommandInternals} */
const __ = { type: 'command', id: '', name: '' };
Expand All @@ -77,7 +77,8 @@ export function command(validate_or_fn, maybe_fn) {
);
}

state.remote.refreshes ??= {};
state.remote.refreshes ??= new Map();
state.remote.reconnects ??= new Map();

const promise = Promise.resolve(
run_remote_function(event, state, true, () => validate(arg), fn)
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/app/server/remote/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ export function form(validate_or_fn, maybe_fn) {
data = validated.value;
}

state.remote.refreshes ??= {};
state.remote.refreshes ??= new Map();
state.remote.reconnects ??= new Map();

const issue = create_issues();

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 @@ -75,7 +75,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
const fn = maybe_fn ?? validate_or_fn;

/** @type {(arg?: any) => MaybePromise<Input>} */
const validate = create_validator(validate_or_fn, maybe_fn);
const validate = create_validator(() => __, validate_or_fn, maybe_fn);

/** @type {RemotePrerenderInternals} */
const __ = {
Expand Down
Loading
Loading