From dccc31d6ec7426df652b526a92deda1a1a4c8645 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Jul 2025 20:41:30 -0400 Subject: [PATCH 1/3] fix: abort and reschedule effect processing after state change in user effect --- .changeset/cuddly-walls-tan.md | 5 ++++ .../svelte/src/internal/client/constants.js | 1 + .../svelte/src/internal/client/context.js | 4 ++-- .../src/internal/client/reactivity/effects.js | 15 ++++++++---- .../svelte/src/internal/client/runtime.js | 15 +++++++++++- .../samples/effect-order-6/A.svelte | 11 +++++++++ .../samples/effect-order-6/B.svelte | 9 ++++++++ .../samples/effect-order-6/Child.svelte | 20 ++++++++++++++++ .../samples/effect-order-6/_config.js | 13 +++++++++++ .../samples/effect-order-6/main.svelte | 23 +++++++++++++++++++ 10 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 .changeset/cuddly-walls-tan.md create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte diff --git a/.changeset/cuddly-walls-tan.md b/.changeset/cuddly-walls-tan.md new file mode 100644 index 000000000000..feececc052d9 --- /dev/null +++ b/.changeset/cuddly-walls-tan.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: abort and reschedule effect processing after state change in user effect diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index cd5e0d224444..b82640b92736 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -19,6 +19,7 @@ export const INSPECT_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 19; export const EFFECT_HAS_DERIVED = 1 << 20; export const EFFECT_IS_UPDATING = 1 << 21; +export const USER_EFFECT = 1 << 22; export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 7c7213b7a2de..04e2fdda0492 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -9,7 +9,7 @@ import { set_active_effect, set_active_reaction } from './runtime.js'; -import { effect, teardown } from './reactivity/effects.js'; +import { create_user_effect, teardown } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; /** @type {ComponentContext | null} */ @@ -153,7 +153,7 @@ export function pop(component) { var component_effect = component_effects[i]; set_active_effect(component_effect.effect); set_active_reaction(component_effect.reaction); - effect(component_effect.fn); + create_user_effect(component_effect.fn); } } finally { set_active_effect(previous_effect); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index a2806bde81fd..1ccb06fab744 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -33,7 +33,8 @@ import { MAYBE_DIRTY, EFFECT_HAS_DERIVED, BOUNDARY_EFFECT, - STALE_REACTION + STALE_REACTION, + USER_EFFECT } from '#client/constants'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -199,11 +200,17 @@ export function user_effect(fn) { reaction: active_reaction }); } else { - var signal = effect(fn); - return signal; + return create_user_effect(fn); } } +/** + * @param {() => void | (() => void)} fn + */ +export function create_user_effect(fn) { + return create_effect(EFFECT | USER_EFFECT, fn, false); +} + /** * Internal representation of `$effect.pre(...)` * @param {() => void | (() => void)} fn @@ -216,7 +223,7 @@ export function user_pre_effect(fn) { value: '$effect.pre' }); } - return render_effect(fn); + return create_effect(RENDER_EFFECT | USER_EFFECT, fn, true); } /** @param {() => void | (() => void)} fn */ diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5a798ba3e996..b2009dae76e2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,8 @@ import { ROOT_EFFECT, DISCONNECTED, EFFECT_IS_UPDATING, - STALE_REACTION + STALE_REACTION, + USER_EFFECT } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -571,6 +572,8 @@ function flush_queued_effects(effects) { if ((effect.f & (DESTROYED | INERT)) === 0) { if (check_dirtiness(effect)) { + var wv = write_version; + update_effect(effect); // Effects with no dependencies or teardown do not get added to the effect tree. @@ -587,9 +590,19 @@ function flush_queued_effects(effects) { effect.fn = null; } } + + // 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 (write_version > wv && (effect.f & USER_EFFECT) !== 0) { + break; + } } } } + + for (; i < length; i += 1) { + schedule_effect(effects[i]); + } } /** diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte new file mode 100644 index 000000000000..2e789a046007 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte new file mode 100644 index 000000000000..1fad19bc1568 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte new file mode 100644 index 000000000000..b905b4b4d7e3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte @@ -0,0 +1,20 @@ + + + + +{#if object?.boolean} + + {@render children(object.boolean)} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js new file mode 100644 index 000000000000..8f9077e9544b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [open, close] = target.querySelectorAll('button'); + + flushSync(() => open.click()); + flushSync(() => close.click()); + + assert.deepEqual(logs, [true]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte new file mode 100644 index 000000000000..eee487fa132f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte @@ -0,0 +1,23 @@ + + + + + + +
+ + + {#snippet children(boolean)} + + {/snippet} + + From b049086bfde8adc541e1253b4b40c3e1d169b0ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Jul 2025 10:51:21 -0400 Subject: [PATCH 2/3] failing test --- .../samples/effect-order-7/A.svelte | 9 ++++++++ .../samples/effect-order-7/B.svelte | 9 ++++++++ .../samples/effect-order-7/Child.svelte | 20 +++++++++++++++++ .../samples/effect-order-7/_config.js | 13 +++++++++++ .../samples/effect-order-7/main.svelte | 22 +++++++++++++++++++ 5 files changed, 73 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte new file mode 100644 index 000000000000..54f4869d6252 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte @@ -0,0 +1,9 @@ + + +{boolean} + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte new file mode 100644 index 000000000000..2a2e634db156 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte new file mode 100644 index 000000000000..9606fd8602c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte @@ -0,0 +1,20 @@ + + + + +{#if object?.nested} + + {@render children(object.nested)} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js new file mode 100644 index 000000000000..8f9077e9544b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [open, close] = target.querySelectorAll('button'); + + flushSync(() => open.click()); + flushSync(() => close.click()); + + assert.deepEqual(logs, [true]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte new file mode 100644 index 000000000000..c9c45c50cf4d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte @@ -0,0 +1,22 @@ + + + + + + +
+ + + {#snippet children(nested)} +
+ {/snippet} + From 8d823390f92f0ad27527445620884ae7ad5769ae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 11:39:32 -0400 Subject: [PATCH 3/3] skip for now --- .../tests/runtime-runes/samples/effect-order-7/_config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js index 8f9077e9544b..29c33c7b1886 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + skip: true, + async test({ assert, target, logs }) { const [open, close] = target.querySelectorAll('button');