fix: update `$effect.pending()` immediately after a batch is removed (#16382)

* WIP sync effect pending updates

* fix

* changeset

* fix

* add test

* inline

* unused
pull/16392/head
Rich Harris 2 months ago committed by GitHub
parent 3fa3dd78a1
commit e01bd97bef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: update `$effect.pending()` immediately after a batch is removed

@ -22,7 +22,7 @@ import { get_next_sibling } from '../operations.js';
import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import { DEV } from 'esm-env';
import { Batch } from '../../reactivity/batch.js';
import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@ -92,6 +92,12 @@ export class Boundary {
*/
#effect_pending = null;
#effect_pending_update = () => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#pending_count);
}
};
#effect_pending_subscriber = createSubscriber(() => {
this.#effect_pending = source(this.#pending_count);
@ -238,11 +244,7 @@ export class Boundary {
this.parent.#update_pending_count(d);
}
queueMicrotask(() => {
if (this.#effect_pending) {
internal_set(this.#effect_pending, this.#pending_count);
}
});
effect_pending_updates.add(this.#effect_pending_update);
}
get_effect_pending() {

@ -49,6 +49,9 @@ export let batch_deriveds = null;
/** @type {Effect[]} Stack of effects, dev only */
export let dev_effect_stack = [];
/** @type {Set<() => void>} */
export let effect_pending_updates = new Set();
/** @type {Effect[]} */
let queued_root_effects = [];
@ -296,6 +299,16 @@ export class Batch {
deactivate() {
current_batch = null;
for (const update of effect_pending_updates) {
effect_pending_updates.delete(update);
update();
if (current_batch !== null) {
// only do one at a time
break;
}
}
}
neuter() {
@ -319,7 +332,7 @@ export class Batch {
batches.delete(this);
}
current_batch = null;
this.deactivate();
}
flush_effects() {
@ -389,6 +402,8 @@ export class Batch {
this.#effects = [];
this.flush();
} else {
this.deactivate();
}
}

@ -0,0 +1,81 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [increment, shift] = target.querySelectorAll('button');
shift.click();
shift.click();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>0</p>
<p>0</p>
<p>0</p>
<p>pending: 0</p>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>0</p>
<p>0</p>
<p>0</p>
<p>pending: 3</p>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>0</p>
<p>0</p>
<p>0</p>
<p>pending: 2</p>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>0</p>
<p>0</p>
<p>0</p>
<p>pending: 1</p>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>1</p>
<p>1</p>
<p>1</p>
<p>pending: 0</p>
`
);
}
});

@ -0,0 +1,32 @@
<script>
let value = $state(0);
let deferreds = [];
function push(value) {
const deferred = Promise.withResolvers();
deferreds.push({ value, deferred });
return deferred.promise;
}
function shift() {
const d = deferreds.shift();
d?.deferred.resolve(d.value);
}
</script>
<button onclick={() => value++}>increment</button>
<button onclick={() => shift()}>shift</button>
<svelte:boundary>
<p>{await push(value)}</p>
<p>{await push(value)}</p>
<p>{await push(value)}</p>
<p>pending: {$effect.pending()}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save