From ab9863cebbd397b281319f771a8c4e502cf0a44e Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 16 Oct 2026 19:37:22 +0200 Subject: [PATCH] fix --- .../src/internal/client/reactivity/batch.js | 3 +- .../internal/client/reactivity/deriveds.js | 18 +++-- .../svelte/src/internal/client/runtime.js | 66 +++++++++++++------ .../async-derived-assigned-view/_config.js | 23 +++++++ .../async-derived-assigned-view/main.svelte | 26 ++++++++ 5 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-assigned-view/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-assigned-view/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 7416ba8a0f..75ac8fc4eb 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -38,7 +38,7 @@ import { flush_eager_effects, old_values, set_eager_effects, source, update } fr import { eager_effect, teardown, unlink_effect } from './effects.js'; import { defer_effect } from './utils.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { set_signal_status, update_derived_status } from './status.js'; +import { set_signal_status } from './status.js'; import { invariant } from '../../shared/dev.js'; import { OBSOLETE } from './deriveds.js'; @@ -1538,7 +1538,6 @@ export function fork(fn) { if ((source.f & DERIVED) !== 0 && /** @type {BatchRecord} */ (entry).dv !== undefined) { // an assigned derived, which holds its value until its dependencies change promote_record(/** @type {Derived} */ (source), /** @type {BatchRecord} */ (entry)); - update_derived_status(/** @type {Derived} */ (source)); } } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index f8ef032762..d4fbb88b23 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -418,10 +418,6 @@ function adopt(derived, records) { records.delete(derived); promote_record(derived, record); - derived.v = record.v; - derived.wv = /** @type {number} */ (record.wv); - derived.f = record.e ? derived.f | ERROR_VALUE : derived.f & ~ERROR_VALUE; - update_derived_status(derived); return true; } @@ -437,11 +433,19 @@ function adopt(derived, records) { */ export function view_entry(derived) { var entry = /** @type {Map} */ (batch_values).get(derived); + var batch = /** @type {Batch} */ (view_batch); - // either cached, or the derived was overridden via assignment in this batch - if (entry !== undefined) return entry; + // Cached — unless it is a value the derived was assigned in this batch, which holds as long as its + // dependencies keep the versions they had then (if the assignment is the latest value, the derived's + // global state is that value, see `internal_set`) + if ( + entry !== undefined && + (entry !== batch.current.get(derived) || + (entry.dv !== undefined && is_current(entry.deps, entry.dv))) + ) { + return entry; + } - var batch = /** @type {Batch} */ (view_batch); var record = batch.record(derived); if (record !== undefined && is_current(record.deps, record.dv)) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6e5af6db5f..81d8768397 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -186,12 +186,22 @@ export function is_current(deps, dv) { return true; } +/** + * The run of `reaction` that applies in the active view of the world: the record of a block or async + * effect in the active batch, if it has one (see `update_record`), or else the reaction itself — whose + * `deps` and `dv` are its global state + * @param {Reaction} reaction + * @returns {Reaction | BatchRecord} + */ +function run_of(reaction) { + return ((reaction.f & (BLOCK_EFFECT | ASYNC)) !== 0 && view_batch?.record(reaction)) || reaction; +} + /** * Determines whether a derived or effect is dirty. * If it is MAYBE_DIRTY, will set the status to CLEAN. * Deriveds are only checked like this in the real world — within the view of a batch, - * see `view_entry`. Effects are checked against their record in the active batch, if - * they have one (see `update_record`), or else their global state. + * see `view_entry`. Effects are checked against the run that applies (see `run_of`). * @param {Reaction} reaction * @returns {boolean} */ @@ -203,17 +213,9 @@ export function is_dirty(reaction) { } if ((flags & MAYBE_DIRTY) !== 0) { - var deps = reaction.deps; - var dv = reaction.dv; - - if ((flags & (BLOCK_EFFECT | ASYNC)) !== 0 && view_batch !== null) { - var record = view_batch.record(reaction); - - if (record !== undefined) { - deps = record.deps; - dv = record.dv; - } - } + var run = run_of(reaction); + var deps = run.deps; + var dv = run.dv; if (dv === null) return true; @@ -413,8 +415,8 @@ function update_dependencies(reaction, flags, value, error) { length === deps?.length && (flags & (BLOCK_EFFECT | ASYNC)) === 0 ) { - // A run saw everything up to its end — writes that happen during the run - // mark it dirty (unless it wrote them itself, which doesn't invalidate it) + // The run saw what its dependencies hold now — a write during the run after it read + // the value marks it dirty (see `Batch#schedule`), so it doesn't take effect here for (var i = 0; i < length; i++) dv[i] = deps[i].wv; if (dv.length !== length) dv.length = length; @@ -485,7 +487,7 @@ function update_record(reaction, flags, deps, length, value, error) { i < skipped_deps ? /** @type {Value[]} */ (deps)[i] : /** @type {Value[]} */ (new_deps)[i - skipped_deps]; - dv[i] = version_of(dep); + dv[i] = seen_version(dep); // Within a batch's view, a derived's run belongs to the batch unless everything it read is the latest // value: the version it saw is the real-world one, and — for a derived — that one is up to date @@ -578,6 +580,20 @@ function update_record(reaction, flags, deps, length, value, error) { return length; } +/** + * The version of `signal` that the current run saw. Recording it never evaluates anything: a derived that + * isn't in the view's cache (anymore) was invalidated by a write during the run, or can't be cached — + * the run then doesn't count as having seen its current version, just like in the real world, where + * a derived that changes during the run gets a new version only once it is evaluated again + * @param {Value} signal + */ +function seen_version(signal) { + if (batch_values === null) return signal.wv; + + var entry = batch_values.get(signal); + return entry !== undefined ? entry.wv : (signal.f & DERIVED) !== 0 ? -1 : signal.wv; +} + /** * The record of a run of `derived` that belongs to `batch`. Its value keeps the version of the same value * in the batch's view (if it held one before), or in the real world, so that reactions which saw it remain valid @@ -605,7 +621,7 @@ function derived_record(derived, batch, deps, dv, v, e) { /** * Make the record a batch holds of `reaction` its global state, e.g. because the batch - * committed, which makes its results the real-world ones + * committed, which makes its results the real-world ones (for a derived, that includes its value) * @param {Reaction} reaction * @param {BatchRecord} record */ @@ -628,6 +644,15 @@ export function promote_record(reaction, record) { } reaction.dv = record.dv; + + if ((reaction.f & DERIVED) !== 0) { + var derived = /** @type {Derived} */ (reaction); + + derived.v = record.v; + derived.wv = /** @type {number} */ (record.wv); + derived.f = record.e ? derived.f | ERROR_VALUE : derived.f & ~ERROR_VALUE; + update_derived_status(derived); + } } /** @@ -1012,10 +1037,9 @@ export function get(signal) { * @param {number} version */ function record_late_read(reaction, signal, version) { - var record = - (reaction.f & (BLOCK_EFFECT | ASYNC)) !== 0 ? view_batch?.record(reaction) : undefined; - var deps = record === undefined ? /** @type {Value[]} */ (reaction.deps) : (record.deps ??= []); - var dv = record === undefined ? reaction.dv : record.dv; + var run = run_of(reaction); + var deps = /** @type {Value[]} */ (run.deps); + var dv = run.dv; if (dv === null) return; diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-assigned-view/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-assigned-view/_config.js new file mode 100644 index 0000000000..35363de35d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-assigned-view/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// A value assigned to a derived holds until one of its dependencies changes — also in the +// view of a batch that is rendered while another batch is pending +export default test({ + async test({ assert, target }) { + const [other, assign, resolve] = target.querySelectorAll('button'); + + resolve.click(); + await tick(); + + // pending async work + other.click(); + await tick(); + + assign.click(); + await tick(); + + const [, p] = target.querySelectorAll('p'); + assert.equal(p.textContent, '4'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-assigned-view/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-assigned-view/main.svelte new file mode 100644 index 0000000000..b620952282 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-assigned-view/main.svelte @@ -0,0 +1,26 @@ + + + + + + + +

{await wait(other)}

+ {#snippet pending()}

loading

{/snippet} +
+ +

{d}