From c93e251654a3193f294d6a4171d4087b4cb2fb80 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 2 Apr 2026 08:42:41 -0400 Subject: [PATCH] fix: never set derived.v inside fork (#18037) This started out as me implementing https://github.com/sveltejs/svelte/pull/17998/changes#r3018047965, but then I realised that I'd also fixed the bug that #17998 addresses. So I guess it's an alternative to that PR --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/poor-tips-send.md | 5 +++++ .../src/internal/client/reactivity/batch.js | 21 +++++++++--------- .../internal/client/reactivity/deriveds.js | 8 ++++--- .../src/internal/client/reactivity/sources.js | 12 ++-------- .../async-fork-resolves-promise/_config.js | 22 +++++++++++++++++++ .../async-fork-resolves-promise/main.svelte | 21 ++++++++++++++++++ 6 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 .changeset/poor-tips-send.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/main.svelte diff --git a/.changeset/poor-tips-send.md b/.changeset/poor-tips-send.md new file mode 100644 index 0000000000..beb0329705 --- /dev/null +++ b/.changeset/poor-tips-send.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: never set derived.v inside fork diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d53d824d03..23cc73ffc8 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -419,18 +419,22 @@ export class Batch { * Associate a change to a given source with the current * batch, noting its previous and current values * @param {Value} source - * @param {any} old_value + * @param {any} value * @param {boolean} [is_derived] */ - capture(source, old_value, is_derived = false) { - if (old_value !== UNINITIALIZED && !this.previous.has(source)) { - this.previous.set(source, old_value); + capture(source, value, is_derived = false) { + if (source.v !== UNINITIALIZED && !this.previous.has(source)) { + this.previous.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, is_derived]); - batch_values?.set(source, source.v); + this.current.set(source, [value, is_derived]); + batch_values?.set(source, value); + } + + if (!this.is_fork) { + source.v = value; } } @@ -1162,11 +1166,6 @@ export function fork(fn) { flushSync(fn); - // revert state changes - for (var [source, value] of batch.previous) { - source.v = value; - } - return { commit: async () => { if (committed) { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5da0df0670..77acb23516 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -384,7 +384,6 @@ export function execute_derived(derived) { * @returns {void} */ export function update_derived(derived) { - var old_value = derived.v; var value = execute_derived(derived); if (!derived.equals(value)) { @@ -395,8 +394,11 @@ export function update_derived(derived) { // otherwise, the next time we get here after a 'real world' state // change, `derived.equals` may incorrectly return `true` if (!current_batch?.is_fork || derived.deps === null) { - derived.v = value; - current_batch?.capture(derived, old_value, true); + if (current_batch !== null) { + current_batch.capture(derived, value, true); + } else { + derived.v = value; + } // deriveds without dependencies should never be recomputed if (derived.deps === null) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3ccde0f211..9235c5f673 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -180,18 +180,10 @@ export function set(source, value, should_proxy = false) { */ export function internal_set(source, value, updated_during_traversal = null) { if (!source.equals(value)) { - var old_value = source.v; - - if (is_destroying_effect) { - old_values.set(source, value); - } else { - old_values.set(source, old_value); - } - - source.v = value; + old_values.set(source, is_destroying_effect ? value : source.v); var batch = Batch.ensure(); - batch.capture(source, old_value); + batch.capture(source, value); if (DEV) { if (tracing_mode_flag || active_effect !== null) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/_config.js new file mode 100644 index 0000000000..ff323b60af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/_config.js @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [x, y, resolve, commit] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + y.click(); + await tick(); + resolve.click(); + await tick(); + x.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '1 0'); + + await tick(); + commit.click(); + assert.htmlEqual(p.innerHTML, '1 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/main.svelte new file mode 100644 index 0000000000..d6995340af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-resolves-promise/main.svelte @@ -0,0 +1,21 @@ + + +

{x} {await delay(y)}

+ + + + +