From 8c5129088064a6dedcd371e4c0832cdda245bd38 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Tue, 9 Dec 2025 01:21:28 -0800 Subject: [PATCH 1/3] fix: prevent reactivity loss during fork fixes #17197, fixes #17304, fixes #17301, fixes #17309 --- .../src/internal/client/reactivity/batch.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 24dee5141960..bcaa9b002ff1 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -147,6 +147,12 @@ export class Batch { is_fork = false; + /** + * Branches that had their CLEAN flag toggled during fork execution + * @type {Set | null} + */ + toggled_branches = null; + is_deferred() { return this.is_fork || this.#blocking_pending > 0; } @@ -862,6 +868,12 @@ export function schedule_effect(signal) { if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { if ((flags & CLEAN) === 0) return; effect.f ^= CLEAN; + + // Track branches toggled during fork execution so we can restore + // their CLEAN flag after flush + if (current_batch !== null && current_batch.is_fork) { + (current_batch.toggled_branches ??= new Set()).add(effect); + } } } @@ -964,6 +976,14 @@ export function fork(fn) { flushSync(fn); + // Restore CLEAN flags that were toggled during fork initialization. + if (batch.toggled_branches !== null) { + for (const effect of batch.toggled_branches) { + effect.f |= CLEAN; + } + batch.toggled_branches = null; + } + batch_values = null; // revert state changes From 5811f25b2090e4ad9ed3a7bc141baf4514a5ba8d Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Tue, 9 Dec 2025 01:21:28 -0800 Subject: [PATCH 2/3] add samples --- .../fork-discard-reactivity/Child.svelte | 5 ++++ .../fork-discard-reactivity/_config.js | 21 +++++++++++++++ .../fork-discard-reactivity/main.svelte | 21 +++++++++++++++ .../fork-pending-reactivity/Child.svelte | 5 ++++ .../fork-pending-reactivity/_config.js | 27 +++++++++++++++++++ .../fork-pending-reactivity/main.svelte | 22 +++++++++++++++ 6 files changed, 101 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/Child.svelte b/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/Child.svelte new file mode 100644 index 000000000000..d8b0f76b40bb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/Child.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/_config.js new file mode 100644 index 000000000000..622f5e52612e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [forkBtn, counterBtn] = target.querySelectorAll('button'); + + flushSync(() => { + forkBtn.click(); + }); + + assert.equal(counterBtn.textContent, '0'); + + flushSync(() => { + counterBtn.click(); + }); + + assert.equal(counterBtn.textContent, '1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/main.svelte new file mode 100644 index 000000000000..6310bd66d5ff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-discard-reactivity/main.svelte @@ -0,0 +1,21 @@ + + + + +{#if show} + hi +{:else} + {#if show || !show} + + {/if} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/Child.svelte b/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/Child.svelte new file mode 100644 index 000000000000..d8b0f76b40bb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/Child.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/_config.js new file mode 100644 index 000000000000..090602be2fdc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [forkBtn, counterBtn] = target.querySelectorAll('button'); + + flushSync(() => { + forkBtn.click(); + }); + + assert.equal(counterBtn.textContent, '0'); + + flushSync(() => { + counterBtn.click(); + }); + + assert.equal(counterBtn.textContent, '1'); + + flushSync(() => { + counterBtn.click(); + }); + + assert.equal(counterBtn.textContent, '2'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/main.svelte new file mode 100644 index 000000000000..4199f2d3a01e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-pending-reactivity/main.svelte @@ -0,0 +1,22 @@ + + + + +{#if show} + hi +{:else} + {#if show || !show} + + {/if} +{/if} From 406d5be1ac2e137eb4f8bf9f5a54efaab9a0a4c2 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Tue, 9 Dec 2025 02:23:34 -0800 Subject: [PATCH 3/3] add changeset --- .changeset/common-boats-travel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/common-boats-travel.md diff --git a/.changeset/common-boats-travel.md b/.changeset/common-boats-travel.md new file mode 100644 index 000000000000..a5ed8639a914 --- /dev/null +++ b/.changeset/common-boats-travel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent reactivity loss during fork