From eea8a18cf2ff8245f0687e8012d5923c5d508d03 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Mon, 3 Nov 2025 21:02:09 +0100
Subject: [PATCH 1/4] fix: ensure fork always accesses correct values (#17098)
Not all batches will flush right after being activated, some will be activated and then `get` is called on a signal. In that case the value was wrong because we did not apply the changes of that batch. By doing `this.apply()` during `activate()` we ensure we do, which fixes (among other things, likely) a forking bug where old values where sneaking in.
Fixes #17079
---
.changeset/eight-news-laugh.md | 5 +++++
.../src/internal/client/reactivity/batch.js | 13 +++++++++----
.../samples/async-fork-if/Child.svelte | 8 ++++++++
.../samples/async-fork-if/_config.js | 12 ++++++++++++
.../samples/async-fork-if/main.svelte | 17 +++++++++++++++++
5 files changed, 51 insertions(+), 4 deletions(-)
create mode 100644 .changeset/eight-news-laugh.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte
diff --git a/.changeset/eight-news-laugh.md b/.changeset/eight-news-laugh.md
new file mode 100644
index 0000000000..e120b19f5e
--- /dev/null
+++ b/.changeset/eight-news-laugh.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ensure fork always accesses correct values
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 27c90d7708..57aa185a31 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -15,7 +15,8 @@ import {
DERIVED,
BOUNDARY_EFFECT,
EAGER_EFFECT,
- HEAD_EFFECT
+ HEAD_EFFECT,
+ ERROR_VALUE
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
@@ -285,12 +286,16 @@ export class Batch {
this.previous.set(source, value);
}
- this.current.set(source, source.v);
- batch_values?.set(source, source.v);
+ // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
+ if ((source.f & ERROR_VALUE) === 0) {
+ this.current.set(source, source.v);
+ batch_values?.set(source, source.v);
+ }
}
activate() {
current_batch = this;
+ this.apply();
}
deactivate() {
@@ -492,7 +497,7 @@ export class Batch {
}
apply() {
- if (!async_mode_flag || batches.size === 1) return;
+ if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return;
// if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch...
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte
new file mode 100644
index 0000000000..6ef7d03eea
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte
@@ -0,0 +1,8 @@
+
+
+{x}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js
new file mode 100644
index 0000000000..1bc168d9ae
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js
@@ -0,0 +1,12 @@
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const btn = target.querySelector('button');
+
+ btn?.click();
+ await new Promise((r) => setTimeout(r, 2));
+ assert.htmlEqual(target.innerHTML, ` universe`);
+ assert.deepEqual(logs, ['universe', 'universe']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte
new file mode 100644
index 0000000000..625040ec13
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+{#if x === 'universe'}
+
+{/if}
From 7a2435471c96c43d6f00144cdb5889772703d786 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 3 Nov 2025 20:53:51 -0500
Subject: [PATCH 2/4] Version Packages (#17087)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.changeset/eight-news-laugh.md | 5 -----
.changeset/legal-mangos-peel.md | 5 -----
.changeset/sixty-comics-bow.md | 5 -----
packages/svelte/CHANGELOG.md | 10 ++++++++++
packages/svelte/package.json | 2 +-
packages/svelte/src/version.js | 2 +-
6 files changed, 12 insertions(+), 17 deletions(-)
delete mode 100644 .changeset/eight-news-laugh.md
delete mode 100644 .changeset/legal-mangos-peel.md
delete mode 100644 .changeset/sixty-comics-bow.md
diff --git a/.changeset/eight-news-laugh.md b/.changeset/eight-news-laugh.md
deleted file mode 100644
index e120b19f5e..0000000000
--- a/.changeset/eight-news-laugh.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: ensure fork always accesses correct values
diff --git a/.changeset/legal-mangos-peel.md b/.changeset/legal-mangos-peel.md
deleted file mode 100644
index bddad21bff..0000000000
--- a/.changeset/legal-mangos-peel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: change title only after any pending work has completed
diff --git a/.changeset/sixty-comics-bow.md b/.changeset/sixty-comics-bow.md
deleted file mode 100644
index 2463e52430..0000000000
--- a/.changeset/sixty-comics-bow.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: preserve symbols when creating derived rest properties
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index bcf17fe45e..bc2f815908 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,15 @@
# svelte
+## 5.43.3
+
+### Patch Changes
+
+- fix: ensure fork always accesses correct values ([#17098](https://github.com/sveltejs/svelte/pull/17098))
+
+- fix: change title only after any pending work has completed ([#17061](https://github.com/sveltejs/svelte/pull/17061))
+
+- fix: preserve symbols when creating derived rest properties ([#17096](https://github.com/sveltejs/svelte/pull/17096))
+
## 5.43.2
### Patch Changes
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index f7a1cca616..f178444593 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.43.2",
+ "version": "5.43.3",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 0a28702778..5ad40ddee6 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.43.2';
+export const VERSION = '5.43.3';
export const PUBLIC_VERSION = '5';
From 46e9d2d357d724479fbf6db6c0d9ebb22bab9796 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Thu, 6 Nov 2025 05:47:39 -0500
Subject: [PATCH 3/4] chore: remove `UNOWNED` flag (#17105)
Fixes #17024
Fixes #17049 (comment) (and therefore everything that was still buggy in that issue I think)
* chore: remove unowned check when calling `e.effect_in_unowned_derived`
* WIP
* all non-unit tests passing
* tidy
* WIP
* WIP
* WIP
* note to self
* fix
* fix
* hmm maybe not
* try this
* simplify
* remove skip_reaction
* docs
* add changeset, in case this results in changed behaviour
* Update packages/svelte/src/internal/client/reactivity/effects.js
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
* fix #17024
* fix comment
* revert
* fix
* dry
* changeset
* fix WAS_MARKED logic
* failing test (that uncovered other unrelated bug) + fix
* 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
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Simon Holthausen
---
.changeset/four-paths-cheer.md | 5 +
.changeset/whole-webs-stick.md | 5 +
.../svelte/src/internal/client/constants.js | 9 +-
.../internal/client/reactivity/deriveds.js | 23 ++--
.../src/internal/client/reactivity/effects.js | 14 +-
.../src/internal/client/reactivity/sources.js | 21 ++-
.../svelte/src/internal/client/runtime.js | 130 ++++++------------
.../Component.svelte | 27 ++++
.../_config.js | 16 +++
.../main.svelte | 19 +++
.../async-derived-unowned/Component.svelte | 6 +
.../samples/async-derived-unowned/_config.js | 30 ++++
.../samples/async-derived-unowned/main.svelte | 19 +++
13 files changed, 212 insertions(+), 112 deletions(-)
create mode 100644 .changeset/four-paths-cheer.md
create mode 100644 .changeset/whole-webs-stick.md
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
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/.changeset/four-paths-cheer.md b/.changeset/four-paths-cheer.md
new file mode 100644
index 0000000000..54a697a8a4
--- /dev/null
+++ b/.changeset/four-paths-cheer.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+chore: simplify connection/disconnection logic
diff --git a/.changeset/whole-webs-stick.md b/.changeset/whole-webs-stick.md
new file mode 100644
index 0000000000..fe8b614a01
--- /dev/null
+++ b/.changeset/whole-webs-stick.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: reconnect deriveds to effect tree when time-travelling
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index c2f7861b78..b39afef516 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,8 +33,6 @@ 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;
/**
* 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 1eb640ad26..7e6f3c6f60 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -9,15 +9,14 @@ import {
EFFECT_PRESERVED,
MAYBE_DIRTY,
STALE_REACTION,
- UNOWNED,
ASYNC,
- WAS_MARKED
+ WAS_MARKED,
+ CONNECTED
} from '#client/constants';
import {
active_reaction,
active_effect,
set_signal_status,
- skip_reaction,
update_reaction,
increment_write_version,
set_active_effect,
@@ -27,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';
@@ -61,9 +60,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;
@@ -368,12 +365,16 @@ 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 =
- (skip_reaction || (derived.f & UNOWNED) !== 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 8c4b84438c..5d7c0ef871 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,
@@ -33,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';
@@ -48,11 +48,11 @@ 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) {
- e.effect_orphan(rune);
- }
+ if (active_effect === null) {
+ 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();
}
@@ -103,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/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index b480d4155a..8ae406b57b 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -23,18 +23,18 @@ 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';
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';
@@ -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();
@@ -333,9 +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) {
- reaction.f |= WAS_MARKED;
- mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
+ // Only connected deriveds can be reliably unmarked right away
+ if (flags & CONNECTED) {
+ reaction.f |= WAS_MARKED;
+ }
+
+ 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 76531d3320..258f6962fa 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 {
@@ -11,13 +12,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,
@@ -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.
-export let skip_reaction = false;
-
export function increment_write_version() {
return ++write_version;
}
@@ -158,55 +154,18 @@ export function is_dirty(reaction) {
return true;
}
+ if (flags & DERIVED) {
+ reaction.f &= ~WAS_MARKED;
+ }
+
if ((flags & MAYBE_DIRTY) !== 0) {
var dependencies = reaction.deps;
- var is_unowned = (flags & UNOWNED) !== 0;
-
- if (flags & DERIVED) {
- reaction.f &= ~WAS_MARKED;
- }
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];
+ for (var i = 0; i < length; i++) {
+ var dependency = dependencies[i];
if (is_dirty(/** @type {Derived} */ (dependency))) {
update_derived(/** @type {Derived} */ (dependency));
@@ -218,9 +177,12 @@ 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 &&
+ // 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);
}
}
@@ -263,7 +225,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;
@@ -274,8 +235,6 @@ 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);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
current_sources = null;
@@ -311,12 +270,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 && effect_tracking() && (reaction.f & CONNECTED) !== 0) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
@@ -373,7 +327,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;
@@ -415,9 +368,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.
- if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) {
- dependency.f ^= 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));
@@ -564,10 +518,7 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
- } else if (!skip_reaction || !new_deps.includes(signal)) {
- // 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);
}
}
@@ -585,20 +536,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 +594,7 @@ export function get(signal) {
}
if (is_derived) {
- derived = /** @type {Derived} */ (signal);
+ var derived = /** @type {Derived} */ (signal);
var value = derived.v;
@@ -684,9 +621,11 @@ export function get(signal) {
if (is_dirty(derived)) {
update_derived(derived);
}
- }
- if (batch_values?.has(signal)) {
+ if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) {
+ reconnect(derived);
+ }
+ } else if (batch_values?.has(signal)) {
return batch_values.get(signal);
}
@@ -697,6 +636,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
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 0000000000..200778dc5b
--- /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 0000000000..15bb42074f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js
@@ -0,0 +1,16 @@
+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();
+ assert.deepEqual(logs, [5]);
+
+ button?.click();
+ await tick();
+ assert.deepEqual(logs, [5, 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 0000000000..bd82e35a3b
--- /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}
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 0000000000..f7d138a3ed
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte
@@ -0,0 +1,6 @@
+
+
+