From 70bb7f42959a7cc8381fbe81e4b0d5f667630a8f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Oct 2026 12:07:21 -0400 Subject: [PATCH] restore capturing deriveds in previous_batch before current_batch The committing batch needs the correct previous value, and the new batch must record that its value came from the committing batch's world. --- .../internal/client/reactivity/deriveds.js | 2 +- .../_config.js | 33 ++++++++++++++++ .../main.svelte | 38 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 2ae137c238..1692762ae4 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -421,8 +421,8 @@ export function update_derived(derived) { // to the previous batch, not the new one (which can already exist if an earlier // effect wrote to a source). Not adding it to either can cause bugs, so we add it to both. // See https://github.com/sveltejs/svelte/pull/18117 for more details. - current_batch?.capture(derived, value, true); previous_batch?.capture(derived, value, true); + current_batch?.capture(derived, value, true); } else { derived.v = value; derived.wv = increment_write_version(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/_config.js new file mode 100644 index 0000000000..8f021032e0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Batch B (x) commits while an earlier batch A (y) is still pending, but B stays around because +// of the async work in the new boundary. While B flushes its effects, an effect creates a new batch C, +// and `d` is computed for the first time afterwards (so it's captured in both B and C). When A +// commits later, it sees B's previous values, which must be consistent (`d` must be `x * 2`). +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [y, x, shift] = target.querySelectorAll('button'); + assert.deepEqual(logs, [0, 0, 0]); + + // A + y.click(); + await tick(); + + // B + x.click(); + await tick(); + assert.deepEqual(logs, [0, 0, 0, 0, 1, 2]); + + // A commits + shift.click(); + await tick(); + + for (let i = 0; i < logs.length; i += 3) { + assert.equal(logs[i + 2], logs[i + 1] * 2, `torn read: ${logs.slice(i, i + 3)}`); + } + + assert.deepEqual(logs.slice(-3), [1, 1, 2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/main.svelte new file mode 100644 index 0000000000..5cd0876884 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-previous-effect-batch/main.svelte @@ -0,0 +1,38 @@ + + + + + + +

{y ? await push(y) : y}

+ +{#if x} + +

{await push(x)}

+ {#snippet pending()} +

loading

+ {/snippet} +
+{/if}