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}