Conversation
… a flush (#3540, L2 regression) L2 regressed A29's boundary exemption in two ways: - In a flush, a first pass under a loading boundary that has not shown content joined the flush to the hold it read, so the whole tick (the Show that mounted the boundary included) waited for the hold. A first pass that reads a hold now asks the boundaries up its chain (GlobalQueue._fresh); one such boundary catches it, and the pass joins pass-scoped (passTx), as a mount outside a flush does: the boundary's, not the tick's. A derivation outside the boundary in the same flush still holds the tick. A flush that set passTx clears it at its end. - A render effect born held under a fresh loading boundary (content bound in JSX: the tree never reads the held value) was not caught, so the boundary revealed empty content, in a flush and out of one. It is now collected as a reader, and the output's first pass shows the fallback whenever the boundary has collected readers. Refs #3540 Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 3f05444 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 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)
|
Coverage Report for CI Build 37441526431Coverage remained the same at 76.058%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 12.41%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | memo + sync render effect only (reference) |
32 ms | 27 ms | +18.54% |
| ⚡ | build + consume |
742 µs | 696 µs | +6.61% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing fix/l2-3540-fresh-loading (a661e91) with next (7acc039)
…dary, and only one that has not shown content (#3540) A revealed boundary (re-armed by `on` or not) neither catches nor records a never-committed first pass, and no boundary past the nearest is asked: which boundaries catch what is unchanged beyond A29's boundary exemption. catchStatus is back to next's. Refs #3540 Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
… hook (#3540) A pass that read a hold asks GlobalQueue._catch with STATUS_UNINITIALIZED in its flags. Only a first pass is caught, and only by the nearest loading boundary if it has not shown content (`_initialized`, not `_armed`): a revealed boundary, re-armed by `on` or not, neither catches nor records it, and no boundary past the nearest is asked. Pins the revealed-boundary, nested and `on` shapes against next. Refs #3540 Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
… B (A29 restore, #3540) app: hydrating + every store primitive family 28.87 -> 28.90 KB, app: compiled CSR 25.13 -> 25.22 KB, app: compiled hydrating 30.93 -> 30.97 KB, page: base server components 44.84 -> 44.90 KB, page: live server components 48.51 -> 48.53 KB. Approved by the maintainer. Refs #3540 Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…ding # Conflicts: # packages/signals/docs/RULES-INDEX.md
Refs #3540
L2 regressed A29's boundary exemption (a
Loadingthat has not shown content, mounted while a transaction holds what it reads, shows its fallback now and its content at the commit) in two ways. This is the #3540 piece offix/create-time-holds(ca71e5d9d) on its own — #3802/#3814 and #3800 (_bornIn) are not in it — reworked to reuseSTATUS_UNINITIALIZED,_initializedand the boundaries' existing catch (GlobalQueue._catch) instead of a new hook.The two defects
flushTransaction), so the tick — theShowthat mounted the boundary included — was held until the release: nothing showed, not even the fallback.<p>{m()}</p>— so the boundary's tree never reads the held value) was never caught, so the boundary revealed its (empty) content instead of its fallback. In a flush and out of one.The fix
joinPass(core.ts): a pass that reads a hold asks its boundaries through the existingGlobalQueue._catch, withSTATUS_UNINITIALIZEDin the flags; a render effect reading a node born into the future (no committed value) joins it like any reader.catchStatuscatches only a first pass (STATUS_UNINITIALIZED), and only at the nearest loading boundary, if it has not shown content (!_initialized). A revealed boundary — re-armed byonor not — neither catches nor records it, no boundary past the nearest is asked, and a committed pass is never caught. So which boundaries catch what is unchanged beyond A29's exemption.joinPassTx(t, own)keeps the pass pass-scoped (passTx) inside a flush too, as a mount outside a flush is: the boundary's, not the tick's. Nothing caught it → it joins the flush as before, so a derivation outside the boundary in the same flush still holds the tick (membership is the tick's).b._readers.size !== 0), not only when its own tree is born held — this covers the bound-content case; the previouscollecting.add(b)is redundant now (the catch adds it).flush()clearspassTxat its end, so a boundary's pass-scoped hold set in a flush does not merge with a later tick's.read()routes the uninitialized-owner case throughjoinPass(it was a duplicate of it minus the render-effect test).SPEC-ASYNC-SEMANTICS.md: a dated amendment paragraph after the direction rule's bullets (the ruling and its bullets are unchanged);RULES-INDEX.mdregenerated.Public API changes
None.
catchStatus's (internal)errorparameter became optional;GlobalQueue._catch(internal) also acceptsSTATUS_UNINITIALIZEDin its flags.Review follow-up: what the first-pass catch records (open questions 1 and 2)
The first revision of this PR routed the first-pass catch through
catchStatus's ordinary walk, which records a pending reader on every revealed loading boundary it passes and catches at the first one still collecting. For a never-committed first pass that meant:Loading): the revealed boundary recorded it as a reader.nextrecords only the pending memo the frame forwards; the first revision also recorded the born-held binding — 2 readers vsnext's 1 (pinned now: exactlynext's 1, and the mount joins the transaction as onnext).Loadinginside one still on its fallback): the revealed inner one recorded it, and the outer one caught it — onnextneither does (pinned now: as onnext).Loadingover an inner fresh one records nothing, is not pending, and the transaction commits without waiting for the fresh computation's own first load).on: the walk'sisCollectingcounts an armed (onchanged this flush) revealed boundary as collecting. With!_initializedinstead, an armed boundary that has shown content is treated as revealed. Everyonshape tried — revealed, re-armed or not by the mounting flush, mounted by a render effect or by the boundary's own tree — behaves exactly as onnext; anonboundary that has not revealed behaves as aLoadingwithouton(from mainline as onnext; in a flush the Loadingonshould behave as a keyed <Show> around the boundary; a boundary mounted under a hold must not be born held (A29) #3540 restore).Now: only the nearest loading boundary is asked, it catches only if it has not shown content, and nothing is ever recorded on a revealed boundary for a first pass.
Tests
packages/signals/tests/loading-fallback-in-flush-3540.test.ts(18; 8 fail onnext, all pass here):Loadingthat has shown content; a derivation outside the boundary still holds the tick; the boundary's hold stays its own (pins the flush-endpassTxclear — fails without it);isPendingis false and it records no reader before, during or after; the transaction commits while the fresh computation's own first load is still out; nothing stays registered after the inner one resolves. Directly under a revealedLoading, and under a revealed one inside one that has not shown content: exactlynext's trace (pass onnext);on(fix(signals): Loadingonfollows the frame; DEV LOADING_ON_OUTSIDE_HOLD (#3540) #3575: a dependency list): revealed, re-armed or not, render-effect mount or tree mount — exactlynext's trace (pass onnext); not yet revealed, from mainline (passes onnext) and in a flush (the restore).packages/web/test/loading-fallback-in-flush-3540.spec.tsx(3, fromca71e5d9d): 2 fail onnext.direction-rule-probe.test.ts: pins unchanged (3it.failsstill fail; the in-flush-boundary row still passes).lowercase-on-attribute.spec.tsx— the same 2 fail onnextin this environment (native compiler binary); signalstsc✓,pnpm types✓;rules-index --check✓.Semantic fuzzer (
fuzz/semantic-fuzzer-l2@08921755e, oracle revision 19)Seeds 3289 and 91501, all 16 cohorts × 2000 cases, the three equivalence modes × 500, and the corpus (2 jobs at a time), against
next(7acc0390e, #3814 included) and this branch's final code: no delta — 330 findings on each, identical signatures and case indices in every cohort (all pre-existing onnext).Size
Size-Exception: approved by the maintainer for #3824 — app: hydrating + every store primitive family, app: compiled CSR, app: compiled hydrating, page: base server components, page: live server components. Restoring A29's boundary exemption (#3540) under L2 costs +27–28 B minified on every scenario with a boundary (+17 B core: the first-pass catch in
joinPass, the pass-scoped join, the flush-endpassTxclear; +10–11 B boundaries: the nearest-boundary stop that keeps revealed boundaries from catching or recording a first pass), 7–8 B past the 20 B allowance where these five are over their brotli caps.The five caps are reset to this PR's CI-measured brotli (run on
2d2c28259, againstnext@7acc0390e) + 10 B, rounded up to 0.01 KB, with dated ledger notes inscripts/size/scenarios.js(the two pages' caps infloor-caps.json). No other cap changes. The core-only scenarios (+16–17 B minified) pass within the allowance.