diff --git a/.changeset/evil-stars-wave.md b/.changeset/evil-stars-wave.md new file mode 100644 index 0000000000..b199afe1dd --- /dev/null +++ b/.changeset/evil-stars-wave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: transfer effects when merging batches diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ae4f5a6dac..2803ffc84c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -289,19 +289,19 @@ export class Batch { } } - // we only reschedule previously-deferred effects if we expect - // to be able to run them after processing the batch - if (!this.#is_deferred()) { - for (const e of this.#dirty_effects) { - this.#maybe_dirty_effects.delete(e); - set_signal_status(e, DIRTY); - this.schedule(e); - } + // We always reschedule previously-deferred effects, not just when + // #is_deferred() is true, because traversing the tree could make + // an if block that contains the last blocking pending effect falsy, + // causing the block to no longer be deferred. + for (const e of this.#dirty_effects) { + this.#maybe_dirty_effects.delete(e); + set_signal_status(e, DIRTY); + this.schedule(e); + } - for (const e of this.#maybe_dirty_effects) { - set_signal_status(e, MAYBE_DIRTY); - this.schedule(e); - } + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); + this.schedule(e); } const roots = this.#roots; @@ -362,6 +362,10 @@ export class Batch { const earlier_batch = this.#find_earlier_batch(); if (earlier_batch) { + // If this batch collected deferred effects during traversal, they still need + // to run after being merged into the earlier batch. + this.#defer_effects(render_effects); + this.#defer_effects(effects); earlier_batch.#merge(this); return; } @@ -503,6 +507,9 @@ export class Batch { if (d) deferred.promise.then(d.resolve); } + // Mark is not guaranteed not touch these, so we transfer them + this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects); + /** * mark all effects that depend on `batch.current`, except the * async effects that we just resolved (TODO unless they depend diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/_config.js new file mode 100644 index 0000000000..c2be623de2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [x, x_y, pop] = target.querySelectorAll('button'); + + x.click(); + await tick(); + x_y.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ' 2 1 1' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/main.svelte new file mode 100644 index 0000000000..61efd4fca0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/main.svelte @@ -0,0 +1,25 @@ + + + + + + +{await push(x)} {await push(y)} + +{#if true} + {y} +{/if} +