From 0aee7a5ee5fd7073cf000a2bc46a2aac0b53669f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 8 Dec 2025 17:46:58 -0800 Subject: [PATCH 1/3] WIP --- .../src/internal/client/reactivity/batch.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 24dee5141960..e9c4563e6a0d 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -128,15 +128,15 @@ export class Batch { /** * Deferred effects (which run after async work has completed) that are DIRTY - * @type {Effect[]} + * @type {Set} */ - #dirty_effects = []; + #dirty_effects = new Set(); /** * Deferred effects that are MAYBE_DIRTY - * @type {Effect[]} + * @type {Set} */ - #maybe_dirty_effects = []; + #maybe_dirty_effects = new Set(); /** * A set of branches that still exist, but will be destroyed when this batch @@ -279,8 +279,11 @@ export class Batch { */ #defer_effects(effects) { for (const e of effects) { - const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects; - target.push(e); + if ((e.f & DIRTY) !== 0) { + this.#dirty_effects.add(e); + } else if ((e.f & MAYBE_DIRTY) !== 0) { + this.#maybe_dirty_effects.add(e); + } // Since we're not executing these effects now, we need to clear any WAS_MARKED flags // so that other batches can correctly reach these effects during their own traversal @@ -484,6 +487,7 @@ export class Batch { revive() { for (const e of this.#dirty_effects) { + this.#maybe_dirty_effects.delete(e); set_signal_status(e, DIRTY); schedule_effect(e); } @@ -493,9 +497,6 @@ export class Batch { schedule_effect(e); } - this.#dirty_effects = []; - this.#maybe_dirty_effects = []; - this.flush(); } From 687f56a7efd5ee49cbc716a9edebc2ec4b2aeb71 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 8 Dec 2025 17:48:02 -0800 Subject: [PATCH 2/3] test --- .../Child.svelte | 3 +++ .../async-reschedule-during-flush/_config.js | 26 +++++++++++++++++++ .../async-reschedule-during-flush/main.svelte | 23 ++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte new file mode 100644 index 000000000000..2684005fcf95 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js new file mode 100644 index 000000000000..a837d02f9f76 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, resolve] = target.querySelectorAll('button'); + + a.click(); + await tick(); + + b.click(); + await tick(); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + 42 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte new file mode 100644 index 000000000000..48940017a8b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte @@ -0,0 +1,23 @@ + + + + + + +{#if a} + {await push(42)} + +{/if} From 425937a64f7e988f48c95ca661a29da7179cacdf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 8 Dec 2025 18:19:59 -0800 Subject: [PATCH 3/3] fix: correctly reschedule deferred effects when reviving a batch after async work --- .changeset/lucky-wasps-grab.md | 5 +++++ .../svelte/src/internal/client/reactivity/batch.js | 14 ++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 .changeset/lucky-wasps-grab.md diff --git a/.changeset/lucky-wasps-grab.md b/.changeset/lucky-wasps-grab.md new file mode 100644 index 000000000000..d2d2283ccd43 --- /dev/null +++ b/.changeset/lucky-wasps-grab.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly reschedule deferred effects when reviving a batch after async work diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e9c4563e6a0d..6f941c7ff231 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -44,7 +44,6 @@ import { eager_effect, unlink_effect } from './effects.js'; * effect: Effect | null; * effects: Effect[]; * render_effects: Effect[]; - * block_effects: Effect[]; * }} EffectTarget */ @@ -167,8 +166,7 @@ export class Batch { parent: null, effect: null, effects: [], - render_effects: [], - block_effects: [] + render_effects: [] }; for (const root of root_effects) { @@ -187,7 +185,6 @@ export class Batch { if (this.is_deferred()) { this.#defer_effects(target.effects); this.#defer_effects(target.render_effects); - this.#defer_effects(target.block_effects); } else { // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. @@ -228,8 +225,7 @@ export class Batch { parent: target, effect, effects: [], - render_effects: [], - block_effects: [] + render_effects: [] }; } @@ -241,7 +237,7 @@ export class Batch { } else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) { target.render_effects.push(effect); } else if (is_dirty(effect)) { - if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect); + if ((effect.f & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect); update_effect(effect); } @@ -263,7 +259,6 @@ export class Batch { // once the boundary is ready? this.#defer_effects(target.effects); this.#defer_effects(target.render_effects); - this.#defer_effects(target.block_effects); target = /** @type {EffectTarget} */ (target.parent); } @@ -393,8 +388,7 @@ export class Batch { parent: null, effect: null, effects: [], - render_effects: [], - block_effects: [] + render_effects: [] }; for (const batch of batches) {