You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a computation's async-iterable flight is superseded, the stale iterator is normally closed (it.return()) synchronously at recompute. Adding a single isPending read over the same source changes this: the superseded iterator is left running — it can land a value that flight identity then discards — and return() only fires after the superseding flight settles.
Iterator close is the cancellation hook for anything resource-shaped behind an async iterable (fibers, sockets, subscriptions), so the deferral means wasted in-flight work and late resource teardown. Found while wiring Effect fibers to memos (examples/effect): the superseded search fiber keeps running its retries to completion instead of being interrupted at supersede time.
Ablation
Same graph in all four cases; only the reader changes. Memo flips [] → 400ms async iterable per query write; supersede happens 100ms in.
Reader
Close timing
plain latest(() => m()) effect
immediate at supersede ✅
fresh-mounted reader (mimics <Show> flip)
immediate ✅
createLoadingBoundary + latest
immediate ✅
createLoadingBoundary + latest + isPending
deferred until superseding flight settles ❌ (stale flight lands first)
Observed output for the failing case:
1ms open a
103ms write 'ab' (supersedes 'a')
103ms open ab
402ms land a <-- stale flight ran to completion (value discarded)
505ms land ab
505ms close a <-- return() only after 'ab' settled
Expected (and observed as soon as the isPending read is removed):
1ms open a
103ms write 'ab' (supersedes 'a')
103ms open ab
103ms close a <-- eager close at supersede
504ms land ab
In the full web-render structure (examples/effect typeahead) the deferral only manifests on the first flight after a region mounts; in this minimal graph it defers every supersede.
Repro
Checked in as examples/effect/repro-close-timing.mjs (runs against packages/signals/dist/dev.js, no external deps). Toggle the isPending line inside the boundary content to flip between eager and deferred close:
constview=createLoadingBoundary(()=>{try{// Comment out this isPending read → closes become eager again.constpending=isPending(()=>m());return{ pending,value: latest(()=>m())};}catch{returnundefined;}},()=>"loading-fallback");
Run with node examples/effect/repro-close-timing.mjs.
Fixed in fa13761 (ships in rc.5). Root cause: the iterator close was registered only as an owner cleanup, and a recompute whose previous-run disposal rides the zombie-deferred channel drains it at commitPendingNode — which a verdict-held write defers until the superseding flight settles. The isPending read is what upgraded the write into that held shape, hence the ablation.
The fix keys cancellation to flight identity instead of cleanup timing: a _flightTeardown slot fires at recompute's _inFlight release, so a superseded stream closes at supersede in every reader shape (the owner cleanup stays as the death backstop; close is idempotent). repro-close-timing.mjs now shows close a at the supersede write in both scenarios, and the stale flight never lands. Regression tests pinned in packages/signals/tests/iterator-supersede-close.test.ts — notably the "plain reader" shape also deferred under test timing, so the eager cases in the ablation were the lucky paths, not the rule. Effect fibers behind memos now interrupt at supersede time.
Activity
Add the Effect example: fibers as read-path flights, sagas as action …
fix: close superseded async-iterable flights at supersede time (#3122)
ryansolid commentedon Aug 31, 2026
Fixed in fa13761 (ships in rc.5). Root cause: the iterator close was registered only as an owner cleanup, and a recompute whose previous-run disposal rides the zombie-deferred channel drains it at
commitPendingNode— which a verdict-held write defers until the superseding flight settles. TheisPendingread is what upgraded the write into that held shape, hence the ablation.The fix keys cancellation to flight identity instead of cleanup timing: a
_flightTeardownslot fires at recompute's_inFlightrelease, so a superseded stream closes at supersede in every reader shape (the owner cleanup stays as the death backstop; close is idempotent).repro-close-timing.mjsnow showsclose aat the supersede write in both scenarios, and the stale flight never lands. Regression tests pinned inpackages/signals/tests/iterator-supersede-close.test.ts— notably the "plain reader" shape also deferred under test timing, so the eager cases in the ablation were the lucky paths, not the rule. Effect fibers behind memos now interrupt at supersede time.chore(size): reconcile scenario budgets with the rc.5 correctness batch
chore: rebase onto next (#3123 landed) — restore upstream budget notes
chore: rebase onto next (#3123 landed) — restore upstream budget notes
chore: rebase onto next (#3123 landed) — restore upstream budget notes
chore: rebase onto next (#3123 landed) — restore upstream budget notes
chore: rebase onto next (#3123 landed) — restore upstream budget notes