Skip to content

Commit 4d3abcd

Browse files
phi-bredummdidumm
andauthored
fix: correct query .set and .refresh behavior in commands (#14877)
Closes #14832 Closes #14602 --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Simon Holthausen <[email protected]>
1 parent 13d9ed3 commit 4d3abcd

File tree

11 files changed

+212
-67
lines changed

11 files changed

+212
-67
lines changed

.changeset/swift-fans-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: correct query `.set` and `.refresh` behavior in commands

packages/kit/src/runtime/app/server/remote/query.js

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { RemoteInfo, MaybePromise } from 'types' */
33
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
44
import { get_request_store } from '@sveltejs/kit/internal/server';
5-
import { create_remote_cache_key, stringify_remote_arg } from '../../../shared.js';
5+
import { create_remote_key, stringify_remote_arg } from '../../../shared.js';
66
import { prerendering } from '__sveltekit/environment';
77
import { create_validator, get_cache, get_response, run_remote_function } from './shared.js';
88

@@ -72,46 +72,21 @@ export function query(validate_or_fn, maybe_fn) {
7272

7373
const { event, state } = get_request_store();
7474

75+
const get_remote_function_result = () =>
76+
run_remote_function(event, state, false, arg, validate, fn);
77+
7578
/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
76-
const promise = get_response(__, arg, state, () =>
77-
run_remote_function(event, state, false, arg, validate, fn)
78-
);
79+
const promise = get_response(__, arg, state, get_remote_function_result);
7980

8081
promise.catch(() => {});
8182

82-
/** @param {Output} value */
83-
promise.set = (value) => {
84-
const { state } = get_request_store();
85-
const refreshes = state.refreshes;
86-
87-
if (!refreshes) {
88-
throw new Error(
89-
`Cannot call set on query '${__.name}' because it is not executed in the context of a command/form remote function`
90-
);
91-
}
92-
93-
if (__.id) {
94-
const cache = get_cache(__, state);
95-
const key = stringify_remote_arg(arg, state.transport);
96-
refreshes[create_remote_cache_key(__.id, key)] = cache[key] = Promise.resolve(value);
97-
}
98-
};
83+
promise.set = (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value);
9984

10085
promise.refresh = () => {
101-
const { state } = get_request_store();
102-
const refreshes = state.refreshes;
103-
104-
if (!refreshes) {
105-
throw new Error(
106-
`Cannot call refresh on query '${__.name}' because it is not executed in the context of a command/form remote function`
107-
);
108-
}
109-
110-
const cache_key = create_remote_cache_key(__.id, stringify_remote_arg(arg, state.transport));
111-
refreshes[cache_key] = promise;
112-
113-
// TODO we could probably just return promise here, but would need to update the types
114-
return promise.then(() => {});
86+
const refresh_context = get_refresh_context(__, 'refresh', arg);
87+
const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key];
88+
const value = is_immediate_refresh ? promise : get_remote_function_result();
89+
return update_refresh_value(refresh_context, value, is_immediate_refresh);
11590
};
11691

11792
promise.withOverride = () => {
@@ -200,8 +175,7 @@ function batch(validate_or_fn, maybe_fn) {
200175

201176
const { event, state } = get_request_store();
202177

203-
/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
204-
const promise = get_response(__, arg, state, () => {
178+
const get_remote_function_result = () => {
205179
// Collect all the calls to the same query in the same macrotask,
206180
// then execute them as one backend request.
207181
return new Promise((resolve, reject) => {
@@ -239,22 +213,20 @@ function batch(validate_or_fn, maybe_fn) {
239213
}
240214
}, 0);
241215
});
242-
});
216+
};
243217

244-
promise.catch(() => {});
218+
/** @type {Promise<any> & Partial<RemoteQuery<any>>} */
219+
const promise = get_response(__, arg, state, get_remote_function_result);
245220

246-
promise.refresh = async () => {
247-
const { state } = get_request_store();
248-
const refreshes = state.refreshes;
221+
promise.catch(() => {});
249222

250-
if (!refreshes) {
251-
throw new Error(
252-
`Cannot call refresh on query.batch '${__.name}' because it is not executed in the context of a command/form remote function`
253-
);
254-
}
223+
promise.set = (value) => update_refresh_value(get_refresh_context(__, 'set', arg), value);
255224

256-
const cache_key = create_remote_cache_key(__.id, stringify_remote_arg(arg, state.transport));
257-
refreshes[cache_key] = await /** @type {Promise<any>} */ (promise);
225+
promise.refresh = () => {
226+
const refresh_context = get_refresh_context(__, 'refresh', arg);
227+
const is_immediate_refresh = !refresh_context.cache[refresh_context.cache_key];
228+
const value = is_immediate_refresh ? promise : get_remote_function_result();
229+
return update_refresh_value(refresh_context, value, is_immediate_refresh);
258230
};
259231

260232
promise.withOverride = () => {
@@ -271,3 +243,51 @@ function batch(validate_or_fn, maybe_fn) {
271243

272244
// Add batch as a property to the query function
273245
Object.defineProperty(query, 'batch', { value: batch, enumerable: true });
246+
247+
/**
248+
* @param {RemoteInfo} __
249+
* @param {'set' | 'refresh'} action
250+
* @param {any} [arg]
251+
* @returns {{ __: RemoteInfo; state: any; refreshes: Record<string, Promise<any>>; cache: Record<string, Promise<any>>; refreshes_key: string; cache_key: string }}
252+
*/
253+
function get_refresh_context(__, action, arg) {
254+
const { state } = get_request_store();
255+
const { refreshes } = state;
256+
257+
if (!refreshes) {
258+
const name = __.type === 'query_batch' ? `query.batch '${__.name}'` : `query '${__.name}'`;
259+
throw new Error(
260+
`Cannot call ${action} on ${name} because it is not executed in the context of a command/form remote function`
261+
);
262+
}
263+
264+
const cache = get_cache(__, state);
265+
const cache_key = stringify_remote_arg(arg, state.transport);
266+
const refreshes_key = create_remote_key(__.id, cache_key);
267+
268+
return { __, state, refreshes, refreshes_key, cache, cache_key };
269+
}
270+
271+
/**
272+
* @param {{ __: RemoteInfo; refreshes: Record<string, Promise<any>>; cache: Record<string, Promise<any>>; refreshes_key: string; cache_key: string }} context
273+
* @param {any} value
274+
* @param {boolean} [is_immediate_refresh=false]
275+
* @returns {Promise<void>}
276+
*/
277+
function update_refresh_value(
278+
{ __, refreshes, refreshes_key, cache, cache_key },
279+
value,
280+
is_immediate_refresh = false
281+
) {
282+
const promise = Promise.resolve(value);
283+
284+
if (!is_immediate_refresh) {
285+
cache[cache_key] = promise;
286+
}
287+
288+
if (__.id) {
289+
refreshes[refreshes_key] = promise;
290+
}
291+
292+
return promise.then(() => {});
293+
}

packages/kit/src/runtime/app/server/remote/shared.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function create_validator(validate_or_fn, maybe_fn) {
6868
* @returns {Promise<T>}
6969
*/
7070
export async function get_response(info, arg, state, get_result) {
71-
// wait a beat, in case `myQuery().set(...)` is immediately called
71+
// wait a beat, in case `myQuery().set(...)` or `myQuery().refresh()` is immediately called
7272
// eslint-disable-next-line @typescript-eslint/await-thenable
7373
await 0;
7474

packages/kit/src/runtime/client/remote-functions/shared.svelte.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as devalue from 'devalue';
55
import { app, goto, query_map, remote_responses } from '../client.js';
66
import { HttpError, Redirect } from '@sveltejs/kit/internal';
77
import { tick } from 'svelte';
8-
import { create_remote_cache_key, stringify_remote_arg } from '../../shared.js';
8+
import { create_remote_key, stringify_remote_arg } from '../../shared.js';
99

1010
/**
1111
*
@@ -48,7 +48,7 @@ export async function remote_request(url) {
4848
export function create_remote_function(id, create) {
4949
return (/** @type {any} */ arg) => {
5050
const payload = stringify_remote_arg(arg, app.hooks.transport);
51-
const cache_key = create_remote_cache_key(id, payload);
51+
const cache_key = create_remote_key(id, payload);
5252
let entry = query_map.get(cache_key);
5353

5454
let tracking = true;

packages/kit/src/runtime/server/page/render.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { add_resolution_suffix } from '../../pathname.js';
1616
import { try_get_request_store, with_request_store } from '@sveltejs/kit/internal/server';
1717
import { text_encoder } from '../../utils.js';
1818
import { get_global_name } from '../utils.js';
19-
import { create_remote_cache_key } from '../../shared.js';
19+
import { create_remote_key } from '../../shared.js';
2020

2121
// TODO rename this function/module
2222

@@ -493,7 +493,7 @@ export async function render_response({
493493
if (!info.id) continue;
494494

495495
for (const key in cache) {
496-
remote[create_remote_cache_key(info.id, key)] = await cache[key];
496+
remote[create_remote_key(info.id, key)] = await cache[key];
497497
}
498498
}
499499

packages/kit/src/runtime/shared.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,6 @@ export function parse_remote_arg(string, transport) {
8888
* @param {string} id
8989
* @param {string} payload
9090
*/
91-
export function create_remote_cache_key(id, payload) {
91+
export function create_remote_key(id, payload) {
9292
return id + '/' + payload;
9393
}

packages/kit/test/apps/basics/src/routes/remote/+page.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
get_count,
77
set_count,
88
set_count_server_refresh,
9+
set_count_server_refresh_after_read,
910
set_count_server_set,
1011
resolve_deferreds
1112
} from './query-command.remote.js';
@@ -67,6 +68,14 @@
6768
>
6869
command (query server refresh)
6970
</button>
71+
<button
72+
onclick={async () => {
73+
command_result = await set_count_server_refresh_after_read(6);
74+
}}
75+
id="multiply-server-refresh-after-read-btn"
76+
>
77+
command (query server refresh after read)
78+
</button>
7079
<button
7180
onclick={async () => {
7281
// slow, else test will not be able to see the override

packages/kit/test/apps/basics/src/routes/remote/batch/+page.svelte

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
<script>
2-
import { get_todo } from './batch.remote.js';
2+
import {
3+
get_todo,
4+
set_todo_title_server_refresh,
5+
reset_todos,
6+
set_todo_title
7+
} from './batch.remote.js';
38
49
const todoIds = ['1', '2', '1', 'error'];
10+
// Need to write this outside at the top level to ensure tests succeed in non-async-mode
11+
// Else updates are not coming through properly because of state-created-inside-effects-not-updating logic in non-async mode
12+
const todos = todoIds.map((id) => ({ id, promise: get_todo(id) }));
513
</script>
614

715
<h1>Query Batch Test</h1>
816

917
<ul>
10-
{#each todoIds as id, idx}
18+
{#each todos as { id, promise }, idx (idx)}
1119
<li>
12-
{#await get_todo(id)}
20+
{#await promise}
1321
<span id="batch-result-{idx + 1}">Loading todo {id}...</span>
1422
{:then todo}
1523
<span id="batch-result-{idx + 1}">{todo.title}</span>
@@ -21,3 +29,27 @@
2129
</ul>
2230

2331
<button onclick={() => todoIds.forEach((id) => get_todo(id).refresh())}>refresh</button>
32+
<button
33+
onclick={async () => {
34+
await set_todo_title({ id: '1', title: 'Buy cat food' });
35+
}}
36+
id="batch-set-btn"
37+
>
38+
set first todo
39+
</button>
40+
<button
41+
onclick={async () => {
42+
await set_todo_title_server_refresh({ id: '2', title: 'Walk the dog (refreshed)' });
43+
}}
44+
id="batch-refresh-btn"
45+
>
46+
refresh second todo via command
47+
</button>
48+
<button
49+
onclick={async () => {
50+
await reset_todos();
51+
}}
52+
id="batch-reset-btn"
53+
>
54+
reset todos
55+
</button>
Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
1-
import { query } from '$app/server';
1+
import { command, query } from '$app/server';
22
import { error } from '@sveltejs/kit';
33

4+
/** @type {Array<[string, { id: string; title: string }]>} **/
5+
const INITIAL_TODOS = [
6+
['1', { id: '1', title: 'Buy groceries' }],
7+
['2', { id: '2', title: 'Walk the dog' }]
8+
];
9+
10+
let todos = new Map(INITIAL_TODOS);
11+
412
export const get_todo = query.batch('unchecked', (ids) => {
5-
if (JSON.stringify(ids) !== JSON.stringify(['1', '2', 'error'])) {
6-
throw new Error(`Expected 3 IDs (deduplicated), got ${JSON.stringify(ids)}`);
13+
if (new Set(ids).size !== ids.length) {
14+
throw new Error(`batch queries must be deduplicated, but got ${JSON.stringify(ids)}`);
715
}
816

9-
return (id) =>
10-
id === '1'
11-
? { id: '1', title: 'Buy groceries' }
12-
: id === '2'
13-
? { id: '2', title: 'Walk the dog' }
14-
: error(404, { message: 'Not found' });
17+
return (id) => {
18+
if (id === 'error') return error(404, { message: 'Not found' });
19+
20+
return todos.get(id);
21+
};
22+
});
23+
24+
export const set_todo_title = command('unchecked', ({ id, title }) => {
25+
const todo = { id, title };
26+
todos.set(id, todo);
27+
get_todo(id).set(todo);
28+
return todo;
29+
});
30+
31+
export const set_todo_title_server_refresh = command('unchecked', ({ id, title }) => {
32+
const todo = { id, title };
33+
todos.set(id, todo);
34+
get_todo(id).refresh();
35+
return todo;
36+
});
37+
38+
export const reset_todos = command(() => {
39+
todos = new Map(INITIAL_TODOS);
40+
for (const [id, todo] of todos) {
41+
get_todo(id).set({ ...todo });
42+
}
1543
});

packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ export const set_count_server_refresh = command('unchecked', (c) => {
3636
return c;
3737
});
3838

39+
export const set_count_server_refresh_after_read = command('unchecked', async (c) => {
40+
await get_count();
41+
count = c;
42+
await get_count().refresh();
43+
return c;
44+
});
45+
3946
export const set_count_server_set = command('unchecked', async (c) => {
4047
get_count_called = false;
4148
count = c;

0 commit comments

Comments
 (0)