Skip to content

Commit 6e571dc

Browse files
authored
fix: ensure deferred effects can be rescheduled later on (#17147)
When deferring effects we didn't unmark the deriveds that lead to those effects. This means that they might not be reached in subsequent runs of `mark_reactions`. Fixes #17118 (comment)
1 parent a7a7d1e commit 6e571dc

File tree

4 files changed

+114
-1
lines changed

4 files changed

+114
-1
lines changed

.changeset/grumpy-gifts-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: ensure deferred effects can be rescheduled later on

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
BOUNDARY_EFFECT,
1717
EAGER_EFFECT,
1818
HEAD_EFFECT,
19-
ERROR_VALUE
19+
ERROR_VALUE,
20+
WAS_MARKED
2021
} from '#client/constants';
2122
import { async_mode_flag } from '../../flags/index.js';
2223
import { deferred, define_property } from '../../shared/utils.js';
@@ -274,11 +275,32 @@ export class Batch {
274275
const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects;
275276
target.push(e);
276277

278+
// Since we're not executing these effects now, we need to clear any WAS_MARKED flags
279+
// so that other batches can correctly reach these effects during their own traversal
280+
this.#clear_marked(e.deps);
281+
277282
// mark as clean so they get scheduled if they depend on pending async state
278283
set_signal_status(e, CLEAN);
279284
}
280285
}
281286

287+
/**
288+
* @param {Value[] | null} deps
289+
*/
290+
#clear_marked(deps) {
291+
if (deps === null) return;
292+
293+
for (const dep of deps) {
294+
if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) {
295+
continue;
296+
}
297+
298+
dep.f ^= WAS_MARKED;
299+
300+
this.#clear_marked(/** @type {Derived} */ (dep).deps);
301+
}
302+
}
303+
282304
/**
283305
* Associate a change to a given source with the current
284306
* batch, noting its previous and current values
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
assert.deepEqual(logs, [0]);
7+
8+
const [fork1, fork2, commit] = target.querySelectorAll('button');
9+
10+
fork1.click();
11+
await tick();
12+
assert.htmlEqual(
13+
target.innerHTML,
14+
`
15+
<button>fork 1</button>
16+
<button>fork 2</button>
17+
<button>commit</button>
18+
<p>0</p>
19+
`
20+
);
21+
assert.deepEqual(logs, [0]);
22+
23+
fork2.click();
24+
await tick();
25+
assert.htmlEqual(
26+
target.innerHTML,
27+
`
28+
<button>fork 1</button>
29+
<button>fork 2</button>
30+
<button>commit</button>
31+
<p>0</p>
32+
`
33+
);
34+
assert.deepEqual(logs, [0]);
35+
36+
commit.click();
37+
await tick();
38+
assert.htmlEqual(
39+
target.innerHTML,
40+
`
41+
<button>fork 1</button>
42+
<button>fork 2</button>
43+
<button>commit</button>
44+
<p>1</p>
45+
`
46+
);
47+
assert.deepEqual(logs, [0, 1]);
48+
}
49+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script>
2+
import { fork } from "svelte";
3+
4+
let state = $state(0);
5+
6+
let count = $derived(state);
7+
8+
$effect.pre(() => {
9+
console.log(count);
10+
});
11+
12+
let forked;
13+
</script>
14+
15+
<button onclick={()=>{
16+
forked?.discard?.();
17+
forked = fork(()=>{
18+
state++;
19+
});
20+
}}>
21+
fork 1
22+
</button>
23+
24+
<button onclick={()=>{
25+
forked?.discard?.();
26+
forked = fork(()=>{
27+
state++;
28+
})
29+
}}>
30+
fork 2
31+
</button>
32+
33+
<button onclick={()=>{
34+
forked?.commit();
35+
}}>commit</button>
36+
37+
<p>{count}</p>

0 commit comments

Comments
 (0)