fix: $effect.pending() should not be affected by uncommitted forks (Closes #18649)

pull/18651/head
waterWang 2 months ago
parent c0987c09f8
commit 29f4cc7348

@ -0,0 +1,7 @@
---
'svelte': patch
---
fix: $effect.pending() should not be affected by uncommitted forks
$effect.pending() and boundary pending snippets were incorrectly triggered by async work inside an uncommitted fork. The `increment_pending()` function in the async reactivity module now checks whether the current batch is a fork (`batch.is_fork`). If it is, the boundary's pending count is left untouched — only the batch's own pending count is updated so that `commit` can still await the async work. This makes `$effect.pending()` behave consistently with `$state.eager()`, which is similarly unaffected by uncommitted forks.

@ -371,11 +371,23 @@ export function increment_pending() {
var batch = /** @type {Batch} */ (current_batch);
var blocking = !!boundary?.is_rendered();
boundary?.update_pending_count(1, batch);
// If we're inside an uncommitted fork (`batch.is_fork === true`), the
// pending state changes are speculative — they must not affect the
// boundary's pending count. Only the batch's own pending count is
// updated (so that `commit` can await the async work). The boundary
// (and therefore `$effect.pending()` and any pending snippet) is only
// updated once the fork is committed.
var is_fork = batch.is_fork;
if (!is_fork) {
boundary?.update_pending_count(1, batch);
}
batch.increment(blocking, effect);
return () => {
boundary?.update_pending_count(-1, batch);
if (!is_fork) {
boundary?.update_pending_count(-1, batch);
}
batch.decrement(blocking, effect);
};
}

@ -0,0 +1,44 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [shift, increment, commit] = target.querySelectorAll('button');
shift.click();
await tick();
// baseline: count 0, even, nothing pending
assert.htmlEqual(
target.innerHTML,
`
<button>shift</button>
<button>increment</button>
<button>commit</button>
<p>count: 0</p>
<p>eager: 0</p>
<p>pending: 0</p>
<p>even</p>
`
);
// start a fork that changes state, but does NOT commit it.
// $effect.pending() must stay 0 — the fork's async work has not
// been committed, so it must not surface as pending
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>shift</button>
<button>increment</button>
<button>commit</button>
<p>count: 0</p>
<p>eager: 0</p>
<p>pending: 0</p>
<p>even</p>
`
);
}
});

@ -0,0 +1,38 @@
<script>
import { fork } from 'svelte';
let count = $state(0);
const resolvers = [];
let f = null;
function push(value) {
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<button onclick={async () => {
f = await fork(() => {
count += 1;
});
}}>increment</button>
<button onclick={() => f?.commit()}>commit</button>
<p>count: {count}</p>
<p>eager: {$state.eager(count)}</p>
<p>pending: {$effect.pending()}</p>
<svelte:boundary>
{#if await push(count) % 2 === 0}
<p>even</p>
{:else}
<p>odd</p>
{/if}
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save