UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

611 lines (548 loc) 18.7 kB
/** @import { RemoteLiveQuery, RemoteLiveQueryFunction, RemoteQuery, RemoteQueryFunction } from '@sveltejs/kit' */ /** @import { RemoteInternals, MaybePromise, RequestState, RemoteQueryLiveInternals, RemoteQueryBatchInternals, RemoteQueryInternals, RemoteLiveQueryUserFunctionReturnType } 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'; import { prerendering } from '__sveltekit/environment'; import { create_validator, get_cache, get_response, run_remote_function, run_remote_generator } from './shared.js'; import { handle_error_and_jsonify } from '../../../server/utils.js'; import { HttpError, SvelteKitError } from '@sveltejs/kit/internal'; import { noop } from '../../../../utils/functions.js'; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query) for full documentation. * * @template Output * @overload * @param {() => MaybePromise<Output>} fn * @returns {RemoteQueryFunction<void, Output>} * @since 2.27 */ /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query) for full documentation. * * @template Input * @template Output * @overload * @param {'unchecked'} validate * @param {(arg: Input) => MaybePromise<Output>} fn * @returns {RemoteQueryFunction<Input, Output>} * @since 2.27 */ /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query) for full documentation. * * @template {StandardSchemaV1} Schema * @template Output * @overload * @param {Schema} schema * @param {(arg: StandardSchemaV1.InferOutput<Schema>) => MaybePromise<Output>} fn * @returns {RemoteQueryFunction<StandardSchemaV1.InferInput<Schema>, Output, StandardSchemaV1.InferOutput<Schema>>} * @since 2.27 */ /** * @template Input * @template Output * @param {any} validate_or_fn * @param {(args?: Input) => MaybePromise<Output>} [maybe_fn] * @returns {RemoteQueryFunction<Input, Output>} * @since 2.27 */ /*@__NO_SIDE_EFFECTS__*/ export function query(validate_or_fn, maybe_fn) { /** @type {(arg?: Input) => Output} */ const fn = maybe_fn ?? validate_or_fn; /** @type {(arg?: any) => MaybePromise<Input>} */ const validate = create_validator(validate_or_fn, maybe_fn); /** @type {RemoteQueryInternals} */ const __ = { type: 'query', id: '', name: '', validate, bind(payload, validated_arg) { const { event, state } = get_request_store(); return create_query_resource(__, payload, state, () => run_remote_function(event, state, false, () => validated_arg, fn) ); } }; /** @type {RemoteQueryFunction<Input, Output> & { __: RemoteQueryInternals }} */ const wrapper = (arg) => { if (prerendering) { throw new Error( `Cannot call query '${__.name}' while prerendering, as prerendered pages need static data. Use 'prerender' from $app/server instead` ); } const { event, state } = get_request_store(); const payload = stringify_remote_arg(arg, state.transport); return create_query_resource(__, payload, state, () => run_remote_function(event, state, false, () => validate(arg), fn) ); }; Object.defineProperty(wrapper, '__', { value: __ }); 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) => RemoteLiveQueryUserFunctionReturnType<Output>} fn * @returns {RemoteLiveQueryFunction<void, Output>} */ /** * @template Input * @template Output * @overload * @param {'unchecked'} validate * @param {(arg: Input) => RemoteLiveQueryUserFunctionReturnType<Output>} fn * @returns {RemoteLiveQueryFunction<Input, Output>} */ /** * @template {StandardSchemaV1} Schema * @template Output * @overload * @param {Schema} schema * @param {(arg: StandardSchemaV1.InferOutput<Schema>) => RemoteLiveQueryUserFunctionReturnType<Output>} fn * @returns {RemoteLiveQueryFunction<StandardSchemaV1.InferInput<Schema>, Output, StandardSchemaV1.InferOutput<Schema>>} */ /** * @template Input * @template Output * @param {any} validate_or_fn * @param {(args: Input) => RemoteLiveQueryUserFunctionReturnType<Output>} [maybe_fn] * @returns {RemoteLiveQueryFunction<Input, Output>} */ /*@__NO_SIDE_EFFECTS__*/ function live(validate_or_fn, maybe_fn) { /** @type {(arg: Input) => RemoteLiveQueryUserFunctionReturnType<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} get_input */ const run = (event, state, get_input) => run_remote_generator(event, state, false, get_input, fn, __.name); /** * @param {any} generator * @returns {Promise<any>} */ const first_value = async (generator) => { try { const { value, done } = await generator.next(); if (done) { throw new Error(`query.live '${__.name}' did not yield a value`); } return value; } finally { await generator.return(undefined); } }; /** @type {RemoteQueryLiveInternals} */ const __ = { type: 'query_live', id: '', name: '', run: (event, state, arg) => run(event, state, () => validate(arg)), validate, bind(payload, validated_arg) { const { event, state } = get_request_store(); return create_live_query_resource(__, payload, state, () => first_value(run(event, state, () => validated_arg)) ); } }; /** @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(); const payload = stringify_remote_arg(arg, state.transport); return create_live_query_resource(__, payload, state, () => first_value(run(event, state, () => validate(arg))) ); }; Object.defineProperty(wrapper, '__', { value: __ }); return wrapper; } /** * Creates a batch query function that collects multiple calls and executes them in a single request * * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.batch) for full documentation. * * @template Input * @template Output * @overload * @param {'unchecked'} validate * @param {(args: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>} fn * @returns {RemoteQueryFunction<Input, Output>} * @since 2.35 */ /** * Creates a batch query function that collects multiple calls and executes them in a single request * * See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.batch) for full documentation. * * @template {StandardSchemaV1} Schema * @template Output * @overload * @param {Schema} schema * @param {(args: StandardSchemaV1.InferOutput<Schema>[]) => MaybePromise<(arg: StandardSchemaV1.InferOutput<Schema>, idx: number) => Output>} fn * @returns {RemoteQueryFunction<StandardSchemaV1.InferInput<Schema>, Output, StandardSchemaV1.InferOutput<Schema>>} * @since 2.35 */ /** * @template Input * @template Output * @param {any} validate_or_fn * @param {(args?: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>} [maybe_fn] * @returns {RemoteQueryFunction<Input, Output>} * @since 2.35 */ /*@__NO_SIDE_EFFECTS__*/ function batch(validate_or_fn, maybe_fn) { /** @type {(args?: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>} */ const fn = maybe_fn ?? validate_or_fn; /** @type {(arg?: any) => MaybePromise<Input>} */ const validate = create_validator(validate_or_fn, maybe_fn); /** * Enqueues a single call into the current batch (creating one if necessary) * and returns a promise that resolves with the result for this entry. * * @param {string} payload — the stringified raw argument (cache key) * @param {() => MaybePromise<any>} get_validated — produces the validated argument for this entry * @returns {Promise<any>} */ const enqueue = (payload, get_validated) => { const { event, state } = get_request_store(); return new Promise((resolve, reject) => { const batches = (state.remote.batches ??= /** @type {NonNullable<typeof state.remote.batches>} */ (new Map())); let batched = batches.get(__.id); if (!batched) { batched = new Map(); batches.set(__.id, batched); } const entry = batched.get(payload); if (entry) { entry.resolvers.push({ resolve, reject }); return; } batched.set(payload, { get_validated, resolvers: [{ resolve, reject }] }); if (batched.size > 1) return; setTimeout(async () => { batches.delete(__.id); const entries = Array.from(batched.values()); try { return await run_remote_function( event, state, false, async () => Promise.all(entries.map((entry) => entry.get_validated())), async (input) => { const get_result = await fn(input); for (let i = 0; i < entries.length; i++) { try { const result = get_result(input[i], i); for (const resolver of entries[i].resolvers) { resolver.resolve(result); } } catch (error) { for (const resolver of entries[i].resolvers) { resolver.reject(error); } } } } ); } catch (error) { for (const entry of batched.values()) { for (const resolver of entry.resolvers) { resolver.reject(error); } } } }, 0); }); }; /** @type {RemoteQueryBatchInternals} */ const __ = { type: 'query_batch', id: '', name: '', validate, run: async (args, options) => { const { event, state } = get_request_store(); return run_remote_function( event, state, false, async () => Promise.all(args.map(validate)), async (/** @type {any[]} */ input) => { const get_result = await fn(input); return Promise.all( input.map(async (arg, i) => { try { const data = get_result(arg, i); return { type: 'result', data: stringify(data, state.transport) }; } catch (error) { return { type: 'error', error: await handle_error_and_jsonify(event, state, options, error), status: error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 }; } }) ); } ); }, bind(payload, validated_arg) { const { state } = get_request_store(); return create_query_resource(__, payload, state, () => enqueue(payload, () => validated_arg)); } }; /** @type {RemoteQueryFunction<Input, Output> & { __: RemoteQueryBatchInternals }} */ const wrapper = (arg) => { if (prerendering) { throw new Error( `Cannot call query.batch '${__.name}' while prerendering, as prerendered pages need static data. Use 'prerender' from $app/server instead` ); } const { state } = get_request_store(); const payload = stringify_remote_arg(arg, state.transport); return create_query_resource(__, payload, state, () => // Collect all the calls to the same query in the same macrotask, // then execute them as one backend request. enqueue(payload, () => validate(arg)) ); }; Object.defineProperty(wrapper, '__', { value: __ }); return wrapper; } /** * @param {RemoteInternals} __ * @param {string} payload — the stringified raw argument (i.e. the cache key the client will use) * @param {RequestState} state * @param {() => Promise<any>} fn * @returns {RemoteQuery<any>} */ function create_query_resource(__, payload, state, fn) { /** @type {Promise<any> | null} */ let promise = null; const get_promise = () => { return (promise ??= get_response(__, payload, state, fn)); }; const populate_hydratable = () => { // accessing data properties needs to kick off the work // so that it gets seeded in the hydration cache // and becomes available on the client void (__.id && state.is_in_render && get_promise()); }; return { /** @type {Promise<any>['catch']} */ catch(onrejected) { return get_promise().catch(onrejected); }, get current() { populate_hydratable(); return undefined; }, get error() { populate_hydratable(); return undefined; }, /** @type {Promise<any>['finally']} */ finally(onfinally) { return get_promise().finally(onfinally); }, get loading() { populate_hydratable(); return true; }, get ready() { populate_hydratable(); return false; }, refresh() { const { event } = get_request_store(); if (!event.isRemoteRequest) { // If the form submission is not a remote request, refreshing the data is // useless, because it can't be returned to the client. return Promise.resolve(); } const refresh_context = get_refresh_context(__, 'refresh', payload); const is_immediate_refresh = !refresh_context.cache[refresh_context.payload]; const value = is_immediate_refresh ? get_promise() : fn(); return update_refresh_value(refresh_context, value, is_immediate_refresh); }, run() { // potential TODO: if we want to be able to run queries at the top level of modules / outside of the request context, we could technically remove // the requirement that `state` is defined, but that's kind of an annoying change to make, so we're going to wait on that until we have any sort of // concrete use case. 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_response(__, payload, state, fn); }, /** @param {any} value */ set(value) { return update_refresh_value(get_refresh_context(__, 'set', payload), value); }, /** @type {Promise<any>['then']} */ then(onfulfilled, onrejected) { return get_promise().then(onfulfilled, onrejected); }, withOverride() { throw new Error(`Cannot call '${__.name}.withOverride()' on the server`); }, get [Symbol.toStringTag]() { return 'QueryResource'; } }; } /** * @param {RemoteQueryLiveInternals} __ * @param {string} payload — the stringified raw argument (i.e. the cache key the client will use) * @param {RequestState} state * @param {() => Promise<any>} get_first_value * @returns {RemoteLiveQuery<any>} */ function create_live_query_resource(__, payload, state, get_first_value) { /** @type {Promise<any> | null} */ let promise = null; const get_promise = () => { return (promise ??= get_response(__, payload, state, get_first_value)); }; const populate_hydratable = () => { void (__.id && state.is_in_render && get_promise()); }; return { /** @type {Promise<any>['catch']} */ catch(onrejected) { return get_promise().catch(onrejected); }, get current() { populate_hydratable(); return undefined; }, get error() { populate_hydratable(); return undefined; }, /** @type {Promise<any>['finally']} */ finally(onfinally) { return get_promise().finally(onfinally); }, get done() { populate_hydratable(); return false; }, get loading() { populate_hydratable(); return true; }, get ready() { populate_hydratable(); return false; }, get connected() { populate_hydratable(); return 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.set(create_remote_key(__.id, payload), get_promise()); return Promise.resolve(); }, run() { throw new Error('Cannot call .run() on a live query on the server'); }, /** @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 }); /** * @param {RemoteInternals} __ * @param {'set' | 'refresh'} action * @param {string} payload — the stringified raw argument (i.e. the cache key the client will use) * @returns {{ __: RemoteInternals; state: any; refreshes: Map<string, Promise<any>>; cache: Record<string, { serialize: boolean; data: any }>; refreshes_key: string; payload: string }} */ function get_refresh_context(__, action, payload) { const { state } = get_request_store(); const { refreshes } = state.remote; if (!refreshes) { const name = __.type === 'query_batch' ? `query.batch '${__.name}'` : `query '${__.name}'`; throw new Error( `Cannot call ${action} on ${name} because it is not executed in the context of a command/form remote function` ); } const cache = get_cache(__, state); const refreshes_key = create_remote_key(__.id, payload); return { __, state, refreshes, refreshes_key, cache, payload }; } /** * @param {{ __: RemoteInternals; refreshes: Map<string, Promise<any>>; cache: Record<string, { serialize: boolean; data: any }>; refreshes_key: string; payload: string }} context * @param {any} value * @param {boolean} [is_immediate_refresh=false] * @returns {Promise<void>} */ function update_refresh_value( { __, refreshes, refreshes_key, cache, payload }, value, is_immediate_refresh = false ) { const promise = Promise.resolve(value); if (!is_immediate_refresh) { cache[payload] = { serialize: true, data: promise }; } if (__.id) { refreshes.set(refreshes_key, promise); } promise.catch(noop); // we return an immediately-resolving promise so that the `refresh()` signature is consistent, // but it doesn't delay anything if awaited inside a command. this way, people aren't // penalised if they do `await q1.refresh(); await q2.refresh()` return Promise.resolve(); }