diff --git a/.changeset/clever-months-clap.md b/.changeset/clever-months-clap.md
new file mode 100644
index 000000000000..9566dcf54d66
--- /dev/null
+++ b/.changeset/clever-months-clap.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: only abort effect flushing if it causes an existing effect to be scheduled
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 123bc95d163a..b109c792e8b0 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -28,7 +28,7 @@ import * as e from '../errors.js';
import { flush_tasks } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
-import { old_values } from './sources.js';
+import { old_values, schedule_version } from './sources.js';
import { unlink_effect } from './effects.js';
import { unset_context } from './async.js';
@@ -292,12 +292,12 @@ export class Batch {
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
+ } else if ((flags & EFFECT) !== 0) {
+ this.#effects.push(effect);
+ } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
+ this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) {
- if ((flags & EFFECT) !== 0) {
- this.#effects.push(effect);
- } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
- this.#render_effects.push(effect);
- } else if ((flags & ASYNC) !== 0) {
+ if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect);
} else if (is_dirty(effect)) {
@@ -598,7 +598,7 @@ function flush_queued_effects(effects) {
var effect = effects[i++];
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
- var n = current_batch ? current_batch.current.size : 0;
+ var sv = schedule_version;
update_effect(effect);
@@ -619,13 +619,9 @@ function flush_queued_effects(effects) {
}
}
- // if state is written in a user effect, abort and re-schedule, lest we run
- // effects that should be removed as a result of the state change
- if (
- current_batch !== null &&
- current_batch.current.size > n &&
- (effect.f & USER_EFFECT) !== 0
- ) {
+ // if a state change in a user effect invalidates a _different_ effect,
+ // abort and reschedule in case that effect now needs to be destroyed
+ if (schedule_version > sv && (effect.f & USER_EFFECT) !== 0) {
break;
}
}
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index 7fb3135708d3..93f4cdaddbe0 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -299,6 +299,12 @@ export function increment(source) {
set(source, source.v + 1);
}
+/**
+ * We increment this value when a block effect is scheduled as a result of a state change,
+ * as its currently-scheduled child effects may need to be destroyed
+ */
+export let schedule_version = 0;
+
/**
* @param {Value} signal
* @param {number} status should be DIRTY or MAYBE_DIRTY
@@ -334,6 +340,7 @@ function mark_reactions(signal, status) {
if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
} else if (not_dirty) {
+ if ((flags & BLOCK_EFFECT) !== 0) schedule_version += 1;
schedule_effect(/** @type {Effect} */ (reaction));
}
}
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte
new file mode 100644
index 000000000000..9bf4db52d6b4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/Child.svelte
@@ -0,0 +1,13 @@
+
+
+{#if inited}
+ {@render children()}
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js
new file mode 100644
index 000000000000..046c1904322a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/_config.js
@@ -0,0 +1,12 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [button] = target.querySelectorAll('button');
+
+ assert.doesNotThrow(() => {
+ flushSync(() => button.click());
+ });
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte
new file mode 100644
index 000000000000..2b3a17179806
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-3/main.svelte
@@ -0,0 +1,15 @@
+
+
+
+
+{#if show}
+ {#each { length: 1234 } as i}
+ {i}
+ {/each}
+{/if}