From 08a17b9839f198883c52954060398f91ee56f7d6 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 24 Sep 2026 01:16:28 +0200 Subject: [PATCH] another fork fix --- .../src/internal/client/reactivity/batch.js | 42 +++------------- .../_config.js | 48 +++++++++++++++++++ .../main.svelte | 29 +++++++++++ 3 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-chain/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-chain/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9b4d9b4d2d..147f7378af 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -21,7 +21,7 @@ import { FORK_ONLY_BRANCH } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; -import { deferred, define_property, includes } from '../../shared/utils.js'; +import { deferred, define_property } from '../../shared/utils.js'; import { active_reaction, get, @@ -911,11 +911,10 @@ export class Batch { !is_derived && (!current || current.v !== value) && ((source.f & ASYNC) === 0 || - !depends_on( - /** @type {Effect} */ (/** @type {Source} */ (source).e), - [...batch.current.keys()].filter((s) => !this.current.has(s)), - new Map() - )) + // If the fork ran an async effect, its pending/resolved result belongs to the + // fork. Revalidate it when its inputs change, not when another batch resolves + // the same expression with a different view of those inputs. + !batch.#stale_effects?.has(/** @type {Effect} */ (/** @type {Source} */ (source).e))) ) { batch.current.delete(source); batch.queue_revalidation(source); @@ -1387,33 +1386,6 @@ function mark_eager_effects(value, effects) { } } -/** - * @param {Reaction} reaction - * @param {Value[]} sources - * @param {Map} checked - */ -function depends_on(reaction, sources, checked) { - const depends = checked.get(reaction); - if (depends !== undefined) return depends; - - if (reaction.deps !== null) { - for (const dep of reaction.deps) { - if (includes.call(sources, dep)) { - return true; - } - - if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources, checked)) { - checked.set(/** @type {Derived} */ (dep), true); - return true; - } - } - } - - checked.set(reaction, false); - - return false; -} - /** * @param {Effect} effect * @returns {void} @@ -1623,8 +1595,8 @@ export function fork(fn) { // Apply changes and update write versions so deriveds see the change. Everything still // in `batch.current` at this point is the latest value: sources that the real world has - // written to in the meantime were removed from the fork via `notify_fork` (an async - // source only survives if its effect depends on inputs that only the fork changed). + // written to in the meantime were removed from the fork via `notify_fork`, while + // async results are kept up to date by revalidating their producers when inputs change. // We use fresh versions rather than the fork-time `content.wv`, because the real world // may have run reactions since then whose versions would otherwise outrank them. for (var [source, content] of batch.current) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-chain/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-chain/_config.js new file mode 100644 index 0000000000..7dc50351cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-chain/_config.js @@ -0,0 +1,48 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [fork, update, pop, commit] = target.querySelectorAll('button'); + const [sum, doubled] = target.querySelectorAll('p'); + logs.length = 0; + + fork.click(); + await tick(); + assert.deepEqual(logs, ['sum 1,0']); + logs.length = 0; + + // Revalidate twice to also check that retaining async results does not + // prevent the fork from responding to genuine changes to its inputs. + for (const y of [1, 2]) { + update.click(); + await tick(); + assert.deepEqual(logs, [`sum 0,${y}`, `sum 1,${y}`]); + logs.length = 0; + + pop.click(); // resolve the fork before the real world + await tick(); + assert.deepEqual(logs, [`double ${y + 1}`]); + logs.length = 0; + + pop.click(); + await tick(); + assert.deepEqual(logs, [`double ${y}`]); + assert.htmlEqual(sum.innerHTML, String(y)); + assert.htmlEqual(doubled.innerHTML, String(y * 2)); + logs.length = 0; + } + + commit.click(); + await tick(); + assert.deepEqual(logs, []); + assert.htmlEqual(sum.innerHTML, '3'); + assert.htmlEqual(doubled.innerHTML, '6'); + + pop.click(); // the superseded first fork run + await tick(); + assert.htmlEqual(sum.innerHTML, '3'); + assert.htmlEqual(doubled.innerHTML, '6'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-chain/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-chain/main.svelte new file mode 100644 index 0000000000..a682f9e170 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-effect-overlap-chain/main.svelte @@ -0,0 +1,29 @@ + + + + + + + +

{sum}

+

{await double(sum)}