Skip to content

Add RemoteCommand type #14020

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 54 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,60 @@ export type RemoteFormAction<Success, Failure> = ((data: FormData) => Promise<vo
};
};

/**
* The return value of a remote `command` function.
* Call it with the input arguments to execute the command.
*
* Note: Prefer remote `form` functions when possible, as they
* work without JavaScript enabled.
*
* ```svelte
* <script>
* import { createTodo } from './todos.remote.js';
*
* let text = $state('');
* </script>
*
* <input bind:value={text} />
* <button onclick={async () => {
* await createTodo({ text });
* }}>
* Create Todo
* </button>
* ```
* Use the `updates` method to specify which queries to update in response to the command.
* ```svelte
* <script>
* import { getTodos, createTodo } from './todos.remote.js';
*
* let text = $state('');
* </script>
*
* <input bind:value={text} />
* <button onclick={async () => {
* await createTodo({ text }).updates(
* getTodos.withOverride((todos) => [...todos, { text, done: false }])
* );
* }}>
* Create Todo
* </button>
*
* <ul>
* {#each await getTodos() as todo}
* <li>{todo.text}</li>
* {/each}
* </ul>
* ```
*/
export type RemoteCommand<Input, Output> = (arg: Input) => Promise<Awaited<Output>> & {
updates: (
...queries: Array<
| ReturnType<RemoteQuery<any, any>>
| ReturnType<ReturnType<RemoteQuery<any, any>>['withOverride']>
>
) => Promise<Awaited<Output>>;
};

/**
* The return value of a remote `query` or `prerender` function.
* Call it with the input arguments to retrieve the value.
Expand Down
18 changes: 8 additions & 10 deletions packages/kit/src/runtime/app/server/remote.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { RemoteFormAction, RemoteQuery, RequestEvent, ActionFailure as IActionFailure } from '@sveltejs/kit' */
/** @import { RemoteFormAction, RemoteQuery, RemoteCommand, RequestEvent, ActionFailure as IActionFailure } from '@sveltejs/kit' */
/** @import { RemotePrerenderEntryGenerator, RemoteInfo, ServerHooks, MaybePromise } from 'types' */
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */

Expand Down Expand Up @@ -304,7 +304,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
return result;
})();

promise.refresh = async () => {
promise.refresh = () => {
throw new Error(
`Cannot call '${wrapper.__.name}.refresh()'. Remote prerender functions are immutable and cannot be refreshed.`
);
Expand Down Expand Up @@ -464,7 +464,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
* @template Output
* @overload
* @param {() => Output} fn
* @returns {() => Promise<Awaited<Output>> & { updates: (...queries: Array<ReturnType<RemoteQuery<any, any>> | ReturnType<ReturnType<RemoteQuery<any, any>>['withOverride']>>) => Promise<Awaited<Output>> }}
* @returns {RemoteCommand<void, Output>}
*/
/**
* Creates a remote command. The given function is invoked directly on the server and via a fetch call on the client.
Expand Down Expand Up @@ -500,7 +500,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
* @overload
* @param {'unchecked'} validate
* @param {(arg: Input) => Output} fn
* @returns {(arg: Input) => Promise<Awaited<Output>> & { updates: (...queries: Array<ReturnType<RemoteQuery<any, any>> | ReturnType<ReturnType<RemoteQuery<any, any>>['withOverride']>>) => Promise<Awaited<Output>> }}
* @returns {RemoteCommand<Input, Output>}
*/
/**
* Creates a remote command. The given function is invoked directly on the server and via a fetch call on the client.
Expand Down Expand Up @@ -536,14 +536,14 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) {
* @overload
* @param {Schema} validate
* @param {(arg: StandardSchemaV1.InferOutput<Schema>) => Output} fn
* @returns {(arg: StandardSchemaV1.InferOutput<Schema>) => Promise<Awaited<Output>> & { updates: (...queries: Array<ReturnType<RemoteQuery<any, any>> | ReturnType<ReturnType<RemoteQuery<any, any>>['withOverride']>>) => Promise<Awaited<Output>> }}
* @returns {RemoteCommand<StandardSchemaV1.InferOutput<Schema>, Output>}
*/
/**
* @template Input
* @template Output
* @param {any} validate_or_fn
* @param {(arg?: Input) => Output} [maybe_fn]
* @returns {(arg?: Input) => Promise<Awaited<Output>> & { updates: (...queries: Array<ReturnType<RemoteQuery<any, any>> | ReturnType<ReturnType<RemoteQuery<any, any>>['withOverride']>>) => Promise<Awaited<Output>> }}
* @returns {RemoteCommand<Input, Output>}
*/
/*@__NO_SIDE_EFFECTS__*/
export function command(validate_or_fn, maybe_fn) {
Expand All @@ -554,9 +554,7 @@ export function command(validate_or_fn, maybe_fn) {
/** @type {(arg?: any) => MaybePromise<Input>} */
const validate = create_validator(validate_or_fn, maybe_fn);

/**
* @param {Input} [arg]
*/
/** @type {RemoteCommand<Input, Output> & { __: RemoteInfo }} */
const wrapper = (arg) => {
if (prerendering) {
throw new Error(
Expand All @@ -581,7 +579,7 @@ export function command(validate_or_fn, maybe_fn) {
promise.updates = () => {
throw new Error(`Cannot call '${wrapper.__.name}(...).updates(...)' on the server`);
};
return /** @type {Promise<Awaited<Output>> & { updates: (...arsg: any[]) => any}} */ (promise);
return /** @type {ReturnType<RemoteCommand<Input, Output>>} */ (promise);
};

Object.defineProperty(wrapper, '__', {
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/client/remote.svelte.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { RemoteFormAction, RemoteQuery } from '@sveltejs/kit' */
/** @import { RemoteFormAction, RemoteQuery, RemoteCommand } from '@sveltejs/kit' */
/** @import { RemoteFunctionResponse } from 'types' */

import { app_dir } from '__sveltekit/paths';
Expand Down Expand Up @@ -432,12 +432,12 @@ export function prerender(id) {
/**
* Client-version of the `command` function from `$app/server`.
* @param {string} id
* @returns {(arg: any) => Promise<any> & { updates: (...args: any[]) => any }}
* @returns {RemoteCommand<any, any>}
*/
export function command(id) {
// Careful: This function MUST be synchronous (can't use the async keyword) because the return type has to be a promise with an updates() method.
// If we make it async, the return type will be a promise that resolves to a promise with an updates() method, which is not what we want.
return (/** @type {any} */ arg) => {
return (arg) => {
/** @type {Array<Query<any> | ReturnType<Query<any>['withOverride']>>} */
let updates = [];

Expand Down
68 changes: 58 additions & 10 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,60 @@ declare module '@sveltejs/kit' {
};
};

/**
* The return value of a remote `command` function.
* Call it with the input arguments to execute the command.
*
* Note: Prefer remote `form` functions when possible, as they
* work without JavaScript enabled.
*
* ```svelte
* <script>
* import { createTodo } from './todos.remote.js';
*
* let text = $state('');
* </script>
*
* <input bind:value={text} />
* <button onclick={async () => {
* await createTodo({ text });
* }}>
* Create Todo
* </button>
* ```
* Use the `updates` method to specify which queries to update in response to the command.
* ```svelte
* <script>
* import { getTodos, createTodo } from './todos.remote.js';
*
* let text = $state('');
* </script>
*
* <input bind:value={text} />
* <button onclick={async () => {
* await createTodo({ text }).updates(
* getTodos.withOverride((todos) => [...todos, { text, done: false }])
* );
* }}>
* Create Todo
* </button>
*
* <ul>
* {#each await getTodos() as todo}
* <li>{todo.text}</li>
* {/each}
* </ul>
* ```
*/
export type RemoteCommand<Input, Output> = (arg: Input) => Promise<Awaited<Output>> & {
updates: (
...queries: Array<
| ReturnType<RemoteQuery<any, any>>
| ReturnType<ReturnType<RemoteQuery<any, any>>['withOverride']>
>
) => Promise<Awaited<Output>>;
};

/**
* The return value of a remote `query` or `prerender` function.
* Call it with the input arguments to retrieve the value.
Expand Down Expand Up @@ -2638,7 +2692,7 @@ declare module '$app/paths' {
}

declare module '$app/server' {
import type { RequestEvent, RemoteQuery, ActionFailure as IActionFailure, RemoteFormAction } from '@sveltejs/kit';
import type { RequestEvent, RemoteQuery, RemoteCommand, ActionFailure as IActionFailure, RemoteFormAction } from '@sveltejs/kit';
import type { StandardSchemaV1 } from '@standard-schema/spec';
/**
* Read the contents of an imported asset from the filesystem
Expand Down Expand Up @@ -2824,9 +2878,7 @@ declare module '$app/server' {
* ```
*
* */
export function command<Output>(fn: () => Output): () => Promise<Awaited<Output>> & {
updates: (...queries: Array<ReturnType<RemoteQuery<any, any>> | ReturnType<ReturnType<RemoteQuery<any, any>>["withOverride"]>>) => Promise<Awaited<Output>>;
};
export function command<Output>(fn: () => Output): RemoteCommand<void, Output>;
/**
* Creates a remote command. The given function is invoked directly on the server and via a fetch call on the client.
*
Expand Down Expand Up @@ -2857,9 +2909,7 @@ declare module '$app/server' {
* ```
*
* */
export function command<Input, Output>(validate: "unchecked", fn: (arg: Input) => Output): (arg: Input) => Promise<Awaited<Output>> & {
updates: (...queries: Array<ReturnType<RemoteQuery<any, any>> | ReturnType<ReturnType<RemoteQuery<any, any>>["withOverride"]>>) => Promise<Awaited<Output>>;
};
export function command<Input, Output>(validate: "unchecked", fn: (arg: Input) => Output): RemoteCommand<Input, Output>;
/**
* Creates a remote command. The given function is invoked directly on the server and via a fetch call on the client.
*
Expand Down Expand Up @@ -2890,9 +2940,7 @@ declare module '$app/server' {
* ```
*
* */
export function command<Schema extends StandardSchemaV1, Output>(validate: Schema, fn: (arg: StandardSchemaV1.InferOutput<Schema>) => Output): (arg: StandardSchemaV1.InferOutput<Schema>) => Promise<Awaited<Output>> & {
updates: (...queries: Array<ReturnType<RemoteQuery<any, any>> | ReturnType<ReturnType<RemoteQuery<any, any>>["withOverride"]>>) => Promise<Awaited<Output>>;
};
export function command<Schema extends StandardSchemaV1, Output>(validate: Schema, fn: (arg: StandardSchemaV1.InferOutput<Schema>) => Output): RemoteCommand<StandardSchemaV1.InferOutput<Schema>, Output>;
/**
* Creates a form action. The passed function will be called when the form is submitted.
* Returns an object that can be spread onto a form element to connect it to the function.
Expand Down
Loading