Skip to content

Commit f8747b9

Browse files
committed
detect async_deriveds inside batches that are destroyed in later batches
1 parent 3d731c7 commit f8747b9

File tree

5 files changed

+92
-5
lines changed

5 files changed

+92
-5
lines changed

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,48 @@ export class Batch {
344344
current_batch = this;
345345
}
346346

347+
/**
348+
* Check if the branch this effect is in is obsolete in a later batch.
349+
* That is, if the branch exists in this batch but will be destroyed in a later batch.
350+
* @param {Effect} effect
351+
*/
352+
branch_obsolete(effect) {
353+
/** @type {Effect[]} */
354+
let alive = [];
355+
/** @type {Effect[]} */
356+
let skipped = [];
357+
/** @type {Effect | null} */
358+
let current = effect;
359+
360+
while (current !== null) {
361+
if ((current.f & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0) {
362+
alive.push(current);
363+
if (this.skipped_effects.has(current)) {
364+
skipped.push(...alive);
365+
alive = [];
366+
}
367+
}
368+
current = current.parent;
369+
}
370+
371+
let check = false;
372+
for (const b of batches) {
373+
if (b === this) {
374+
check = true;
375+
} else if (check) {
376+
if (
377+
alive.some((branch) => b.skipped_effects.has(branch)) ||
378+
// TODO do we even have to check skipped here? how would an async_derived run for a branch that was already skipped?
379+
(skipped.length > 0 && !skipped.some((branch) => b.skipped_effects.has(branch)))
380+
) {
381+
return true;
382+
}
383+
}
384+
}
385+
386+
return false;
387+
}
388+
347389
deactivate() {
348390
current_batch = null;
349391
previous_batch = null;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export function async_derived(fn, location) {
115115
// only suspend in async deriveds created on initialisation
116116
var should_suspend = !active_reaction;
117117

118-
async_effect(() => {
118+
var effect = async_effect(() => {
119119
if (DEV) current_async_effect = active_effect;
120120

121121
try {
@@ -153,7 +153,7 @@ export function async_derived(fn, location) {
153153
batch.activate();
154154

155155
if (error) {
156-
if (error !== STALE_REACTION) {
156+
if (error !== STALE_REACTION && !batch.branch_obsolete(effect)) {
157157
signal.f |= ERROR_VALUE;
158158

159159
// @ts-expect-error the error is the wrong type, but we don't care

packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ export default test({
88
increment.click();
99
await tick();
1010

11+
reject.click();
1112
reject.click();
1213
await tick();
1314

15+
resolve.click();
1416
resolve.click();
1517
await tick();
1618

@@ -22,6 +24,35 @@ export default test({
2224
<button>reject</button>
2325
<p>false</p>
2426
<p>1</p>
27+
<p>false</p>
28+
<p>1</p>
29+
`
30+
);
31+
32+
increment.click();
33+
await tick();
34+
35+
increment.click();
36+
await tick();
37+
38+
reject.click();
39+
reject.click();
40+
await tick();
41+
42+
resolve.click();
43+
resolve.click();
44+
await tick();
45+
46+
assert.htmlEqual(
47+
target.innerHTML,
48+
`
49+
<button>increment</button>
50+
<button>resolve</button>
51+
<button>reject</button>
52+
<p>false</p>
53+
<p>3</p>
54+
<p>false</p>
55+
<p>3</p>
2556
`
2657
);
2758
}

packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
let deferreds = [];
55
6-
function push() {
6+
function push(_just_so_that_template_is_reactive_) {
77
const deferred = Promise.withResolvers();
88
deferreds.push(deferred);
99
return deferred.promise;
@@ -17,10 +17,19 @@
1717
<svelte:boundary>
1818
{#if count % 2 === 0}
1919
<p>true</p>
20-
{#each await push() as count}<p>{count}</p>{/each}
20+
{#each await push(count) as count}<p>{count}</p>{/each}
2121
{:else}
2222
<p>false</p>
23-
{#each await push() as count}<p>{count}</p>{/each}
23+
{#each await push(count) as count}<p>{count}</p>{/each}
24+
{/if}
25+
26+
{#if count % 2 === 0}
27+
<p>true</p>
28+
{#each await push(count) as count}<p>{count}</p>{/each}
29+
{/if}
30+
{#if count % 2 === 1}
31+
<p>false</p>
32+
{#each await push(count) as count}<p>{count}</p>{/each}
2433
{/if}
2534

2635
{#snippet pending()}

packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
export let route = $state({ current: 'home' });
44
</script>
55

6+
<script>
7+
// reset from earlier tests
8+
route.current = 'home'
9+
</script>
10+
611
<button onclick={() => route.reject()}>reject</button>
712

813
<svelte:boundary>

0 commit comments

Comments
 (0)