From fea81ad68a3f6903f38257a6104d6b2be7828a64 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Fri, 21 Nov 2025 15:07:05 +0100 Subject: [PATCH 1/2] fix: store forked derived values --- .changeset/strong-berries-fry.md | 5 +++++ .../src/internal/client/reactivity/batch.js | 12 ++++++++++++ .../src/internal/client/reactivity/deriveds.js | 6 ++++-- packages/svelte/src/internal/client/runtime.js | 12 +++++++++++- .../fork-derived-value-immediate/_config.js | 15 +++++++++++++++ .../fork-derived-value-immediate/main.svelte | 13 +++++++++++++ 6 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 .changeset/strong-berries-fry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte diff --git a/.changeset/strong-berries-fry.md b/.changeset/strong-berries-fry.md new file mode 100644 index 000000000000..60dbb290a854 --- /dev/null +++ b/.changeset/strong-berries-fry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: store forked derived values diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 22526df7c1f2..0c2ba81f0c91 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -68,6 +68,14 @@ export let previous_batch = null; */ export let batch_values = null; +/** + * When time travelling (i.e. working in one batch, while other batches + * still have ongoing work), we ignore the real values of affected + * signals in favour of their values within the batch + * @type {Map | null} + */ +export let forked_derived_values = null; + // TODO this should really be a property of `batch` /** @type {Effect[]} */ let queued_root_effects = []; @@ -962,8 +970,12 @@ export function fork(fn) { var committed = false; var settled = batch.settled(); + forked_derived_values = new Map(); + flushSync(fn); + forked_derived_values = null; + // revert state changes for (var [source, value] of batch.previous) { source.v = value; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 39e02be7649e..693a86fcf07a 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -34,7 +34,7 @@ import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { batch_values, current_batch } from './batch.js'; +import { batch_values, current_batch, forked_derived_values } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; @@ -360,8 +360,10 @@ export function update_derived(derived) { // the underlying value will be updated when the fork is committed. // otherwise, the next time we get here after a 'real world' state // change, `derived.equals` may incorrectly return `true` - if (!current_batch?.is_fork) { + if (!forked_derived_values) { derived.v = value; + } else { + forked_derived_values.set(derived, value); } derived.wv = increment_write_version(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5ece0d79b6b8..4e53b417283c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -43,7 +43,13 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js'; +import { + Batch, + batch_values, + flushSync, + forked_derived_values, + schedule_effect +} from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; @@ -621,6 +627,10 @@ export function get(signal) { if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { reconnect(derived); } + + if (forked_derived_values?.has(derived)) { + return forked_derived_values.get(derived); + } } if (batch_values?.has(signal)) { diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js new file mode 100644 index 000000000000..52478687e50e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target, logs }) { + const fork = target.querySelector('button'); + + flushSync(() => { + fork?.click(); + }); + + assert.deepEqual(logs, [1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte new file mode 100644 index 000000000000..fe3fa26195ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-value-immediate/main.svelte @@ -0,0 +1,13 @@ + + + From 5f058844834039ca340db7b21f0da8b3d5866572 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Fri, 21 Nov 2025 17:09:39 +0100 Subject: [PATCH 2/2] fix: reuse `batch_values` --- .../svelte/src/internal/client/reactivity/batch.js | 13 ++----------- .../src/internal/client/reactivity/deriveds.js | 8 +++----- packages/svelte/src/internal/client/runtime.js | 12 +----------- 3 files changed, 6 insertions(+), 27 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0c2ba81f0c91..8f63922ab26e 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -68,14 +68,6 @@ export let previous_batch = null; */ export let batch_values = null; -/** - * When time travelling (i.e. working in one batch, while other batches - * still have ongoing work), we ignore the real values of affected - * signals in favour of their values within the batch - * @type {Map | null} - */ -export let forked_derived_values = null; - // TODO this should really be a property of `batch` /** @type {Effect[]} */ let queued_root_effects = []; @@ -966,15 +958,14 @@ export function fork(fn) { var batch = Batch.ensure(); batch.is_fork = true; + batch_values = new Map(); var committed = false; var settled = batch.settled(); - forked_derived_values = new Map(); - flushSync(fn); - forked_derived_values = null; + batch_values = null; // revert state changes for (var [source, value] of batch.previous) { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 693a86fcf07a..3bf38bf0b2a4 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -34,7 +34,7 @@ import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { batch_values, current_batch, forked_derived_values } from './batch.js'; +import { batch_values, current_batch } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; @@ -360,10 +360,8 @@ export function update_derived(derived) { // the underlying value will be updated when the fork is committed. // otherwise, the next time we get here after a 'real world' state // change, `derived.equals` may incorrectly return `true` - if (!forked_derived_values) { + if (!current_batch?.is_fork) { derived.v = value; - } else { - forked_derived_values.set(derived, value); } derived.wv = increment_write_version(); @@ -380,7 +378,7 @@ export function update_derived(derived) { if (batch_values !== null) { // 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()) { + if (effect_tracking() || current_batch?.is_fork) { batch_values.set(derived, value); } } else { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4e53b417283c..5ece0d79b6b8 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -43,13 +43,7 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { - Batch, - batch_values, - flushSync, - forked_derived_values, - schedule_effect -} from './reactivity/batch.js'; +import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; @@ -627,10 +621,6 @@ export function get(signal) { if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { reconnect(derived); } - - if (forked_derived_values?.has(derived)) { - return forked_derived_values.get(derived); - } } if (batch_values?.has(signal)) {