From f8747b9774ef02422cc30ecbb08705067c6f120c Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 5 Sep 2025 17:02:19 +0200 Subject: [PATCH] detect async_deriveds inside batches that are destroyed in later batches --- .../src/internal/client/reactivity/batch.js | 42 +++++++++++++++++++ .../internal/client/reactivity/deriveds.js | 4 +- .../_config.js | 31 ++++++++++++++ .../main.svelte | 15 +++++-- .../main.svelte | 5 +++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e647f478be..75cd75da8f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -344,6 +344,48 @@ export class Batch { current_batch = this; } + /** + * Check if the branch this effect is in is obsolete in a later batch. + * That is, if the branch exists in this batch but will be destroyed in a later batch. + * @param {Effect} effect + */ + branch_obsolete(effect) { + /** @type {Effect[]} */ + let alive = []; + /** @type {Effect[]} */ + let skipped = []; + /** @type {Effect | null} */ + let current = effect; + + while (current !== null) { + if ((current.f & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0) { + alive.push(current); + if (this.skipped_effects.has(current)) { + skipped.push(...alive); + alive = []; + } + } + current = current.parent; + } + + let check = false; + for (const b of batches) { + if (b === this) { + check = true; + } else if (check) { + if ( + alive.some((branch) => b.skipped_effects.has(branch)) || + // TODO do we even have to check skipped here? how would an async_derived run for a branch that was already skipped? + (skipped.length > 0 && !skipped.some((branch) => b.skipped_effects.has(branch))) + ) { + return true; + } + } + } + + return false; + } + deactivate() { current_batch = null; previous_batch = null; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b020b84723..96947ef0fc 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -115,7 +115,7 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - async_effect(() => { + var effect = async_effect(() => { if (DEV) current_async_effect = active_effect; try { @@ -153,7 +153,7 @@ export function async_derived(fn, location) { batch.activate(); if (error) { - if (error !== STALE_REACTION) { + if (error !== STALE_REACTION && !batch.branch_obsolete(effect)) { signal.f |= ERROR_VALUE; // @ts-expect-error the error is the wrong type, but we don't care diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js index c5dae7fee2..18f59ce3c8 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js @@ -8,9 +8,11 @@ export default test({ increment.click(); await tick(); + reject.click(); reject.click(); await tick(); + resolve.click(); resolve.click(); await tick(); @@ -22,6 +24,35 @@ export default test({

false

1

+

false

+

1

+ ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + reject.click(); + reject.click(); + await tick(); + + resolve.click(); + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

false

+

3

+

false

+

3

` ); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte index 1ad6cb84de..f17f492292 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte @@ -3,7 +3,7 @@ let deferreds = []; - function push() { + function push(_just_so_that_template_is_reactive_) { const deferred = Promise.withResolvers(); deferreds.push(deferred); return deferred.promise; @@ -17,10 +17,19 @@ {#if count % 2 === 0}

true

- {#each await push() as count}

{count}

{/each} + {#each await push(count) as count}

{count}

{/each} {:else}

false

- {#each await push() as count}

{count}

{/each} + {#each await push(count) as count}

{count}

{/each} + {/if} + + {#if count % 2 === 0} +

true

+ {#each await push(count) as count}

{count}

{/each} + {/if} + {#if count % 2 === 1} +

false

+ {#each await push(count) as count}

{count}

{/each} {/if} {#snippet pending()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte index 2f461e96c8..2d8032228a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte @@ -3,6 +3,11 @@ export let route = $state({ current: 'home' }); + +