Skip to content

Commit b268ccb

Browse files
authored
fix: correctly reschedule deferred effects when reviving a batch after async work (#17332)
* WIP * test * fix: correctly reschedule deferred effects when reviving a batch after async work
1 parent 4e6104a commit b268ccb

File tree

5 files changed

+71
-19
lines changed

5 files changed

+71
-19
lines changed

.changeset/lucky-wasps-grab.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: correctly reschedule deferred effects when reviving a batch after async work

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

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import { eager_effect, unlink_effect } from './effects.js';
4444
* effect: Effect | null;
4545
* effects: Effect[];
4646
* render_effects: Effect[];
47-
* block_effects: Effect[];
4847
* }} EffectTarget
4948
*/
5049

@@ -128,15 +127,15 @@ export class Batch {
128127

129128
/**
130129
* Deferred effects (which run after async work has completed) that are DIRTY
131-
* @type {Effect[]}
130+
* @type {Set<Effect>}
132131
*/
133-
#dirty_effects = [];
132+
#dirty_effects = new Set();
134133

135134
/**
136135
* Deferred effects that are MAYBE_DIRTY
137-
* @type {Effect[]}
136+
* @type {Set<Effect>}
138137
*/
139-
#maybe_dirty_effects = [];
138+
#maybe_dirty_effects = new Set();
140139

141140
/**
142141
* A set of branches that still exist, but will be destroyed when this batch
@@ -167,8 +166,7 @@ export class Batch {
167166
parent: null,
168167
effect: null,
169168
effects: [],
170-
render_effects: [],
171-
block_effects: []
169+
render_effects: []
172170
};
173171

174172
for (const root of root_effects) {
@@ -187,7 +185,6 @@ export class Batch {
187185
if (this.is_deferred()) {
188186
this.#defer_effects(target.effects);
189187
this.#defer_effects(target.render_effects);
190-
this.#defer_effects(target.block_effects);
191188
} else {
192189
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
193190
// newly updated sources, which could lead to infinite loops when effects run over and over again.
@@ -228,8 +225,7 @@ export class Batch {
228225
parent: target,
229226
effect,
230227
effects: [],
231-
render_effects: [],
232-
block_effects: []
228+
render_effects: []
233229
};
234230
}
235231

@@ -241,7 +237,7 @@ export class Batch {
241237
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
242238
target.render_effects.push(effect);
243239
} else if (is_dirty(effect)) {
244-
if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect);
240+
if ((effect.f & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect);
245241
update_effect(effect);
246242
}
247243

@@ -263,7 +259,6 @@ export class Batch {
263259
// once the boundary is ready?
264260
this.#defer_effects(target.effects);
265261
this.#defer_effects(target.render_effects);
266-
this.#defer_effects(target.block_effects);
267262

268263
target = /** @type {EffectTarget} */ (target.parent);
269264
}
@@ -279,8 +274,11 @@ export class Batch {
279274
*/
280275
#defer_effects(effects) {
281276
for (const e of effects) {
282-
const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects;
283-
target.push(e);
277+
if ((e.f & DIRTY) !== 0) {
278+
this.#dirty_effects.add(e);
279+
} else if ((e.f & MAYBE_DIRTY) !== 0) {
280+
this.#maybe_dirty_effects.add(e);
281+
}
284282

285283
// Since we're not executing these effects now, we need to clear any WAS_MARKED flags
286284
// so that other batches can correctly reach these effects during their own traversal
@@ -390,8 +388,7 @@ export class Batch {
390388
parent: null,
391389
effect: null,
392390
effects: [],
393-
render_effects: [],
394-
block_effects: []
391+
render_effects: []
395392
};
396393

397394
for (const batch of batches) {
@@ -484,6 +481,7 @@ export class Batch {
484481

485482
revive() {
486483
for (const e of this.#dirty_effects) {
484+
this.#maybe_dirty_effects.delete(e);
487485
set_signal_status(e, DIRTY);
488486
schedule_effect(e);
489487
}
@@ -493,9 +491,6 @@ export class Batch {
493491
schedule_effect(e);
494492
}
495493

496-
this.#dirty_effects = [];
497-
this.#maybe_dirty_effects = [];
498-
499494
this.flush();
500495
}
501496

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
$effect(() => {})
3+
</script>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [a, b, resolve] = target.querySelectorAll('button');
7+
8+
a.click();
9+
await tick();
10+
11+
b.click();
12+
await tick();
13+
14+
resolve.click();
15+
await tick();
16+
assert.htmlEqual(
17+
target.innerHTML,
18+
`
19+
<button>a (true)</button>
20+
<button>b (true)</button>
21+
<button>resolve</button>
22+
42
23+
`
24+
);
25+
}
26+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script>
2+
import Child from './Child.svelte';
3+
4+
let a = $state(false);
5+
let b = $state(false);
6+
7+
let deferred = [];
8+
9+
function push(value) {
10+
const d = Promise.withResolvers();
11+
deferred.push(() => d.resolve(value))
12+
return d.promise;
13+
}
14+
</script>
15+
16+
<button onclick={() => a = !a}>a ({a})</button>
17+
<button onclick={() => b = !b}>b ({b})</button>
18+
<button onclick={() => deferred.shift()()}>resolve</button>
19+
20+
{#if a}
21+
{await push(42)}
22+
<Child />
23+
{/if}

0 commit comments

Comments
 (0)