Skip to content

Commit 2deebde

Browse files
fix: handle asnyc updates within pending boundary (#17873)
When an async value is updated inside the boundary while the pending snippet is shown, we previously didn't notice that update and instead showed an outdated value once it resolved. This fixes that by rejecting all deferreds inside an async_derived while the pending snippet is shown. --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent 0206a20 commit 2deebde

4 files changed

Lines changed: 87 additions & 2 deletions

File tree

.changeset/nasty-friends-crash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: handle asnyc updates within pending boundary

packages/svelte/src/internal/client/reactivity/deriveds.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,18 @@ export function async_derived(fn, label, location) {
146146
if (should_suspend) {
147147
var decrement_pending = increment_pending();
148148

149-
deferreds.get(batch)?.reject(STALE_REACTION);
150-
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
149+
if (/** @type {Boundary} */ (parent.b).is_rendered()) {
150+
deferreds.get(batch)?.reject(STALE_REACTION);
151+
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
152+
} else {
153+
// While the boundary is still showing pending, a new run supersedes all older in-flight runs
154+
// for this async expression. Cancel eagerly so resolution cannot commit stale values.
155+
for (const d of deferreds.values()) {
156+
d.reject(STALE_REACTION);
157+
}
158+
deferreds.clear();
159+
}
160+
151161
deferreds.set(batch, d);
152162
}
153163

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
await tick();
7+
const [shift, increment] = target.querySelectorAll('button');
8+
9+
assert.htmlEqual(
10+
target.innerHTML,
11+
`
12+
<button>shift</button>
13+
<button>increment</button>
14+
loading
15+
`
16+
);
17+
18+
increment.click();
19+
await tick();
20+
assert.htmlEqual(
21+
target.innerHTML,
22+
`
23+
<button>shift</button>
24+
<button>increment</button>
25+
loading
26+
`
27+
);
28+
29+
shift.click();
30+
await tick();
31+
assert.htmlEqual(
32+
target.innerHTML,
33+
`
34+
<button>shift</button>
35+
<button>increment</button>
36+
loading
37+
`
38+
);
39+
40+
shift.click();
41+
await tick();
42+
assert.htmlEqual(
43+
target.innerHTML,
44+
`
45+
<button>shift</button>
46+
<button>increment</button>
47+
1
48+
`
49+
);
50+
}
51+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script>
2+
let queue = [];
3+
4+
function push(value) {
5+
const deferred = Promise.withResolvers();
6+
queue.push(() => deferred.resolve(value));
7+
return deferred.promise;
8+
}
9+
10+
let count = $state(0);
11+
</script>
12+
13+
<button onclick={() => queue.shift()()}>shift</button>
14+
<button onclick={() => count++}>increment</button>
15+
16+
<svelte:boundary>
17+
{await push(count)}
18+
{#snippet pending()}loading{/snippet}
19+
</svelte:boundary>

0 commit comments

Comments
 (0)