Skip to content

Commit e01bd97

Browse files
authored
fix: update $effect.pending() immediately after a batch is removed (#16382)
* WIP sync effect pending updates * fix * changeset * fix * add test * inline * unused
1 parent 3fa3dd7 commit e01bd97

File tree

5 files changed

+142
-7
lines changed

5 files changed

+142
-7
lines changed

.changeset/popular-tips-lie.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: update `$effect.pending()` immediately after a batch is removed

packages/svelte/src/internal/client/dom/blocks/boundary.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { get_next_sibling } from '../operations.js';
2222
import { queue_micro_task } from '../task.js';
2323
import * as e from '../../errors.js';
2424
import { DEV } from 'esm-env';
25-
import { Batch } from '../../reactivity/batch.js';
25+
import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
2626
import { internal_set, source } from '../../reactivity/sources.js';
2727
import { tag } from '../../dev/tracing.js';
2828
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@@ -92,6 +92,12 @@ export class Boundary {
9292
*/
9393
#effect_pending = null;
9494

95+
#effect_pending_update = () => {
96+
if (this.#effect_pending) {
97+
internal_set(this.#effect_pending, this.#pending_count);
98+
}
99+
};
100+
95101
#effect_pending_subscriber = createSubscriber(() => {
96102
this.#effect_pending = source(this.#pending_count);
97103

@@ -238,11 +244,7 @@ export class Boundary {
238244
this.parent.#update_pending_count(d);
239245
}
240246

241-
queueMicrotask(() => {
242-
if (this.#effect_pending) {
243-
internal_set(this.#effect_pending, this.#pending_count);
244-
}
245-
});
247+
effect_pending_updates.add(this.#effect_pending_update);
246248
}
247249

248250
get_effect_pending() {

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export let batch_deriveds = null;
4949
/** @type {Effect[]} Stack of effects, dev only */
5050
export let dev_effect_stack = [];
5151

52+
/** @type {Set<() => void>} */
53+
export let effect_pending_updates = new Set();
54+
5255
/** @type {Effect[]} */
5356
let queued_root_effects = [];
5457

@@ -296,6 +299,16 @@ export class Batch {
296299

297300
deactivate() {
298301
current_batch = null;
302+
303+
for (const update of effect_pending_updates) {
304+
effect_pending_updates.delete(update);
305+
update();
306+
307+
if (current_batch !== null) {
308+
// only do one at a time
309+
break;
310+
}
311+
}
299312
}
300313

301314
neuter() {
@@ -319,7 +332,7 @@ export class Batch {
319332
batches.delete(this);
320333
}
321334

322-
current_batch = null;
335+
this.deactivate();
323336
}
324337

325338
flush_effects() {
@@ -389,6 +402,8 @@ export class Batch {
389402
this.#effects = [];
390403

391404
this.flush();
405+
} else {
406+
this.deactivate();
392407
}
393408
}
394409

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [increment, shift] = target.querySelectorAll('button');
7+
8+
shift.click();
9+
shift.click();
10+
shift.click();
11+
12+
await tick();
13+
assert.htmlEqual(
14+
target.innerHTML,
15+
`
16+
<button>increment</button>
17+
<button>shift</button>
18+
<p>0</p>
19+
<p>0</p>
20+
<p>0</p>
21+
<p>pending: 0</p>
22+
`
23+
);
24+
25+
increment.click();
26+
await tick();
27+
assert.htmlEqual(
28+
target.innerHTML,
29+
`
30+
<button>increment</button>
31+
<button>shift</button>
32+
<p>0</p>
33+
<p>0</p>
34+
<p>0</p>
35+
<p>pending: 3</p>
36+
`
37+
);
38+
39+
shift.click();
40+
await tick();
41+
assert.htmlEqual(
42+
target.innerHTML,
43+
`
44+
<button>increment</button>
45+
<button>shift</button>
46+
<p>0</p>
47+
<p>0</p>
48+
<p>0</p>
49+
<p>pending: 2</p>
50+
`
51+
);
52+
53+
shift.click();
54+
await tick();
55+
assert.htmlEqual(
56+
target.innerHTML,
57+
`
58+
<button>increment</button>
59+
<button>shift</button>
60+
<p>0</p>
61+
<p>0</p>
62+
<p>0</p>
63+
<p>pending: 1</p>
64+
`
65+
);
66+
67+
shift.click();
68+
await tick();
69+
assert.htmlEqual(
70+
target.innerHTML,
71+
`
72+
<button>increment</button>
73+
<button>shift</button>
74+
<p>1</p>
75+
<p>1</p>
76+
<p>1</p>
77+
<p>pending: 0</p>
78+
`
79+
);
80+
}
81+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script>
2+
let value = $state(0);
3+
let deferreds = [];
4+
5+
function push(value) {
6+
const deferred = Promise.withResolvers();
7+
deferreds.push({ value, deferred });
8+
return deferred.promise;
9+
}
10+
11+
function shift() {
12+
const d = deferreds.shift();
13+
d?.deferred.resolve(d.value);
14+
}
15+
</script>
16+
17+
<button onclick={() => value++}>increment</button>
18+
<button onclick={() => shift()}>shift</button>
19+
20+
<svelte:boundary>
21+
<p>{await push(value)}</p>
22+
<p>{await push(value)}</p>
23+
<p>{await push(value)}</p>
24+
25+
<p>pending: {$effect.pending()}</p>
26+
27+
{#snippet pending()}
28+
<p>loading...</p>
29+
{/snippet}
30+
</svelte:boundary>
31+
32+

0 commit comments

Comments
 (0)