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 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 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}