From 64f078d1dc94bbe1006aa0aee05b52e70ba12555 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 10:58:32 -0500 Subject: [PATCH 01/27] chore: remove unowned check when calling `e.effect_in_unowned_derived` --- packages/svelte/src/internal/client/reactivity/effects.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 8c4b84438c5b..cb2466642417 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -25,7 +25,6 @@ import { ROOT_EFFECT, EFFECT_TRANSPARENT, DERIVED, - UNOWNED, CLEAN, EAGER_EFFECT, HEAD_EFFECT, @@ -49,10 +48,10 @@ import { without_reactive_context } from '../dom/elements/bindings/shared.js'; */ export function validate_effect(rune) { if (active_effect === null && active_reaction === null) { - e.effect_orphan(rune); - } + if (active_reaction === null) { + e.effect_orphan(rune); + } - if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) { e.effect_in_unowned_derived(); } From c9d26db23fcece89071f6e591337f10dc0be6e81 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 13:13:07 -0500 Subject: [PATCH 02/27] WIP --- .../svelte/src/internal/client/constants.js | 3 +- .../internal/client/reactivity/deriveds.js | 12 ++-- .../src/internal/client/reactivity/sources.js | 7 +- .../svelte/src/internal/client/runtime.js | 72 ++----------------- 4 files changed, 18 insertions(+), 76 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index c2f7861b78ae..6597208bd6bb 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -26,8 +26,7 @@ export const EFFECT_PRESERVED = 1 << 19; export const USER_EFFECT = 1 << 20; // Flags exclusive to deriveds -export const UNOWNED = 1 << 8; -export const DISCONNECTED = 1 << 9; +export const CONNECTED = 1 << 9; /** * Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase. * Will be lifted during execution of the derived and during checking its dirty state (both are necessary diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 1eb640ad260c..1190a9a6aaef 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -9,9 +9,9 @@ import { EFFECT_PRESERVED, MAYBE_DIRTY, STALE_REACTION, - UNOWNED, ASYNC, - WAS_MARKED + WAS_MARKED, + CONNECTED } from '#client/constants'; import { active_reaction, @@ -61,9 +61,7 @@ export function derived(fn) { ? /** @type {Derived} */ (active_reaction) : null; - if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) { - flags |= UNOWNED; - } else { + if (active_effect !== null) { // Since deriveds are evaluated lazily, any effects created inside them are // created too late to ensure that the parent effect is added to the tree active_effect.f |= EFFECT_PRESERVED; @@ -372,7 +370,9 @@ export function update_derived(derived) { batch_values.set(derived, derived.v); } else { var status = - (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN; + (skip_reaction || (derived.f & CONNECTED) === 0) && derived.deps !== null + ? MAYBE_DIRTY + : CLEAN; set_signal_status(derived, status); } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index b480d4155aa9..f3568325cba9 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -23,12 +23,12 @@ import { DIRTY, BRANCH_EFFECT, EAGER_EFFECT, - UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, ROOT_EFFECT, ASYNC, - WAS_MARKED + WAS_MARKED, + CONNECTED } from '#client/constants'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -211,7 +211,8 @@ export function internal_set(source, value) { if ((source.f & DIRTY) !== 0) { execute_derived(/** @type {Derived} */ (source)); } - set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY); + + set_signal_status(source, (source.f & CONNECTED) !== 0 ? CLEAN : MAYBE_DIRTY); } source.wv = increment_write_version(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 76531d33207e..b1ecd1480b24 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -11,13 +11,12 @@ import { MAYBE_DIRTY, CLEAN, DERIVED, - UNOWNED, DESTROYED, BRANCH_EFFECT, STATE_SYMBOL, BLOCK_EFFECT, ROOT_EFFECT, - DISCONNECTED, + CONNECTED, REACTION_IS_UPDATING, STALE_REACTION, ERROR_VALUE, @@ -160,7 +159,6 @@ export function is_dirty(reaction) { if ((flags & MAYBE_DIRTY) !== 0) { var dependencies = reaction.deps; - var is_unowned = (flags & UNOWNED) !== 0; if (flags & DERIVED) { reaction.f &= ~WAS_MARKED; @@ -169,42 +167,8 @@ export function is_dirty(reaction) { if (dependencies !== null) { var i; var dependency; - var is_disconnected = (flags & DISCONNECTED) !== 0; - var is_unowned_connected = is_unowned && active_effect !== null && !skip_reaction; var length = dependencies.length; - // If we are working with a disconnected or an unowned signal that is now connected (due to an active effect) - // then we need to re-connect the reaction to the dependency, unless the effect has already been destroyed - // (which can happen if the derived is read by an async derived) - if ( - (is_disconnected || is_unowned_connected) && - (active_effect === null || (active_effect.f & DESTROYED) === 0) - ) { - var derived = /** @type {Derived} */ (reaction); - var parent = derived.parent; - - for (i = 0; i < length; i++) { - dependency = dependencies[i]; - - // We always re-add all reactions (even duplicates) if the derived was - // previously disconnected, however we don't if it was unowned as we - // de-duplicate dependencies in that case - if (is_disconnected || !dependency?.reactions?.includes(derived)) { - (dependency.reactions ??= []).push(derived); - } - } - - if (is_disconnected) { - derived.f ^= DISCONNECTED; - } - // If the unowned derived is now fully connected to the graph again (it's unowned and reconnected, has a parent - // and the parent is not unowned), then we can mark it as connected again, removing the need for the unowned - // flag - if (is_unowned_connected && parent !== null && (parent.f & UNOWNED) === 0) { - derived.f ^= UNOWNED; - } - } - for (i = 0; i < length; i++) { dependency = dependencies[i]; @@ -218,9 +182,7 @@ export function is_dirty(reaction) { } } - // Unowned signals should never be marked as clean unless they - // are used within an active_effect without skip_reaction - if (!is_unowned || (active_effect !== null && !skip_reaction)) { + if ((flags & CONNECTED) !== 0) { set_signal_status(reaction, CLEAN); } } @@ -274,8 +236,7 @@ export function update_reaction(reaction) { new_deps = /** @type {null | Value[]} */ (null); skipped_deps = 0; untracked_writes = null; - skip_reaction = - (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); + skip_reaction = false && (untracking || !is_updating_effect || active_reaction === null); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; current_sources = null; @@ -311,12 +272,7 @@ export function update_reaction(reaction) { reaction.deps = deps = new_deps; } - if ( - !skip_reaction || - // Deriveds that already have reactions can cleanup, so we still add them as reactions - ((flags & DERIVED) !== 0 && - /** @type {import('#client').Derived} */ (reaction).reactions !== null) - ) { + if (is_updating_effect) { for (i = skipped_deps; i < deps.length; i++) { (deps[i].reactions ??= []).push(reaction); } @@ -416,8 +372,8 @@ function remove_reaction(signal, dependency) { set_signal_status(dependency, MAYBE_DIRTY); // If we are working with a derived that is owned by an effect, then mark it as being // disconnected. - if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) { - dependency.f ^= DISCONNECTED; + if ((dependency.f & CONNECTED) !== 0) { + dependency.f ^= CONNECTED; } // Disconnect any reactions owned by this reaction destroy_derived_effects(/** @type {Derived} **/ (dependency)); @@ -585,20 +541,6 @@ export function get(signal) { } } } - } else if ( - is_derived && - /** @type {Derived} */ (signal).deps === null && - /** @type {Derived} */ (signal).effects === null - ) { - var derived = /** @type {Derived} */ (signal); - var parent = derived.parent; - - if (parent !== null && (parent.f & UNOWNED) === 0) { - // If the derived is owned by another derived then mark it as unowned - // as the derived value might have been referenced in a different context - // since and thus its parent might not be its true owner anymore - derived.f ^= UNOWNED; - } } if (DEV) { @@ -657,7 +599,7 @@ export function get(signal) { } if (is_derived) { - derived = /** @type {Derived} */ (signal); + var derived = /** @type {Derived} */ (signal); var value = derived.v; From db21188d4ad81e4a8cdb362200e455c8da9b8801 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 14:29:26 -0500 Subject: [PATCH 03/27] all non-unit tests passing --- .../svelte/src/internal/client/reactivity/deriveds.js | 6 +----- .../svelte/src/internal/client/reactivity/effects.js | 5 +++-- packages/svelte/src/internal/client/runtime.js | 11 ++++++++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 1190a9a6aaef..5d7ff712297a 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -369,11 +369,7 @@ export function update_derived(derived) { if (batch_values !== null) { batch_values.set(derived, derived.v); } else { - var status = - (skip_reaction || (derived.f & CONNECTED) === 0) && derived.deps !== null - ? MAYBE_DIRTY - : CLEAN; - + var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN; set_signal_status(derived, status); } } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index cb2466642417..2430e1231784 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -32,7 +32,8 @@ import { EFFECT_PRESERVED, STALE_REACTION, USER_EFFECT, - ASYNC + ASYNC, + CONNECTED } from '#client/constants'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; @@ -102,7 +103,7 @@ function create_effect(type, fn, sync, push = true) { deps: null, nodes_start: null, nodes_end: null, - f: type | DIRTY, + f: type | DIRTY | CONNECTED, first: null, fn, last: null, diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b1ecd1480b24..c4d97d15d0e1 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -272,7 +272,7 @@ export function update_reaction(reaction) { reaction.deps = deps = new_deps; } - if (is_updating_effect) { + if (is_updating_effect && (reaction.f & CONNECTED) !== 0) { for (i = skipped_deps; i < deps.length; i++) { (deps[i].reactions ??= []).push(reaction); } @@ -626,6 +626,15 @@ export function get(signal) { if (is_dirty(derived)) { update_derived(derived); } + + // reconnect a disconnected derived to the graph + if (is_updating_effect && (derived.f & CONNECTED) === 0 && derived.deps !== null) { + derived.f |= CONNECTED; + + for (const dep of derived.deps) { + (dep.reactions ??= []).push(derived); + } + } } if (batch_values?.has(signal)) { From b5bb6fb04bf4fc45ec8246a0c1bf898d771f048b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 15:41:23 -0500 Subject: [PATCH 04/27] tidy --- packages/svelte/src/internal/client/reactivity/deriveds.js | 1 - packages/svelte/src/internal/client/runtime.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5d7ff712297a..d7a093a1dafe 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -17,7 +17,6 @@ import { active_reaction, active_effect, set_signal_status, - skip_reaction, update_reaction, increment_write_version, set_active_effect, diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c4d97d15d0e1..c71b527049a5 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -138,7 +138,7 @@ export function set_update_version(value) { // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. -export let skip_reaction = false; +let skip_reaction = false; export function increment_write_version() { return ++write_version; From 625d28902c2410e55eabd165f48b873bcd243365 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 15:49:44 -0500 Subject: [PATCH 05/27] WIP --- packages/svelte/src/internal/client/runtime.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c71b527049a5..2d3064793100 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -4,6 +4,7 @@ import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js' import { destroy_block_effect_children, destroy_effect_children, + effect_tracking, execute_effect_teardown } from './reactivity/effects.js'; import { @@ -236,7 +237,7 @@ export function update_reaction(reaction) { new_deps = /** @type {null | Value[]} */ (null); skipped_deps = 0; untracked_writes = null; - skip_reaction = false && (untracking || !is_updating_effect || active_reaction === null); + skip_reaction = !effect_tracking(); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; current_sources = null; From 6700adb8fdc26b8c20ba7c22d11ca921b8b96630 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 16:18:00 -0500 Subject: [PATCH 06/27] WIP --- packages/svelte/src/internal/client/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2d3064793100..039ab1646c34 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -629,7 +629,7 @@ export function get(signal) { } // reconnect a disconnected derived to the graph - if (is_updating_effect && (derived.f & CONNECTED) === 0 && derived.deps !== null) { + if (effect_tracking() && (derived.f & CONNECTED) === 0 && derived.deps !== null) { derived.f |= CONNECTED; for (const dep of derived.deps) { From 697fba06fa81054c2d0332e35f70a335a4f72f45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 16:19:34 -0500 Subject: [PATCH 07/27] WIP --- packages/svelte/src/internal/client/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 039ab1646c34..4f7c56360931 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -273,7 +273,7 @@ export function update_reaction(reaction) { reaction.deps = deps = new_deps; } - if (is_updating_effect && (reaction.f & CONNECTED) !== 0) { + if (effect_tracking() && (reaction.f & CONNECTED) !== 0) { for (i = skipped_deps; i < deps.length; i++) { (deps[i].reactions ??= []).push(reaction); } From 6eb4deeb6d9cc1e45afc8c0b76bffcbcb3540c50 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 16:37:21 -0500 Subject: [PATCH 08/27] note to self --- packages/svelte/src/internal/client/runtime.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4f7c56360931..2cc9e0624bdb 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -49,6 +49,7 @@ import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; import { without_reactive_context } from './dom/elements/bindings/shared.js'; +// TODO we can remove this i think export let is_updating_effect = false; /** @param {boolean} value */ From 93f277dbbebb90a11ba3a34e3537e61d2ad5d69b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 17:10:44 -0500 Subject: [PATCH 09/27] fix --- packages/svelte/src/internal/client/runtime.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2cc9e0624bdb..ee87d109061c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -274,7 +274,7 @@ export function update_reaction(reaction) { reaction.deps = deps = new_deps; } - if (effect_tracking() && (reaction.f & CONNECTED) !== 0) { + if (is_updating_effect && effect_tracking() && (reaction.f & CONNECTED) !== 0) { for (i = skipped_deps; i < deps.length; i++) { (deps[i].reactions ??= []).push(reaction); } @@ -630,7 +630,12 @@ export function get(signal) { } // reconnect a disconnected derived to the graph - if (effect_tracking() && (derived.f & CONNECTED) === 0 && derived.deps !== null) { + if ( + is_updating_effect && + effect_tracking() && + (derived.f & CONNECTED) === 0 && + derived.deps !== null + ) { derived.f |= CONNECTED; for (const dep of derived.deps) { From e1fbee5160ede7a2255e9ac9358dc5dc589c0101 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 17:25:15 -0500 Subject: [PATCH 10/27] fix --- .../svelte/src/internal/client/runtime.js | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index ee87d109061c..f7da942e7990 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -629,18 +629,8 @@ export function get(signal) { update_derived(derived); } - // reconnect a disconnected derived to the graph - if ( - is_updating_effect && - effect_tracking() && - (derived.f & CONNECTED) === 0 && - derived.deps !== null - ) { - derived.f |= CONNECTED; - - for (const dep of derived.deps) { - (dep.reactions ??= []).push(derived); - } + if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { + reconnect(derived); } } @@ -655,6 +645,25 @@ export function get(signal) { return signal.v; } +/** + * (Re)connect a disconnected derived, so that it is notified + * of changes in `mark_reactions` + * @param {Derived} derived + */ +function reconnect(derived) { + if (derived.deps === null) return; + + derived.f ^= CONNECTED; + + for (const dep of derived.deps) { + (dep.reactions ??= []).push(derived); + + if ((dep.f & DERIVED) !== 0 && (dep.f & CONNECTED) === 0) { + reconnect(/** @type {Derived} */ (dep)); + } + } +} + /** @param {Derived} derived */ function depends_on_old_values(derived) { if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst From b61d1b0887794c5e9b484cfce4d675cfe7fa6102 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Nov 2025 17:26:53 -0500 Subject: [PATCH 11/27] hmm maybe not --- packages/svelte/src/internal/client/runtime.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index f7da942e7990..5f6f4e473e9a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -49,7 +49,6 @@ import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; import { without_reactive_context } from './dom/elements/bindings/shared.js'; -// TODO we can remove this i think export let is_updating_effect = false; /** @param {boolean} value */ From 15f9f859de4743e237c4f76e6fe625ec6f5781fb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 08:30:23 -0500 Subject: [PATCH 12/27] try this --- packages/svelte/src/internal/client/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5f6f4e473e9a..b42736dfe529 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -521,7 +521,7 @@ export function get(signal) { skipped_deps++; } else if (new_deps === null) { new_deps = [signal]; - } else if (!skip_reaction || !new_deps.includes(signal)) { + } else { // Normally we can push duplicated dependencies to `new_deps`, but if we're inside // an unowned derived because skip_reaction is true, then we need to ensure that // we don't have duplicates From 67f544e0640054ec9c7e730f91af60d3c2cd1a18 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:01:04 -0500 Subject: [PATCH 13/27] simplify --- packages/svelte/src/internal/client/runtime.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b42736dfe529..fdced6b29eac 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -521,10 +521,7 @@ export function get(signal) { skipped_deps++; } else if (new_deps === null) { new_deps = [signal]; - } else { - // Normally we can push duplicated dependencies to `new_deps`, but if we're inside - // an unowned derived because skip_reaction is true, then we need to ensure that - // we don't have duplicates + } else if (!new_deps.includes(signal)) { new_deps.push(signal); } } From d1149d61c6645fe75409d28b79ab69bfb912a028 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:05:08 -0500 Subject: [PATCH 14/27] remove skip_reaction --- packages/svelte/src/internal/client/runtime.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fdced6b29eac..8eafd223e57c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -137,10 +137,6 @@ export function set_update_version(value) { update_version = value; } -// If we are working with a get() chain that has no active container, -// to prevent memory leaks, we skip adding the reaction. -let skip_reaction = false; - export function increment_write_version() { return ++write_version; } @@ -226,7 +222,6 @@ export function update_reaction(reaction) { var previous_skipped_deps = skipped_deps; var previous_untracked_writes = untracked_writes; var previous_reaction = active_reaction; - var previous_skip_reaction = skip_reaction; var previous_sources = current_sources; var previous_component_context = component_context; var previous_untracking = untracking; @@ -237,7 +232,6 @@ export function update_reaction(reaction) { new_deps = /** @type {null | Value[]} */ (null); skipped_deps = 0; untracked_writes = null; - skip_reaction = !effect_tracking(); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; current_sources = null; @@ -330,7 +324,6 @@ export function update_reaction(reaction) { skipped_deps = previous_skipped_deps; untracked_writes = previous_untracked_writes; active_reaction = previous_reaction; - skip_reaction = previous_skip_reaction; current_sources = previous_sources; set_component_context(previous_component_context); untracking = previous_untracking; From d5c760570baafe1c38403a54e0396b34364f263f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:17:14 -0500 Subject: [PATCH 15/27] docs --- packages/svelte/src/internal/client/constants.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 6597208bd6bb..b39afef51682 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -6,6 +6,13 @@ export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; export const ROOT_EFFECT = 1 << 6; export const BOUNDARY_EFFECT = 1 << 7; +/** + * Indicates that a reaction is connected to an effect root — either it is an effect, + * or it is a derived that is depended on by at least one effect. If a derived has + * no dependents, we can disconnect it from the graph, allowing it to either be + * GC'd or reconnected later if an effect comes to depend on it again + */ +export const CONNECTED = 1 << 9; export const CLEAN = 1 << 10; export const DIRTY = 1 << 11; export const MAYBE_DIRTY = 1 << 12; @@ -26,7 +33,6 @@ export const EFFECT_PRESERVED = 1 << 19; export const USER_EFFECT = 1 << 20; // Flags exclusive to deriveds -export const CONNECTED = 1 << 9; /** * Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase. * Will be lifted during execution of the derived and during checking its dirty state (both are necessary From 0bd13a961735f3e0e946d878e01a2769c2048c97 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:18:01 -0500 Subject: [PATCH 16/27] add changeset, in case this results in changed behaviour --- .changeset/four-paths-cheer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-paths-cheer.md diff --git a/.changeset/four-paths-cheer.md b/.changeset/four-paths-cheer.md new file mode 100644 index 000000000000..54a697a8a42b --- /dev/null +++ b/.changeset/four-paths-cheer.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify connection/disconnection logic From 31970224714e1a4612ebf18e6f83b762f7378c72 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:19:46 -0500 Subject: [PATCH 17/27] Update packages/svelte/src/internal/client/reactivity/effects.js Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- packages/svelte/src/internal/client/reactivity/effects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2430e1231784..5d7c0ef871fd 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -48,7 +48,7 @@ import { without_reactive_context } from '../dom/elements/bindings/shared.js'; * @param {'$effect' | '$effect.pre' | '$inspect'} rune */ export function validate_effect(rune) { - if (active_effect === null && active_reaction === null) { + if (active_effect === null) { if (active_reaction === null) { e.effect_orphan(rune); } From 4d5209c5116bd479f167ee4a1dcca6cd9ac6dafc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:28:11 -0500 Subject: [PATCH 18/27] fix #17024 --- .../svelte/src/internal/client/runtime.js | 8 ++--- .../async-derived-unowned/Component.svelte | 6 ++++ .../samples/async-derived-unowned/_config.js | 30 +++++++++++++++++++ .../samples/async-derived-unowned/main.svelte | 19 ++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8eafd223e57c..654df812dfc7 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -610,6 +610,10 @@ export function get(signal) { } else if (is_derived) { derived = /** @type {Derived} */ (signal); + if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { + reconnect(derived); + } + if (batch_values?.has(derived)) { return batch_values.get(derived); } @@ -617,10 +621,6 @@ export function get(signal) { if (is_dirty(derived)) { update_derived(derived); } - - if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { - reconnect(derived); - } } if (batch_values?.has(signal)) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte new file mode 100644 index 000000000000..36ad0dfaea28 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte @@ -0,0 +1,6 @@ + + +

{double}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js new file mode 100644 index 000000000000..fc0135623d7a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js @@ -0,0 +1,30 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2

+ ` + ); + + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

4

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte new file mode 100644 index 000000000000..bd82e35a3bc3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte @@ -0,0 +1,19 @@ + + + + {await new Promise((r) => { + // long enough for the test to do all its other stuff while this is pending + setTimeout(r, 10); + })} + {#snippet pending()}{/snippet} + + + + +{#if count > 0} + +{/if} From b8fc364dbd5c7aa1876b245a4731620a54eb9899 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:29:39 -0500 Subject: [PATCH 19/27] fix comment --- .../samples/async-derived-unowned/Component.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte index 36ad0dfaea28..f7d138a3ed2e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte @@ -1,6 +1,6 @@

{double}

From 3d4fa95f449b41e4fb8710f3b65e220c3fb10419 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:43:41 -0500 Subject: [PATCH 20/27] revert --- packages/svelte/src/internal/client/runtime.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 654df812dfc7..8eafd223e57c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -610,10 +610,6 @@ export function get(signal) { } else if (is_derived) { derived = /** @type {Derived} */ (signal); - if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { - reconnect(derived); - } - if (batch_values?.has(derived)) { return batch_values.get(derived); } @@ -621,6 +617,10 @@ export function get(signal) { if (is_dirty(derived)) { update_derived(derived); } + + if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { + reconnect(derived); + } } if (batch_values?.has(signal)) { From 027194a6716ab67e2ff6ce0664e5799fd77c66ce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:47:01 -0500 Subject: [PATCH 21/27] fix --- packages/svelte/src/internal/client/runtime.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8eafd223e57c..c81fcdd1c565 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -611,6 +611,11 @@ export function get(signal) { derived = /** @type {Derived} */ (signal); if (batch_values?.has(derived)) { + // TODO DRY out + if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { + reconnect(derived); + } + return batch_values.get(derived); } @@ -621,9 +626,7 @@ export function get(signal) { if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { reconnect(derived); } - } - - if (batch_values?.has(signal)) { + } else if (batch_values?.has(signal)) { return batch_values.get(signal); } From 69290af1fed1ed6604709a7737fd6149588006e7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:48:30 -0500 Subject: [PATCH 22/27] dry --- packages/svelte/src/internal/client/runtime.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c81fcdd1c565..ed1521354591 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -610,9 +610,10 @@ export function get(signal) { } else if (is_derived) { derived = /** @type {Derived} */ (signal); + var should_reconnect = is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0; + if (batch_values?.has(derived)) { - // TODO DRY out - if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { + if (should_reconnect) { reconnect(derived); } @@ -623,7 +624,7 @@ export function get(signal) { update_derived(derived); } - if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { + if (should_reconnect) { reconnect(derived); } } else if (batch_values?.has(signal)) { From c96eb44343ad148fe2aa8d8686842bc327e93710 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Nov 2025 09:49:31 -0500 Subject: [PATCH 23/27] changeset --- .changeset/whole-webs-stick.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/whole-webs-stick.md diff --git a/.changeset/whole-webs-stick.md b/.changeset/whole-webs-stick.md new file mode 100644 index 000000000000..fe8b614a019e --- /dev/null +++ b/.changeset/whole-webs-stick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reconnect deriveds to effect tree when time-travelling From 1032b313aba1b79e0a942289dceb4eafc62f9096 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 4 Nov 2025 16:53:10 +0100 Subject: [PATCH 24/27] fix WAS_MARKED logic --- .../src/internal/client/reactivity/sources.js | 5 +++- .../svelte/src/internal/client/runtime.js | 29 +++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index f3568325cba9..0b5ad33bfcb1 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -335,7 +335,10 @@ function mark_reactions(signal, status) { if ((flags & DERIVED) !== 0) { if ((flags & WAS_MARKED) === 0) { - reaction.f |= WAS_MARKED; + // Only connected deriveds can be reliably unmarked right away + if (flags & CONNECTED) { + reaction.f |= WAS_MARKED; + } mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); } } else if (not_dirty) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index ed1521354591..3a31c175f109 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -154,13 +154,13 @@ export function is_dirty(reaction) { return true; } + if (flags & DERIVED) { + reaction.f &= ~WAS_MARKED; + } + if ((flags & MAYBE_DIRTY) !== 0) { var dependencies = reaction.deps; - if (flags & DERIVED) { - reaction.f &= ~WAS_MARKED; - } - if (dependencies !== null) { var i; var dependency; @@ -365,9 +365,10 @@ function remove_reaction(signal, dependency) { ) { set_signal_status(dependency, MAYBE_DIRTY); // If we are working with a derived that is owned by an effect, then mark it as being - // disconnected. + // disconnected and remove the mark flag, as it cannot be reliably removed otherwise if ((dependency.f & CONNECTED) !== 0) { dependency.f ^= CONNECTED; + dependency.f &= ~WAS_MARKED; } // Disconnect any reactions owned by this reaction destroy_derived_effects(/** @type {Derived} **/ (dependency)); @@ -613,6 +614,10 @@ export function get(signal) { var should_reconnect = is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0; if (batch_values?.has(derived)) { + // This happens as part of is_dirty normally, but we return early + // here so we need to do it separately + remove_marked_flag(derived); + if (should_reconnect) { reconnect(derived); } @@ -657,6 +662,20 @@ function reconnect(derived) { } } +/** + * Removes the WAS_MARKED flag from the derived and its dependencies + * @param {Derived} derived + */ +function remove_marked_flag(derived) { + if ((derived.f & WAS_MARKED) === 0) return; + derived.f ^= WAS_MARKED; + + // Only deriveds with dependencies can be marked + for (const dep of /** @type {Value[]} */ (derived.deps)) { + remove_marked_flag(/** @type {Derived} */ (dep)); + } +} + /** @param {Derived} derived */ function depends_on_old_values(derived) { if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst From 22435b2447645b0e8b083373d1e00c7424c5f27c Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 5 Nov 2025 15:14:43 +0100 Subject: [PATCH 25/27] failing test (that uncovered other unrelated bug) + fix --- .../svelte/src/internal/client/runtime.js | 13 +++++---- .../Component.svelte | 27 +++++++++++++++++++ .../_config.js | 18 +++++++++++++ .../main.svelte | 19 +++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3a31c175f109..5f59b7aeacd3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -664,15 +664,18 @@ function reconnect(derived) { /** * Removes the WAS_MARKED flag from the derived and its dependencies - * @param {Derived} derived + * @param {Value} derived */ function remove_marked_flag(derived) { - if ((derived.f & WAS_MARKED) === 0) return; - derived.f ^= WAS_MARKED; + // We cannot stop at the first non-marked derived because batch_values can + // cause "holes" of unmarked deriveds in an otherwise marked graph + if ((derived.f & DERIVED) === 0) return; + + derived.f &= ~WAS_MARKED; // Only deriveds with dependencies can be marked - for (const dep of /** @type {Value[]} */ (derived.deps)) { - remove_marked_flag(/** @type {Derived} */ (dep)); + for (const dep of /** @type {Value[]} */ (/** @type {Derived} */ (derived).deps)) { + remove_marked_flag(dep); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte new file mode 100644 index 000000000000..200778dc5bc9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte @@ -0,0 +1,27 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js new file mode 100644 index 000000000000..600e7f096b0a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js @@ -0,0 +1,18 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + // TODO this is wrong: it should be [5] + assert.deepEqual(logs, [4]); + + button?.click(); + await tick(); + // TODO this is wrong: it should be [5, 7] + assert.deepEqual(logs, [4, 7]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte new file mode 100644 index 000000000000..bd82e35a3bc3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte @@ -0,0 +1,19 @@ + + + + {await new Promise((r) => { + // long enough for the test to do all its other stuff while this is pending + setTimeout(r, 10); + })} + {#snippet pending()}{/snippet} + + + + +{#if count > 0} + +{/if} From d55107f94cda5568aae141389a4846e8f7340458 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:17:07 +0100 Subject: [PATCH 26/27] fix: delete from batch_values on updates (#17115) * fix: delete from batch_values on updates This fixes a bug where a derived would still show its old value even after it was indirectly updated again within the same batch. This can for example happen by reading a derived on an effect, then writing to a source in that same effect that makes the derived update, and then read the derived value in a sibling effect - it still shows the old value without the fix. The fix is to _delete_ the value from batch_values, as it's now the newest value across all batches. In order to not prevent breakage on other batches we have to leave the status of deriveds as-is, i.e. within is_dirty and update_derived we cannot update its status. That might be a bit more inefficient as you now have to traverse the graph for each `get` of that derived (it's a bit like they are all disconnected) but we can always optimize that later if need be. Another advantage of this fix is that we can get rid of duplicate logic we had to add about unmarking and reconnecting deriveds, because that logic was only needed for the case where deriveds are read after they are updated, which now no longer hits that if-branch * keep derived cache, but clear it in mark_reactions (#17116) --------- Co-authored-by: Rich Harris --- .../internal/client/reactivity/deriveds.js | 10 ++++-- .../src/internal/client/reactivity/sources.js | 9 +++-- .../svelte/src/internal/client/runtime.js | 36 ++++--------------- .../_config.js | 6 ++-- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index d7a093a1dafe..7e6f3c6f604f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -26,7 +26,7 @@ import { import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; -import { async_effect, destroy_effect, teardown } from './effects.js'; +import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js'; import { eager_effects, internal_set, set_eager_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -365,8 +365,14 @@ export function update_derived(derived) { return; } + // During time traveling we don't want to reset the status so that + // traversal of the graph in the other batches still happens if (batch_values !== null) { - batch_values.set(derived, derived.v); + // only cache the value if we're in a tracking context, otherwise we won't + // clear the cache in `mark_reactions` when dependencies are updated + if (effect_tracking()) { + batch_values.set(derived, derived.v); + } } else { var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN; set_signal_status(derived, status); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 0b5ad33bfcb1..8ae406b57b30 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -34,7 +34,7 @@ import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack, tag_proxy } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; -import { Batch, eager_block_effects, schedule_effect } from './batch.js'; +import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; @@ -334,12 +334,17 @@ function mark_reactions(signal, status) { } if ((flags & DERIVED) !== 0) { + var derived = /** @type {Derived} */ (reaction); + + batch_values?.delete(derived); + if ((flags & WAS_MARKED) === 0) { // Only connected deriveds can be reliably unmarked right away if (flags & CONNECTED) { reaction.f |= WAS_MARKED; } - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + + mark_reactions(derived, MAYBE_DIRTY); } } else if (not_dirty) { if ((flags & BLOCK_EFFECT) !== 0) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5f59b7aeacd3..74797f3b56fe 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -179,7 +179,12 @@ export function is_dirty(reaction) { } } - if ((flags & CONNECTED) !== 0) { + if ( + (flags & CONNECTED) !== 0 && + // During time traveling we don't want to reset the status so that + // traversal of the graph in the other batches still happens + batch_values === null + ) { set_signal_status(reaction, CLEAN); } } @@ -611,17 +616,7 @@ export function get(signal) { } else if (is_derived) { derived = /** @type {Derived} */ (signal); - var should_reconnect = is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0; - if (batch_values?.has(derived)) { - // This happens as part of is_dirty normally, but we return early - // here so we need to do it separately - remove_marked_flag(derived); - - if (should_reconnect) { - reconnect(derived); - } - return batch_values.get(derived); } @@ -629,7 +624,7 @@ export function get(signal) { update_derived(derived); } - if (should_reconnect) { + if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { reconnect(derived); } } else if (batch_values?.has(signal)) { @@ -662,23 +657,6 @@ function reconnect(derived) { } } -/** - * Removes the WAS_MARKED flag from the derived and its dependencies - * @param {Value} derived - */ -function remove_marked_flag(derived) { - // We cannot stop at the first non-marked derived because batch_values can - // cause "holes" of unmarked deriveds in an otherwise marked graph - if ((derived.f & DERIVED) === 0) return; - - derived.f &= ~WAS_MARKED; - - // Only deriveds with dependencies can be marked - for (const dep of /** @type {Value[]} */ (/** @type {Derived} */ (derived).deps)) { - remove_marked_flag(dep); - } -} - /** @param {Derived} derived */ function depends_on_old_values(derived) { if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js index 600e7f096b0a..15bb42074f73 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js @@ -7,12 +7,10 @@ export default test({ button?.click(); await tick(); - // TODO this is wrong: it should be [5] - assert.deepEqual(logs, [4]); + assert.deepEqual(logs, [5]); button?.click(); await tick(); - // TODO this is wrong: it should be [5, 7] - assert.deepEqual(logs, [4, 7]); + assert.deepEqual(logs, [5, 7]); } }); From df077f319e864cfa3f4dc346279163227254006c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Nov 2025 14:25:28 -0500 Subject: [PATCH 27/27] tidy up --- packages/svelte/src/internal/client/runtime.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 74797f3b56fe..258f6962fa81 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -162,12 +162,10 @@ export function is_dirty(reaction) { var dependencies = reaction.deps; if (dependencies !== null) { - var i; - var dependency; var length = dependencies.length; - for (i = 0; i < length; i++) { - dependency = dependencies[i]; + for (var i = 0; i < length; i++) { + var dependency = dependencies[i]; if (is_dirty(/** @type {Derived} */ (dependency))) { update_derived(/** @type {Derived} */ (dependency));