Repository navigation
fix(signals): a lazy memo read only through a rebuilt child memo is not released mid-pass (#4142) - #4164
Conversation
…ot released mid-pass (#4142) A pass whose previous pass never committed tears that frame's children down on the spot, before it builds its own. When a lazy memo's only subscriber was one of those children (the compiled condition memo of `user() && <User data={user()} />`), the teardown released it, the new child's read recomputed it from scratch, the async fetch went pending again, and the hole looped behind its Loading fallback forever. The teardown now collects those last-one-out releases and runs them when the pass ends, so a node the pass reads again stays alive and one it stopped reading is still released. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
🦋 Changeset detectedLatest commit: 0e272be The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
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 |
Size (brotli, eager entry chunk)
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
Coverage Report for CI Build 39339297559Coverage remained the same at 77.555%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Generated by Claude Code |
Merging this PR will not alter performance
|
Generated by Claude Code |
Accepted by the maintainer on 2026-10-19: +104 B minified on the core floor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
Summary
Fixes #4142, the last open regression gating the next RC (since rc.14).
Before: a lazy async memo read on both sides of a conditional, as in
{user() && <User data={user()} />}under<Show>/<Loading>, fetched forever and the boundary never left its fallback.After: it fetches once and shows its content.
Cause. The compiler turns the condition into a child memo of the hole's pass, and that child memo is the lazy memo's only subscriber. While the fetch is pending the pass never commits, so the next pass tears the old frame's children down on the spot (
recompute,CONFIG_STAGED) before it builds its own. That teardown unlinked the lazy memo's last subscriber, andunlinkSubsreleased it right away. The new condition memo's read then recomputed it from scratch, the fetch went pending again, and the cycle repeated.How. During that one teardown, last-one-out releases are collected (
deferredReleasesingraph.ts) instead of run. When the pass ends,recomputereleases each one that is still unobserved, reusingreleaseIfSettledUnobserved. A lazy node the new pass reads again stays alive. One it stopped reading is still released at the end of the same pass. No public API change.dormant-memo-chain-3554.test.ts: one expectation changes. In the "made dormant and reawakened while its owner's held children drain" case,lazy1's last reader dies in the same teardown, solazy1is no longer made dormant mid-drain and reawakened into the new frame by the cleanup's read. It now dies with the frame it belongs to. The test's point still holds: the drain runs to the end,sibling1is cleaned, andsibling2is cleaned on dispose.Size: +104 B minified on the core floor, about +65 B brotli. Most of it is mangled-name churn from the two new module bindings: the code itself is about 80 B, and I tried three shapes before settling on this one. Hello world (
app: compiled floor) stays under 10 KB brotli at 9.86 KB. Caps are raised to measured + 10 B with dated ledger notes, and each recorded minified size is set to the measured value.Size-Exception: deferring lazy-memo releases until a pending pass ends stops the infinite re-fetch loop in #4142; the maintainer accepted the +104 B minified on 2026-10-19.
How did you test this change?
packages/signals/tests/lazy-memo-rebuilt-reader-4142.test.ts:next(11 runs, nothing shown), passes here.packages/web/test/lazy-memo-loading-loop-4142.spec.tsx: the report's playground, driven through the checkbox's change event. It loops onnextand fetches once here.Suites, run locally after
turbo build:server-functions-adapter-requestoneGenerated by Claude Code