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}
+