From ece2e83eb9d0e2a97d7db50d41f68ce2edfb697d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 28 Jan 2026 16:09:32 -0500 Subject: [PATCH] fix: increment signal versions when discarding forks (#17577) * fix: always update an UNINITIALIZED derived on read Co-authored-by: David Roizenman * obsolete comments * rename test * add tests, fix * revert * Update .changeset/vast-hornets-draw.md --------- Co-authored-by: David Roizenman --- .changeset/vast-hornets-draw.md | 5 +++ .../src/internal/client/reactivity/batch.js | 7 ++++ .../fork-derived-uncached-1/_config.js | 40 +++++++++++++++++++ .../fork-derived-uncached-1/main.svelte | 23 +++++++++++ .../fork-derived-uncached-2/_config.js | 38 ++++++++++++++++++ .../fork-derived-uncached-2/main.svelte | 23 +++++++++++ .../fork-derived-uncached-3/_config.js | 32 +++++++++++++++ .../fork-derived-uncached-3/main.svelte | 23 +++++++++++ 8 files changed, 191 insertions(+) create mode 100644 .changeset/vast-hornets-draw.md create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/main.svelte diff --git a/.changeset/vast-hornets-draw.md b/.changeset/vast-hornets-draw.md new file mode 100644 index 0000000000..256439e9da --- /dev/null +++ b/.changeset/vast-hornets-draw.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: increment signal versions when discarding forks diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 134ebe53a2..d82f965541 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -972,6 +972,13 @@ export function fork(fn) { await settled; }, discard: () => { + // cause any MAYBE_DIRTY deriveds to update + // if they depend on things thath changed + // inside the discarded fork + for (var source of batch.current.keys()) { + source.wv = increment_write_version(); + } + if (!committed && batches.has(batch)) { batches.delete(batch); batch.discard(); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/_config.js new file mode 100644 index 0000000000..fb50ceb9b1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/_config.js @@ -0,0 +1,40 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork, toggle, increment] = target.querySelectorAll('button'); + + // derived is first evaluated in block effect, then discarded + flushSync(() => fork.click()); + + // should not throw "Cannot convert a Symbol value to a string" due to cached UNINITIALIZED from first fork + flushSync(() => fork.click()); + + // should not reflect the temporary change to `clicks` inside the fork + flushSync(() => toggle.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

0

+ ` + ); + + flushSync(() => increment.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/main.svelte new file mode 100644 index 0000000000..08ebaba25a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-1/main.svelte @@ -0,0 +1,23 @@ + + + + + + + + +{#if show} +

{derived}

+{/if} + diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/_config.js new file mode 100644 index 0000000000..21ab140fc0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/_config.js @@ -0,0 +1,38 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork, toggle, increment] = target.querySelectorAll('button'); + + // derived is first evaluated in block effect, then discarded + flushSync(() => fork.click()); + + // should not reflect the temporary change to `clicks` inside the fork + // or throw "Cannot convert a Symbol value to a string" due to cached UNINITIALIZED + flushSync(() => toggle.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

0

+ ` + ); + + flushSync(() => increment.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/main.svelte new file mode 100644 index 0000000000..08ebaba25a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-2/main.svelte @@ -0,0 +1,23 @@ + + + + + + + + +{#if show} +

{derived}

+{/if} + diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/_config.js new file mode 100644 index 0000000000..4827aa99f3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork, toggle, increment] = target.querySelectorAll('button'); + + // initialize derived by showing it + flushSync(() => toggle.click()); + flushSync(() => toggle.click()); + + // increment clicks + flushSync(() => increment.click()); + + // update derived, but without writing to `derived.v` + flushSync(() => fork.click()); + + // show derived + flushSync(() => toggle.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/main.svelte new file mode 100644 index 0000000000..08ebaba25a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-uncached-3/main.svelte @@ -0,0 +1,23 @@ + + + + + + + + +{#if show} +

{derived}

+{/if} +