Describe the bug
When a query request is in flight and something newer lands first, the older response still replaces the value when it arrives. Two cases:
- A command's single-flight update (
get_value().refresh() inside the command) sets the new value, then a refresh() that started before the command finishes and puts the old value back.
- Two
refresh() calls on the same query whose responses arrive out of order. The second, newer response is shown, then the first response replaces it.
Query already has logic for this. #run drops a response once a newer one has resolved, and set() settles pending requests so they can't apply their results afterwards. But a direct query response has its value applied before that check can protect it. For a normal successful call, the server sends the query's value in _ and also under the query's own key in q ({ _: value, q: { [key]: { v: value } } }). remote_request applies every q entry with entry.resource.set(...), including the query's own entry. So the value is already applied before #run checks whether the response is still current, and the function passed to QueryProxy resolves with undefined. As a result, the latest response to arrive always wins, no matter how old its request is.
This seems to date from #15991 (first released in 2.65.0), which replaced returning the parsed value with the side-channel q data.
I have a fix with unit tests and will open a PR.
Reproduction
Repository: https://github.com/xyrolle/sveltekit-query-order-repro (the same code is below).
With remote functions enabled (sveltekit({ experimental: { remoteFunctions: true } }) in vite.config.js), case 1 happens in Chromium and WebKit:
// src/routes/race/data.remote.js
import { command, query } from '$app/server';
let value = 0;
let slow = false;
export const get_value = query(async () => {
const v = value;
if (slow) {
slow = false;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return v;
});
export const slow_down_next_read = command(() => {
slow = true;
});
export const increment = command(async () => {
value += 1;
get_value().refresh();
});
<!-- src/routes/race/+page.svelte -->
<script>
import { get_value, increment, slow_down_next_read } from './data.remote';
const v = get_value();
</script>
<p>{v.current}</p>
<button
onclick={async () => {
await slow_down_next_read();
v.refresh(); // reads the value before the increment, responds after 1s
await new Promise((resolve) => setTimeout(resolve, 100));
await increment(); // responds right away with the incremented value
}}>go</button
>
Click "go". The value goes from 0 to 1 when the command responds. About a second later the refresh responds, and the value goes back to 0.
Case 2 needs a query that returns something different on every call, for example a counter that increments on each request. Call v.refresh() twice, where the first request is slow:
// src/routes/race2/data.remote.js
import { command, query } from '$app/server';
let n = 0;
let slow = false;
export const get_count = query(async () => {
const c = ++n;
if (slow) {
slow = false;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return c;
});
export const slow_down_next_read = command(() => {
slow = true;
});
<!-- src/routes/race2/+page.svelte -->
<script>
import { get_count, slow_down_next_read } from './data.remote';
const v = get_count();
</script>
<p>{v.current}</p>
<button
onclick={async () => {
await slow_down_next_read();
v.refresh(); // first request, responds after 1s
await new Promise((resolve) => setTimeout(resolve, 100));
v.refresh(); // second request, responds right away
}}>go</button
>
The second, newer value is shown and then replaced by the first one. I could reproduce this in WebKit. In Chromium this setup doesn't show it, because Chromium held the second identical GET until the first one responded.
Expected behaviour
The value should stay at the newest result: 1 in case 1, and the second refresh's value in case 2. A response that has been superseded should be discarded, the same way Query#run already handles it.
Logs
No errors in the browser console or the server output.
System Info
System:
OS: macOS 27.2
CPU: (18) arm64 Apple M5 Pro
Binaries:
Node: 24.16.0
npm: 11.13.0
Browsers:
Chrome: 154.0.8037.93
Safari: 27.2
npmPackages:
@sveltejs/kit: 3.0.0 => 3.0.0
@sveltejs/vite-plugin-svelte: 7.3.1 => 7.3.1
svelte: 5.57.1 => 5.57.1
vite: 8.3.2 => 8.3.2
Also reproduced on current main (7b4bb3c), in Chromium 153 and WebKit 26.6.
Severity
serious, but I can work around it
Additional Information
In the client, a direct query that isn't redirected returns its own result through Query#run, and remote_request skips that key. Redirect responses keep the current behaviour. If the query updated itself on the server, its own q entry is used, so that still works. Other queries' entries in q (command/form single-flight updates) are still applied as before (set(), or fail() for errors). One behaviour change: if a query refreshes itself on the server and that refresh fails, refresh() now rejects (as for any other query error) instead of resolving with .error set. Normal query.batch results aren't affected because they already come back through the promise.
Describe the bug
When a
queryrequest is in flight and something newer lands first, the older response still replaces the value when it arrives. Two cases:get_value().refresh()inside the command) sets the new value, then arefresh()that started before the command finishes and puts the old value back.refresh()calls on the same query whose responses arrive out of order. The second, newer response is shown, then the first response replaces it.Queryalready has logic for this.#rundrops a response once a newer one has resolved, andset()settles pending requests so they can't apply their results afterwards. But a direct query response has its value applied before that check can protect it. For a normal successful call, the server sends the query's value in_and also under the query's own key inq({ _: value, q: { [key]: { v: value } } }).remote_requestapplies everyqentry withentry.resource.set(...), including the query's own entry. So the value is already applied before#runchecks whether the response is still current, and the function passed toQueryProxyresolves withundefined. As a result, the latest response to arrive always wins, no matter how old its request is.This seems to date from #15991 (first released in 2.65.0), which replaced returning the parsed value with the side-channel
qdata.I have a fix with unit tests and will open a PR.
Reproduction
Repository: https://github.com/xyrolle/sveltekit-query-order-repro (the same code is below).
With remote functions enabled (
sveltekit({ experimental: { remoteFunctions: true } })invite.config.js), case 1 happens in Chromium and WebKit:Click "go". The value goes from
0to1when the command responds. About a second later the refresh responds, and the value goes back to0.Case 2 needs a query that returns something different on every call, for example a counter that increments on each request. Call
v.refresh()twice, where the first request is slow:The second, newer value is shown and then replaced by the first one. I could reproduce this in WebKit. In Chromium this setup doesn't show it, because Chromium held the second identical GET until the first one responded.
Expected behaviour
The value should stay at the newest result:
1in case 1, and the second refresh's value in case 2. A response that has been superseded should be discarded, the same wayQuery#runalready handles it.Logs
No errors in the browser console or the server output.
System Info
Also reproduced on current
main(7b4bb3c), in Chromium 153 and WebKit 26.6.Severity
serious, but I can work around it
Additional Information
In the client, a direct query that isn't redirected returns its own result through
Query#run, andremote_requestskips that key. Redirect responses keep the current behaviour. If the query updated itself on the server, its ownqentry is used, so that still works. Other queries' entries inq(command/form single-flight updates) are still applied as before (set(), orfail()for errors). One behaviour change: if a query refreshes itself on the server and that refresh fails,refresh()now rejects (as for any other query error) instead of resolving with.errorset. Normalquery.batchresults aren't affected because they already come back through the promise.