UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

295 lines (272 loc) • 9.2 kB
/** @import { RemoteLiveQuery, RemoteLiveQueryFunction, RemoteQuery, RemoteQueryFunction, RequestedResult, RemoteQueryRequestedResult, RemoteLiveQueryRequestedResult } from '$app/server' */ /** @import { MaybePromise, RemoteAnyQueryInternals } from 'types' */ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_key, parse_remote_arg } from '../../../shared.js'; import { noop } from '../../../../utils/functions.js'; import { get_cache } from './shared.js'; import { refresh } from './query.js'; import * as e from '../../../../messages/server-errors.js'; /** * Inside a remote `command` or `form` callback, returns an iterable * of `{ arg, query }` entries for the query instances the client asked to refresh, up to * the supplied `limit`. Each `query` is a `RemoteQuery` bound to the original * client-side cache key, so `refresh()` / `set()` propagate correctly even when * the query's schema transforms the input. `arg` is the *validated* argument, * i.e. the value after the schema has run (so `InferOutput<Schema>` for queries * declared with a Standard Schema). * * Arguments that fail validation or exceed `limit` are recorded as failures in * the response to the client. * See [Client-requested refreshes](https://svelte.dev/docs/kit/remote-functions#Single-flight-mutations-Client-requested-refreshes) * for usage in a remote `command` or `form`. * * @example * ```ts * import { requested } from '$app/server'; * * for (const { arg, query } of requested(getPost, 5)) { * // `arg` is the validated argument; `query` is bound to the client's * // cache key. It's safe to throw away this promise -- SvelteKit will * // await it and forward any errors to the client. * void query.refresh(); * } * ``` * * As a shorthand for the above, you can also call `refreshAll` on the result: * * @example * ```ts * import { requested } from '$app/server'; * * await requested(getPost, 5).refreshAll(); * ``` * * Works with `query.batch` as well — refreshes for individual entries are * collected into a single batched call. * * For live queries, the same applies, but with `reconnect` and `reconnectAll`. * * @template Input * @template Output * @template [Validated=Input] * @overload * @param {RemoteQueryFunction<Input, Output, Validated>} query * @param {number} limit * @returns {RemoteQueryRequestedResult<Validated, Output>} */ /** * Inside a remote `command` or `form` callback, returns an iterable * of `{ arg, query }` entries for the live query instances the client asked to reconnect, up to * the supplied `limit`. Each `query` is a `RemoteLiveQuery` bound to the original * client-side cache key, so `reconnect()` propagates correctly even when * the query's schema transforms the input. `arg` is the *validated* argument. * * Arguments that fail validation or exceed `limit` are recorded as failures in * the response to the client. * See [Client-requested refreshes](https://svelte.dev/docs/kit/remote-functions#Single-flight-mutations-Client-requested-refreshes) * for usage in a remote `command` or `form`. * * @example * ```ts * import { requested } from '$app/server'; * * for (const { query } of requested(getPost, 5)) { * void query.reconnect(); * } * ``` * * As a shorthand, you can also call `reconnectAll` on the result: * * @example * ```ts * import { requested } from '$app/server'; * * await requested(getPost, 5).reconnectAll(); * ``` * * @template Input * @template Output * @template [Validated=Input] * @overload * @param {RemoteLiveQueryFunction<Input, Output, Validated>} query * @param {number} limit * @returns {RemoteLiveQueryRequestedResult<Validated, Output>} */ /** * @template Input * @template Output * @template [Validated=Input] * @param {RemoteQueryFunction<Input, Output, Validated> | RemoteLiveQueryFunction<Input, Output, Validated>} query * @param {number} limit * @returns {RequestedResult<Validated, Output>} */ export function requested(query, limit) { const { event, state } = get_request_store(); const internals = /** @type {RemoteAnyQueryInternals | undefined} */ ( /** @type {any} */ (query).__ ); if ( internals?.type !== 'query' && internals?.type !== 'query_batch' && internals?.type !== 'query_live' ) { e.remote_requested_invalid_query(); } // narrow-stable alias so generator closures below don't lose the narrowing const __ = internals; const requested = state.remote.requested; const payloads = requested?.get(__.id) ?? new Set(); const ignored = (state.remote.ignored ??= new Set()); /** @param {string} payload */ const consume = (payload) => { payloads.delete(payload); if (payloads.size === 0) requested?.delete(__.id); }; /** @param {string} payload */ const create_ignore = (payload) => () => { ignored.add(create_remote_key(__.id, payload)); }; // note: don't initialize these maps here -- they will be initialized by the // command/form wrapper when we enter them, and if we initialize them here // we will enable requested(...) in contexts where it shouldn't be allowed, // such as load functions or other server functions if (!state.is_in_remote_form_or_command) { e.remote_requested_context(); } const [selected, skipped] = split_limit([...payloads], limit); /** * Registers the failure exactly like `.set()` registers a value: the error record * is serialized to the client (putting the query there into a failed state), and * subsequent server-side calls of the query with the same argument reject with it. * @param {string} payload * @param {unknown} error */ const record_failure = (payload, error) => { const promise = Promise.reject(error); promise.catch(noop); get_cache(__, state)[payload] = promise; refresh(event, state, __, payload, () => promise); }; for (const payload of skipped) consume(payload); const result = { *[Symbol.iterator]() { for (const payload of selected) { consume(payload); try { const parsed = parse_remote_arg(payload); const validated = __.validate(parsed); if (is_thenable(validated)) { e.remote_requested_async_validator({ name: __.name, limit: String(limit) }); } yield { arg: validated, query: __.bind(payload, validated), ignore: create_ignore(payload) }; } catch (error) { record_failure(payload, error); continue; } } }, async *[Symbol.asyncIterator]() { yield* race_all(selected, async (payload) => { consume(payload); try { const parsed = parse_remote_arg(payload); const validated = await __.validate(parsed); return { arg: validated, query: __.bind(payload, validated), ignore: create_ignore(payload) }; } catch (error) { record_failure(payload, error); throw new Error(`Skipping ${__.name}(${payload})`, { cause: error }); } }); }, async refreshAll() { if (__.type === 'query_live') { e.remote_requested_wrong_method({ method: 'refreshAll', type: 'live', replacement: 'reconnectAll' }); } for await (const { query } of result) { void (/** @type {RemoteQuery<Output>} */ (query).refresh()); } }, async reconnectAll() { if (__.type !== 'query_live') { e.remote_requested_wrong_method({ method: 'reconnectAll', type: 'regular', replacement: 'refreshAll' }); } for await (const { query } of result) { void (/** @type {RemoteLiveQuery<Output>} */ (query).reconnect()); } }, async ignoreAll() { for await (const { ignore } of result) ignore(); } }; return /** @type {RequestedResult<Validated, Output>} */ (/** @type {unknown} */ (result)); } /** * @template T * @param {Array<T>} array * @param {number} limit * @returns {[Array<T>, Array<T>]} */ function split_limit(array, limit) { if (limit === Infinity) { return [array, []]; } if (!Number.isInteger(limit) || limit < 0) { e.remote_requested_invalid_limit(); } return [array.slice(0, limit), array.slice(limit)]; } /** * @param {any} value * @returns {value is PromiseLike<any>} */ function is_thenable(value) { return !!value && (typeof value === 'object' || typeof value === 'function') && 'then' in value; } /** * Runs all callbacks immediately and yields resolved values in completion order. * If the promise rejects, it is skipped. * * @template T * @template R * @param {Array<T>} array * @param {(value: T) => MaybePromise<R>} fn * @returns {AsyncIterable<R>} */ async function* race_all(array, fn) { /** @type {Set<Promise<{ promise: Promise<any>, value: Awaited<R> }>>} */ const pending = new Set(); for (const value of array) { /** @type {Promise<{ promise: Promise<any>, value: Awaited<R> }>} */ const promise = Promise.resolve(fn(value)).then((result) => ({ promise, value: result })); promise.catch(() => pending.delete(promise)); pending.add(promise); } while (pending.size > 0) { try { const { promise, value } = await Promise.race(pending); pending.delete(promise); yield value; } catch { // Ignore errors, they are handled in the fn callback and result in skip } } }