Skip to content

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

Open
waterWang wants to merge 1 commit into
sveltejs:mainfrom
waterWang:fix/effect-pending-ignores-uncommitted-fork-18649
Open

fix: $effect.pending() should not be affected by uncommitted forks#18651
waterWang wants to merge 1 commit into
sveltejs:mainfrom
waterWang:fix/effect-pending-ignores-uncommitted-fork-18649

Conversation

@waterWang

Copy link
Copy Markdown

Summary

Fixes #18649

$effect.pending() (and boundary pending snippets) should not be affected by async work that happens inside an uncommitted fork(). The fork's work is speculative — it hasn't been committed yet, so it must not surface as pending state. This mirrors how $state.eager() is already unaffected by uncommitted forks.

Root cause

In increment_pending() (packages/svelte/src/internal/client/reactivity/async.js), the boundary's pending count was updated unconditionally:

boundary?.update_pending_count(1, batch);

When a promise is created inside a fork (e.g. an async expression that re-evaluates because state changed within the fork), increment_pending() ran with current_batch.is_fork === true and still bumped the boundary's pending count — even though the fork had not been committed, and the DOM had not (and might never) be updated.

Fix

Capture the is_fork state at the time increment_pending() runs and skip the boundary update when it's an uncommitted fork:

var is_fork = batch.is_fork;

if (!is_fork) {
	boundary?.update_pending_count(1, batch);
}
batch.increment(blocking, effect);

return () => {
	if (!is_fork) {
		boundary?.update_pending_count(-1, batch);
	}
	batch.decrement(blocking, effect);
};

The batch's own pending count is still updated (batch.increment/decrement), so fork().commit() can still await the async work as before. Only the boundary-visible pending state (which powers $effect.pending() and the {#snippet pending()} block) is skipped while the fork is uncommitted.

Tests

  • New runtime-runes test async-fork-effect-pending: asserts that $effect.pending() stays 0 (and no pending snippet is shown) when a fork is started but not committed.
  • Full runtime-runes + runtime-legacy suites pass: 5966 passed, 0 failed.

@pkg-svelte-dev

Copy link
Copy Markdown

Install the latest version of svelte from 29f4cc7:

pnpm add https://pkg.svelte.dev/svelte/c/29f4cc7348036a7338e49e48c66256e1eb9f2a26

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/svelte/pr/18651

Note

This PR is from a fork. A maintainer must approve approve each commit before it can be built and installed.

@changeset-bot

changeset-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 29f4cc7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot

Copy link
Copy Markdown

@dummdidumm dummdidumm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, but this solution is incorrect. If the fork commits before the async operation has finished, $effect.pending() will be lower than it should be. Example:

<script>
	import { fork } from 'svelte';
	let count = $state(0);

	function delay(v) {
		return new Promise(r => setTimeout(r, 2000,v))
	}
</script>

{await delay(count)}
{$effect.pending()}
<button onclick={() => {
	const f = fork(() => count++);
	setTimeout(() => f.commit(), 1000);
}}>i</button>

does not show 0 1 after 1 second, just 1 0 after 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Uncommitted fork() should not affect $effect.pending()

2 participants