Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/angry-deer-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

breaking: remove client-driven refreshes
5 changes: 5 additions & 0 deletions .changeset/heavy-pots-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

breaking: rename `command.updates` to `command.with` and `query.withOverride` to `query.override`
5 changes: 5 additions & 0 deletions .changeset/quiet-icons-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

chore: tighten up override implementation
55 changes: 9 additions & 46 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -764,38 +764,22 @@ We can customize what happens when the form is submitted with the `enhance` meth

The callback receives the `form` element, the `data` it contains, and a `submit` function.

To enable client-driven [single-flight mutations](#form-Single-flight-mutations), use `submit().updates(...)`. For example, if the `getPosts()` query was used on this page, we could refresh it like so:
Use `submit().with(...)` for optimistic overrides while the submission is ongoing:

```ts
import type { RemoteQuery, RemoteQueryOverride } from '@sveltejs/kit';
import type { RemoteQuery } from '@sveltejs/kit';
interface Post {}
declare function submit(): Promise<any> & {
updates(...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Promise<any>;
}

declare function getPosts(): RemoteQuery<Post[]>;
// ---cut---
await submit().updates(getPosts());
```

We can also _override_ the current data while the submission is ongoing:

```ts
import type { RemoteQuery, RemoteQueryOverride } from '@sveltejs/kit';
interface Post {}
declare function submit(): Promise<any> & {
updates(...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Promise<any>;
with(...callbacks: Array<() => void>): Promise<any>;
}

declare function getPosts(): RemoteQuery<Post[]>;
declare const newPost: Post;
// ---cut---
await submit().updates(
getPosts().withOverride((posts) => [newPost, ...posts])
);
await submit().with(getPosts().override((posts) => [newPost, ...posts]));
```

The override will be applied immediately, and released when the submission completes (or fails).
The override will be applied immediately, and released when the submission completes (or fails). Query refreshes for single-flight mutations must be declared in the server-side `form` handler with `query(...).refresh()` or `query(...).set(...)`.

### Multiple instances of a form

Expand Down Expand Up @@ -938,9 +922,7 @@ Now simply call `addLike`, from (for example) an event handler:

### Updating queries

To update `getLikes(item.id)`, or any other query, we need to tell SvelteKit _which_ queries need to be refreshed (unlike `form`, which by default invalidates everything, to approximate the behaviour of a native form submission).

We either do that inside the command itself...
To update `getLikes(item.id)`, or any other query, declare which queries should be refreshed inside the command itself:

```js
/// file: likes.remote.js
Expand Down Expand Up @@ -970,26 +952,7 @@ export const addLike = command(v.string(), async (id) => {
});
```

...or when we call it:

```ts
import { RemoteCommand, RemoteQueryFunction } from '@sveltejs/kit';

interface Item { id: string }

declare const addLike: RemoteCommand<string, void>;
declare const getLikes: RemoteQueryFunction<string, number>;
declare function showToast(message: string): void;
declare const item: Item;
// ---cut---
try {
await addLike(item.id).+++updates(getLikes(item.id))+++;
} catch (error) {
showToast('Something went wrong!');
}
```

As before, we can use `withOverride` for optimistic updates:
For optimistic UI updates while the command is pending, you can use `override` at call time:

```ts
import { RemoteCommand, RemoteQueryFunction } from '@sveltejs/kit';
Expand All @@ -1002,8 +965,8 @@ declare function showToast(message: string): void;
declare const item: Item;
// ---cut---
try {
await addLike(item.id).updates(
getLikes(item.id).+++withOverride((n) => n + 1)+++
await addLike(item.id).with(
getLikes(item.id).override((n) => n + 1)
);
} catch (error) {
showToast('Something went wrong!');
Expand Down
4 changes: 3 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export default [
...svelte_config,
{
rules: {
'no-undef': 'off'
'no-undef': 'off',
// we have some non-reactive state in our runtime modules, and we don't want to be nagged about it
'svelte/prefer-svelte-reactivity': 'off'
}
},
{
Expand Down
17 changes: 6 additions & 11 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2058,7 +2058,7 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
form: HTMLFormElement;
data: Input;
submit: () => Promise<void> & {
updates: (...queries: Array<RemoteQuery<any> | RemoteQueryOverride>) => Promise<void>;
with: (...callbacks: Array<() => void>) => Promise<void>;
};
}) => void | Promise<void>
): {
Expand Down Expand Up @@ -2103,7 +2103,7 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
*/
export type RemoteCommand<Input, Output> = {
(arg: undefined extends Input ? Input | void : Input): Promise<Output> & {
updates(...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Promise<Output>;
with(...callbacks: Array<() => void>): Promise<Output>;
};
/** The number of pending command executions */
get pending(): number;
Expand Down Expand Up @@ -2149,7 +2149,7 @@ export type RemoteQuery<T> = RemoteResource<T> & {
*/
refresh(): Promise<void>;
/**
* Temporarily override the value of a query. This is used with the `updates` method of a [command](https://svelte.dev/docs/kit/remote-functions#command-Updating-queries) or [enhanced form submission](https://svelte.dev/docs/kit/remote-functions#form-enhance) to provide optimistic updates.
* Temporarily override the value of a query. This is used with the `with` method of a [command](https://svelte.dev/docs/kit/remote-functions#command-Updating-queries) or [enhanced form submission](https://svelte.dev/docs/kit/remote-functions#form-enhance) to provide optimistic updates.
*
* ```svelte
* <script>
Expand All @@ -2158,23 +2158,18 @@ export type RemoteQuery<T> = RemoteResource<T> & {
* </script>
*
* <form {...addTodo.enhance(async ({ data, submit }) => {
* await submit().updates(
* todos.withOverride((todos) => [...todos, { text: data.get('text') }])
* await submit().with(
* todos.override((todos) => [...todos, { text: data.get('text') }])
* );
* })}>
* <input type="text" name="text" />
* <button type="submit">Add Todo</button>
* </form>
* ```
*/
withOverride(update: (current: T) => T): RemoteQueryOverride;
override(update: (current: T) => T): () => void;
};

export interface RemoteQueryOverride {
_key: string;
release(): void;
}

/**
* The return value of a remote `prerender` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/app/server/remote/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export function command(validate_or_fn, maybe_fn) {
);

// @ts-expect-error
promise.updates = () => {
throw new Error(`Cannot call '${__.name}(...).updates(...)' on the server`);
promise.with = () => {
throw new Error(`Cannot call '${__.name}(...).with(...)' on the server`);
};

return /** @type {ReturnType<RemoteCommand<Input, Output>>} */ (promise);
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/app/server/remote/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ function create_query_resource(__, arg, state, fn) {
then(onfulfilled, onrejected) {
return get_promise().then(onfulfilled, onrejected);
},
withOverride() {
throw new Error(`Cannot call '${__.name}.withOverride()' on the server`);
override() {
throw new Error(`Cannot call '${__.name}.override()' on the server`);
},
get [Symbol.toStringTag]() {
return 'QueryResource';
Expand Down
40 changes: 21 additions & 19 deletions packages/kit/src/runtime/client/remote-functions/command.svelte.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/** @import { RemoteCommand, RemoteQueryOverride } from '@sveltejs/kit' */
/** @import { RemoteCommand } from '@sveltejs/kit' */
/** @import { RemoteFunctionResponse } from 'types' */
/** @import { Query } from './query.svelte.js' */
import { app_dir, base } from '$app/paths/internal/client';
import * as devalue from 'devalue';
import { HttpError } from '@sveltejs/kit/internal';
import { app } from '../client.js';
import { stringify_remote_arg } from '../../shared.js';
import { get_remote_request_headers, refresh_queries, release_overrides } from './shared.svelte.js';
import { get_remote_request_headers, refresh_queries, release_callbacks } from './shared.svelte.js';

/**
* Client-version of the `command` function from `$app/server`.
Expand All @@ -17,12 +16,12 @@ export function command(id) {
/** @type {number} */
let pending_count = $state(0);

// 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.
// Careful: This function MUST be synchronous (can't use the async keyword) because the return type has to be a promise with a with() method.
// If we make it async, the return type will be a promise that resolves to a promise with a with() method, which is not what we want.
/** @type {RemoteCommand<any, any>} */
const command_function = (arg) => {
/** @type {Array<Query<any> | RemoteQueryOverride>} */
let updates = [];
/** @type {Array<() => void>} */
const callbacks = [];

// Increment pending count when command starts
pending_count++;
Expand All @@ -34,54 +33,57 @@ export function command(id) {
...get_remote_request_headers()
};

/** @type {Promise<any> & { updates: (...args: any[]) => any }} */
/** @type {Promise<any> & { with: (...args: Array<() => void>) => Promise<any> }} */
const promise = (async () => {
try {
// Wait a tick to give room for the `updates` method to be called
// Wait a tick to give room for the `with` method to be called
await Promise.resolve();

const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
method: 'POST',
body: JSON.stringify({
payload: stringify_remote_arg(arg, app.hooks.transport),
refreshes: updates.map((u) => u._key)
payload: stringify_remote_arg(arg, app.hooks.transport)
}),
headers
});

if (!response.ok) {
release_overrides(updates);
// We only end up here in case of a network error or if the server has an internal error
// (which shouldn't happen because we handle errors on the server and always send a 200 response)
throw new Error('Failed to execute remote function');
}

const result = /** @type {RemoteFunctionResponse} */ (await response.json());
if (result.type === 'redirect') {
release_overrides(updates);
throw new Error(
'Redirects are not allowed in commands. Return a result instead and use goto on the client'
);
} else if (result.type === 'error') {
release_overrides(updates);
throw new HttpError(result.status ?? 500, result.error);
} else {
if (result.refreshes) {
refresh_queries(result.refreshes, updates);
refresh_queries(result.refreshes);
}

return devalue.parse(result.result, app.decoders);
}
} finally {
release_callbacks(callbacks);
// Decrement pending count when command completes
pending_count--;
}
})();

promise.updates = (/** @type {any} */ ...args) => {
updates = args;
// @ts-expect-error Don't allow updates to be called multiple times
delete promise.updates;
let with_called = false;
promise.with = (...args) => {
if (with_called) {
throw new Error('The with() method can only be called once per command invocation');
}
with_called = true;

callbacks.length = 0;
callbacks.push(...args);

return promise;
};

Expand Down
Loading
Loading