UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

294 lines (250 loc) • 9.58 kB
/** @import { RemoteQuery, RemoteQueryFunction } from '@sveltejs/kit' */ /** @import { RemoteInfo, MaybePromise } from 'types' */ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_key, stringify_remote_arg } from '../../../shared.js'; import { prerendering } from '__sveltekit/environment'; import { create_validator, get_cache, get_response, run_remote_function } from './shared.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>} * @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 {RemoteInfo} */ const __ = { type: 'query', id: '', name: '' }; /** @type {RemoteQueryFunction<Input, Output> & { __: RemoteInfo }} */ 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 get_remote_function_result = () => run_remote_function(event, state, false, arg, validate, fn); /** @type {Promise<any> & Partial<RemoteQuery<any>>} */ const promise = get_response(__, arg, state, get_remote_function_result); promise.catch(() => {}); promise.set = (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value); promise.refresh = () => { const refresh_context = get_refresh_context(__, 'refresh', arg); const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key]; const value = is_immediate_refresh ? promise : get_remote_function_result(); return update_refresh_value(refresh_context, value, is_immediate_refresh); }; promise.withOverride = () => { throw new Error(`Cannot call '${__.name}.withOverride()' on the server`); }; return /** @type {RemoteQuery<Output>} */ (promise); }; 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>} * @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[]) => (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); /** @type {RemoteInfo & { type: 'query_batch' }} */ const __ = { type: 'query_batch', id: '', name: '', run: (args) => { const { event, state } = get_request_store(); return run_remote_function( event, state, false, args, (array) => Promise.all(array.map(validate)), fn ); } }; /** @type {{ args: any[], resolvers: Array<{resolve: (value: any) => void, reject: (error: any) => void}> }} */ let batching = { args: [], resolvers: [] }; /** @type {RemoteQueryFunction<Input, Output> & { __: RemoteInfo }} */ 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 { event, state } = get_request_store(); const get_remote_function_result = () => { // Collect all the calls to the same query in the same macrotask, // then execute them as one backend request. return new Promise((resolve, reject) => { // We don't need to deduplicate args here, because get_response already caches/reuses identical calls batching.args.push(arg); batching.resolvers.push({ resolve, reject }); if (batching.args.length > 1) return; setTimeout(async () => { const batched = batching; batching = { args: [], resolvers: [] }; try { const get_result = await run_remote_function( event, state, false, batched.args, (array) => Promise.all(array.map(validate)), fn ); for (let i = 0; i < batched.resolvers.length; i++) { try { batched.resolvers[i].resolve(get_result(batched.args[i], i)); } catch (error) { batched.resolvers[i].reject(error); } } } catch (error) { for (const resolver of batched.resolvers) { resolver.reject(error); } } }, 0); }); }; /** @type {Promise<any> & Partial<RemoteQuery<any>>} */ const promise = get_response(__, arg, state, get_remote_function_result); promise.catch(() => {}); promise.set = (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value); promise.refresh = () => { const refresh_context = get_refresh_context(__, 'refresh', arg); const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key]; const value = is_immediate_refresh ? promise : get_remote_function_result(); return update_refresh_value(refresh_context, value, is_immediate_refresh); }; promise.withOverride = () => { throw new Error(`Cannot call '${__.name}.withOverride()' on the server`); }; return /** @type {RemoteQuery<Output>} */ (promise); }; Object.defineProperty(wrapper, '__', { value: __ }); return wrapper; } // Add batch as a property to the query function Object.defineProperty(query, 'batch', { value: batch, enumerable: true }); /** * @param {RemoteInfo} __ * @param {'set' | 'refresh'} action * @param {any} [arg] * @returns {{ __: RemoteInfo; state: any; refreshes: Record<string, Promise<any>>; cache: Record<string, Promise<any>>; refreshes_key: string; cache_key: string }} */ function get_refresh_context(__, action, arg) { const { state } = get_request_store(); const { refreshes } = state; 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 cache_key = stringify_remote_arg(arg, state.transport); const refreshes_key = create_remote_key(__.id, cache_key); return { __, state, refreshes, refreshes_key, cache, cache_key }; } /** * @param {{ __: RemoteInfo; refreshes: Record<string, Promise<any>>; cache: Record<string, Promise<any>>; refreshes_key: string; cache_key: string }} context * @param {any} value * @param {boolean} [is_immediate_refresh=false] * @returns {Promise<void>} */ function update_refresh_value( { __, refreshes, refreshes_key, cache, cache_key }, value, is_immediate_refresh = false ) { const promise = Promise.resolve(value); if (!is_immediate_refresh) { cache[cache_key] = promise; } if (__.id) { refreshes[refreshes_key] = promise; } return promise.then(() => {}); }