Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8c51290
fix: prevent reactivity loss during fork
hmnd Dec 9, 2025
5811f25
add samples
hmnd Dec 9, 2025
406d5be
add changeset
hmnd Dec 9, 2025
4ee4b75
Merge branch 'main' into fix/fork-lost-reactivity
paoloricciuti Dec 16, 2025
a40d79b
Merge branch 'main' into fix/fork-lost-reactivity
teemingc Jan 1, 2026
46df9a4
Merge branch 'main' into fix/fork-lost-reactivity
teemingc Jan 5, 2026
bb6b150
Merge branch 'main' into fix/fork-lost-reactivity
teemingc Jan 6, 2026
19baf10
fix var casing in tests
hmnd Jan 7, 2026
22ef246
Merge branch 'main' into fix/fork-lost-reactivity
hmnd Jan 12, 2026
180b043
Merge branch 'main' into fix/fork-lost-reactivity
hmnd Jan 15, 2026
10226e5
Merge branch 'main' into fix/fork-lost-reactivity
hmnd Jan 17, 2026
3943fcd
don't remove reactions during fork
hmnd Jan 19, 2026
b50fa06
add sample for derived dep tracking in fork
hmnd Jan 19, 2026
e066915
Merge branch 'main' into fix/fork-lost-reactivity
hmnd Jan 19, 2026
ebbad63
fix sample type check error
hmnd Jan 19, 2026
b9cdbc1
set derived.v on first eval in fork
hmnd Jan 20, 2026
aa647c7
add sample for derived.v remaining UNINITIALIZED
hmnd Jan 20, 2026
098dc55
Merge branch 'main' into fix/fork-lost-reactivity
hmnd Jan 20, 2026
54171d8
Merge branch 'main' into fix/fork-lost-reactivity
hmnd Jan 25, 2026
4078d8b
Merge branch 'main' into fix/fork-lost-reactivity
hmnd Jan 25, 2026
09d42a2
lost current_batch import in runtime.js
hmnd Jan 25, 2026
40d77a1
Merge branch 'main' into fix/fork-lost-reactivity
Rich-Harris Jan 28, 2026
3591440
Delete how
Rich-Harris Jan 28, 2026
140f838
Update packages/svelte/src/internal/client/reactivity/deriveds.js
Rich-Harris Jan 28, 2026
9d1ca05
delete test in favour of #17577
Rich-Harris Jan 28, 2026
ab021bd
Merge branch 'fix/fork-lost-reactivity' of https://github.com/hmnd/sv…
Rich-Harris Jan 28, 2026
4e913de
Merge branch 'main' into fix/fork-lost-reactivity
Rich-Harris Jan 28, 2026
96d62c0
Merge branch 'main' into pr/17335
Rich-Harris Jan 28, 2026
bb658a1
extract runtime.js changes into separate PR
Rich-Harris Jan 28, 2026
dbaa7c4
Merge branch 'fix/fork-lost-reactivity' of https://github.com/hmnd/sv…
Rich-Harris Jan 28, 2026
1f7975c
Merge branch 'main' into fix/fork-lost-reactivity
Rich-Harris Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/common-boats-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: prevent reactivity loss during fork
24 changes: 23 additions & 1 deletion packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export class Batch {
*/
#blocking_pending = 0;

#decrement_queued = false;

/**
* A deferred that resolves when the batch is committed, used with `settled()`
* TODO replace with Promise.withResolvers once supported widely enough
Expand Down Expand Up @@ -138,7 +140,11 @@ export class Batch {

is_fork = false;

#decrement_queued = false;
/**
* Branches that had their CLEAN flag toggled during fork execution
* @type {Set<Effect> | null}
*/
toggled_branches = null;

is_deferred() {
return this.is_fork || this.#blocking_pending > 0;
Expand Down Expand Up @@ -816,6 +822,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);
}
Comment on lines +826 to +830
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense...there's only one thing that it kinda irks me here: we are restoring ALL the branches to CLEAN but wouldn't this lead to over-running?

Copy link
Contributor Author

@hmnd hmnd Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I don't think that's a concern because we only do this for branches that had CLEAN before the fork. I'm pretty sure setting CLEAN doesn't necessarily mean branches will run either, it just allows them to be scheduled.

}
}

Expand Down Expand Up @@ -918,6 +930,16 @@ 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
for (var [source, value] of batch.previous) {
source.v = value;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let count = $state(0);
</script>

<button onclick={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
skip_no_async: true,
async test({ assert, target }) {
const [fork_btn, counter_btn] = target.querySelectorAll('button');

flushSync(() => {
fork_btn.click();
});

assert.equal(counter_btn.textContent, '0');

flushSync(() => {
counter_btn.click();
});

assert.equal(counter_btn.textContent, '1');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script>
import { fork } from 'svelte';
import Child from './Child.svelte';

let show = $state(false);
</script>

<button onclick={() => {
let f = fork(() => {
show = !show;
});
f.discard();
}}>fork</button>

{#if show}
hi
{:else}
{#if show || !show}
<Child />
{/if}
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let count = $state(0);
</script>

<button onclick={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
skip_no_async: true,
async test({ assert, target }) {
const [fork_btn, counter_btn] = target.querySelectorAll('button');

flushSync(() => {
fork_btn.click();
});

assert.equal(counter_btn.textContent, '0');

flushSync(() => {
counter_btn.click();
});

assert.equal(counter_btn.textContent, '1');

flushSync(() => {
counter_btn.click();
});

assert.equal(counter_btn.textContent, '2');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script>
import { fork } from 'svelte';
import Child from './Child.svelte';

let show = $state(false);
let pendingFork = $state(null);
</script>

<button onclick={() => {
// Create fork but don't discard to simulate SvelteKit preload
pendingFork = fork(() => {
show = !show;
});
}}>fork</button>

{#if show}
hi
{:else}
{#if show || !show}
<Child />
{/if}
{/if}
Loading