-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat: experimental query.live remote function
#15563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aaebc03
af10f8b
9fd9c25
92afb7d
29d67e9
704eec4
c69d5ee
1a6ba2f
1b8991b
872710b
1d0fff3
1545da9
54ad84e
dd9db79
da1e41c
f0dadad
19f5e96
84d4d78
fa1fb0b
6cc8478
2fb9c67
af19c4b
165e514
cf5c566
31e80f2
a06f3cf
e271658
7fb2d8e
b579163
c228920
6fe92bf
c036ff7
dd63c1c
c380c4b
3372198
53815f5
3a63d78
ff17ca9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@sveltejs/kit': minor | ||
| --- | ||
|
|
||
| feat: experimental `query.live` function |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2170,6 +2170,21 @@ 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(): Promise<AsyncIterator<T>>; | ||
| /** `true` if the live stream is currently connected. */ | ||
| readonly connected: boolean; | ||
| /** `true` once the live stream iterator has completed. */ | ||
| readonly finished: boolean; | ||
| /** Reconnects the live stream immediately. */ | ||
| reconnect(): void; | ||
| }; | ||
|
|
||
| export interface RemoteQueryOverride { | ||
| _key: string; | ||
| release(): void; | ||
|
|
@@ -2189,4 +2204,11 @@ export type RemoteQueryFunction<Input, Output> = ( | |
| arg: undefined extends Input ? Input | void : Input | ||
| ) => RemoteQuery<Output>; | ||
|
|
||
| /** | ||
| * The return value of a remote `query.live` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.live) for full documentation. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This JSDoc is a little confusing. It's true that this is literally what
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Codex was just following the convention set by the other remote functions. Open to changing it but we should probably change them all |
||
| */ | ||
| export type RemoteLiveQueryFunction<Input, Output> = ( | ||
| arg: undefined extends Input ? Input | void : Input | ||
| ) => RemoteLiveQuery<Output>; | ||
|
|
||
| export * from './index.js'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /** @import { RemoteQuery, RemoteQueryFunction } from '@sveltejs/kit' */ | ||
| /** @import { RemoteInternals, MaybePromise, RequestState, RemoteQueryBatchInternals, RemoteQueryInternals } from 'types' */ | ||
| /** @import { RemoteLiveQuery, RemoteLiveQueryFunction, RemoteQuery, RemoteQueryFunction } from '@sveltejs/kit' */ | ||
| /** @import { RemoteInternals, MaybePromise, RequestState, RemoteQueryLiveInternals, RemoteQueryBatchInternals, RemoteQueryInternals } from 'types' */ | ||
| /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ | ||
| import { get_request_store } from '@sveltejs/kit/internal/server'; | ||
| import { create_remote_key, stringify, stringify_remote_arg } from '../../../shared.js'; | ||
|
|
@@ -84,6 +84,103 @@ export function query(validate_or_fn, maybe_fn) { | |
| return wrapper; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a live remote query. When called from the browser, the function will be invoked on the server via a streaming `fetch` call. | ||
| * | ||
| * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.live) for full documentation. | ||
| * | ||
| * @template Output | ||
| * @overload | ||
| * @param {(arg: void) => MaybePromise<Generator<Output> | AsyncIterator<Output> | AsyncIterable<Output>>} fn | ||
| * @returns {RemoteLiveQueryFunction<void, Output>} | ||
| */ | ||
| /** | ||
| * @template Input | ||
| * @template Output | ||
| * @overload | ||
| * @param {'unchecked'} validate | ||
| * @param {(arg: Input) => MaybePromise<Generator<Output> | AsyncIterator<Output> | AsyncIterable<Output>>} fn | ||
| * @returns {RemoteLiveQueryFunction<Input, Output>} | ||
| */ | ||
| /** | ||
| * @template {StandardSchemaV1} Schema | ||
| * @template Output | ||
| * @overload | ||
| * @param {Schema} schema | ||
| * @param {(arg: StandardSchemaV1.InferOutput<Schema>) => MaybePromise<Generator<Output> | AsyncIterator<Output> | AsyncIterable<Output>>} fn | ||
| * @returns {RemoteLiveQueryFunction<StandardSchemaV1.InferInput<Schema>, Output>} | ||
| */ | ||
| /** | ||
| * @template Input | ||
| * @template Output | ||
| * @param {any} validate_or_fn | ||
| * @param {(args: Input) => MaybePromise<Generator<Output> | AsyncIterator<Output> | AsyncIterable<Output>>} [maybe_fn] | ||
| * @returns {RemoteLiveQueryFunction<Input, Output>} | ||
| */ | ||
| /*@__NO_SIDE_EFFECTS__*/ | ||
| function live(validate_or_fn, maybe_fn) { | ||
| /** @type {(arg: Input) => MaybePromise<Generator<Output> | AsyncIterator<Output> | AsyncIterable<Output>>} */ | ||
| const fn = maybe_fn ?? validate_or_fn; | ||
|
|
||
| /** @type {(arg?: any) => MaybePromise<Input>} */ | ||
| const validate = create_validator(validate_or_fn, maybe_fn); | ||
|
|
||
| /** | ||
| * @param {any} event | ||
| * @param {any} state | ||
| * @param {any} arg | ||
| */ | ||
| const run = async (event, state, arg) => { | ||
| return await run_remote_function( | ||
| event, | ||
| state, | ||
| false, | ||
| () => validate(arg), | ||
| async (input) => to_async_iterator(await fn(input), __.name) | ||
| ); | ||
| }; | ||
|
|
||
| /** @type {RemoteQueryLiveInternals} */ | ||
| const __ = { type: 'query_live', id: '', name: '', run }; | ||
|
|
||
| /** @type {RemoteLiveQueryFunction<Input, Output> & { __: RemoteQueryLiveInternals }} */ | ||
| const wrapper = (arg) => { | ||
| if (prerendering) { | ||
| throw new Error( | ||
| `Cannot call query.live '${__.name}' while prerendering, as prerendered pages need static data. Use 'prerender' from $app/server instead` | ||
| ); | ||
| } | ||
|
|
||
| const { event, state } = get_request_store(); | ||
|
|
||
| return create_live_query_resource( | ||
| __, | ||
| arg, | ||
| state, | ||
| async () => { | ||
| const iterator = await run(event, state, arg); | ||
|
|
||
| try { | ||
| const { value, done } = await iterator.next(); | ||
|
|
||
| if (done) { | ||
| throw new Error(`query.live '${__.name}' did not yield a value`); | ||
| } | ||
Rich-Harris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return value; | ||
| } finally { | ||
| await iterator.return?.(); | ||
| } | ||
| }, | ||
| async () => run(event, state, arg) | ||
| ); | ||
| }; | ||
|
|
||
| Object.defineProperty(wrapper, '__', { value: __ }); | ||
|
|
||
| return wrapper; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a batch query function that collects multiple calls and executes them in a single request | ||
| * | ||
|
|
@@ -305,8 +402,90 @@ function create_query_resource(__, arg, state, fn) { | |
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @param {RemoteQueryLiveInternals} __ | ||
| * @param {any} arg | ||
| * @param {RequestState} state | ||
| * @param {() => Promise<any>} get_first_value | ||
| * @param {() => MaybePromise<AsyncIterator<any>>} get_iterator | ||
| * @returns {RemoteLiveQuery<any>} | ||
| */ | ||
| function create_live_query_resource(__, arg, state, get_first_value, get_iterator) { | ||
| /** @type {Promise<any> | null} */ | ||
| let promise = null; | ||
|
|
||
| const get_promise = () => { | ||
| return (promise ??= get_response(__, arg, state, get_first_value)); | ||
| }; | ||
|
|
||
| return { | ||
| /** @type {Promise<any>['catch']} */ | ||
| catch(onrejected) { | ||
| return get_promise().catch(onrejected); | ||
| }, | ||
| current: undefined, | ||
| error: undefined, | ||
| /** @type {Promise<any>['finally']} */ | ||
| finally(onfinally) { | ||
| return get_promise().finally(onfinally); | ||
| }, | ||
| finished: false, | ||
| loading: true, | ||
| ready: false, | ||
| connected: false, | ||
| reconnect() { | ||
| const reconnects = state.remote.reconnects; | ||
|
|
||
| if (!reconnects) { | ||
| throw new Error( | ||
| `Cannot call reconnect on query.live '${__.name}' because it is not executed in the context of a command/form remote function` | ||
| ); | ||
| } | ||
|
|
||
| reconnects.add(create_remote_key(__.id, stringify_remote_arg(arg, state.transport))); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably add docs for reconnecting (...single-flight reconnects?) |
||
| }, | ||
| async run() { | ||
| if (!state.is_in_universal_load) { | ||
| throw new Error( | ||
| 'On the server, .run() can only be called in universal `load` functions. Anywhere else, just await the query directly' | ||
| ); | ||
| } | ||
|
|
||
| return get_iterator(); | ||
| }, | ||
| /** @type {Promise<any>['then']} */ | ||
| then(onfulfilled, onrejected) { | ||
| return get_promise().then(onfulfilled, onrejected); | ||
| }, | ||
| get [Symbol.toStringTag]() { | ||
| return 'LiveQueryResource'; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| // Add batch as a property to the query function | ||
| Object.defineProperty(query, 'batch', { value: batch, enumerable: true }); | ||
| Object.defineProperty(query, 'live', { value: live, enumerable: true }); | ||
|
|
||
| /** | ||
| * @template T | ||
| * @param {Generator<T> | AsyncIterator<T> | AsyncIterable<T>} source | ||
| * @param {string} name | ||
| * @returns {AsyncIterator<T>} | ||
| */ | ||
| function to_async_iterator(source, name) { | ||
| const maybe = /** @type {any} */ (source); | ||
|
|
||
| if (maybe && typeof maybe[Symbol.asyncIterator] === 'function') { | ||
| return maybe[Symbol.asyncIterator](); | ||
| } | ||
|
|
||
| if (maybe && typeof maybe.next === 'function') { | ||
| return maybe; | ||
| } | ||
|
|
||
| throw new Error(`query.live '${name}' must return an AsyncIterator or AsyncIterable`); | ||
| } | ||
|
|
||
| /** | ||
| * @param {RemoteInternals} __ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kind of confusing... Can
finishgo back tofalseif you callrefresh?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. We could go with
completedinstead?