Skip to content

fix(signals): seat a first pass in its creator's lane, never a verdict lane (#3835, #3851) - #3869

Merged
ryansolid merged 10 commits into
nextfrom
fix/3835-3851-first-pass-lane
Oct 7, 2026
Merged

ryansolid merged 10 commits into
nextfrom
fix/3835-3851-first-pass-lane

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Fixes #3835
Fixes #3851

A first pass is seated in its creator's lane when that lane is a guess's, and never in a verdict lane. Three commits on top of the #3835 seat, plus the maintainer's 2026-10-07 ruling on born held.

Credit: the frameRead gate for uninitialized lane memos, the A31 skip, and the optimistic-mount-nested-memo-3835 tests come from @brenelz's #3843 (carried verbatim, Co-authored-by on that commit, named in its changeset). His diagnosis — passLane cleared at the start of the creation pass — is what the #3835 fix builds on.

Fixes

#3835: a binding an optimistic mount creates lands with its element

  • Cause. A first pass created by a guess's lane routed its result to the lane, but recompute's head reset passLane for every fresh node, so the pass read as the action's own. It was served the held staging and joined the transaction. Inside a memo the lane created, the binding was born held while the element showed through the lane (<div>Hello</div> without its style until the action landed).
  • Fix. The head seats a first pass in its creator's guess lane, so the binding is the lane's stale reader of the held write: committed value now, re-derived at the landing. Verdict lanes are not inherited.
  • Tests. optimistic-mount-binding-3835.test.ts, packages/web/test/optimistic-mount-style-3835.spec.tsx.

A Loading a lane mounts over a flight shows its fallback (from #3843)

  • The seat made the lane's first pass lane work from its start, so a Loading the lane mounts over a flight read it through A31 and was served the flight's committed value. Lane work with no committed value yet now reads a pending flight as a mainline mount's memo does: it enters and throws, and the boundary catches it (A29's boundary exemption). Render effects keep rule 3's stale read.
  • Tests. optimistic-mount-nested-memo-3835.test.ts (brenelz, verbatim).

#3851: a verdict reader's mount stays mainline

Inside an action, <Show when={latest(x) > 0}> opens at once, and a <Loading> it mounted showed content 1 beside the committed x = 0.

Rulings (dated amendments in SPEC-ASYNC-SEMANTICS.md)

  • L2 statement 3, the staging-read rule (2026-10-07): verdict-lane work that is not itself a verdict reader re-runs against committed values when the transaction stays parked. This reverses the earlier "a verdict lane's work reads the frame's proposal"; the stagedRead comment is updated.
  • A29 (2026-10-07): the maintainer's ruling, quoted: "we don't want to add size for these unseen things.. that being said in general transitions can hold on any unready read.. But new memos created don't need to be part of it." The first version of this branch made every first pass that read a staging inside a flush with a transaction born held. That held the content of a Loading mounted by a held memo, and the hidden boundary reported pending (fix(signals): a Loading mounted under a hold shows its fallback (L2 regression of #3540) #3824 pins isPending(view) === false). The shipped form applies the hold only to a verdict lane's mount: prevLane && el._flags & REACTIVE_STAGED_READ. It also replaces the globalQueue._running guard, because an action body never runs under a verdict lane. That makes it 3 B smaller (minified) than the first version.

Tests

  • Full @solidjs/signals suite: 5052 passed, 26 expected failures, 2 skipped.
  • packages/web/test/optimistic-mount-style-3835.spec.tsx and loading-fallback-in-flush-3540.spec.tsx pass.
  • On next, optimistic-mount-binding-3835 (2/2), verdict-mount-first-pass-3851 (2/2) and the web Optimistically mounted component appears before its dynamic style is applied #3835 spec (3/3) fail. optimistic-mount-nested-memo-3835 has 2 that fail on next and 2 that pass there; those two guard against the regression the seat caused (the Loading over a flight).

Size

CI Size run 37642512669, against next @ 53ef0e69e (after #3853, #3855, #3865). The approved budget was this branch's numbers before the 2026-10-07 ruling: about +36 B minified on the floors and +62 B on isPending/latest. The ruled form comes in 3 B smaller than that on every affected scenario.

Scenario Brotli (base → head) Minified (base → head) Cap
signals: core floor (frozen) 7,374 → 7,384 (+10) 20,144 → 20,178 (+34) 7.35 → 7.40 KB
app: render + one signal / hello world (frozen) 9,884 → 9,867 (−17) 27,698 → 27,731 (+33) 9.86 → 9.88 KB
signals: + isPending/latest 9,561 → 9,568 (+7) 26,962 → 27,021 (+59) 9.58 KB, under
signals: + createStore 14,603 → 14,606 (+3) 44,407 → 44,439 (+32) 14.56 → 14.62 KB
app: hydrating (no stores) (frozen) 17,738 → 17,724 (−14) 52,641 → 52,674 (+33) 17.73 KB, under
app: hydrating + every store primitive family 29,003 → 29,015 (+12) 91,882 → 91,942 (+60) 29.02 KB, under
app: CSR 12,890 → 12,866 (−24) 36,584 → 36,617 (+33) 12.86 → 12.88 KB
app: compiled floor 10,042 → 10,058 (+16) 28,229 → 28,261 (+32) 10.05 → 10.07 KB
app: compiled CSR 25,197 → 25,207 (+10) 78,913 → 78,946 (+33) 25.13 → 25.22 KB
app: compiled hydrating 30,958 → 31,050 (+92) 99,272 → 99,305 (+33) 31.03 → 31.06 KB
page: base server components (frozen) 45,117 → 45,130 (+13) 146,391 → 146,424 (+33) 45.12 → 45.14 KB
page: live server components (frozen) 48,831 → 48,892 (+61) 158,477 → 158,537 (+60) 48.89 → 48.91 KB

Every raised cap is CI brotli + 10 B (rounded up to 0.01 KB) with its recorded minified set to the head's, and has a dated ledger note in scripts/size/scenarios.js (floor-caps.json for the frozen ones). Most of those scenarios were already over their brotli cap on layout and within the minified allowance; this PR's ~+33 B minified goes past it. The observe tiers, frames, and server scenarios are unchanged or under their caps.

Public API changes

None. Internal recompute/read/frameRead and lane-seam logic only.

Size-Exception: first-pass lane seating for #3835/#3851 (about +33 B minified on the signals core, +59 B on signals: + isPending/latest), accepted by the maintainer 2026-10-07, so the frozen signals core floor, simple-app floor and both page floors move.

ryansolid and others added 6 commits October 6, 2026 10:04
…ement (#3835)

A first pass created by a guess's lane is the lane's frame (ruling A) and
recompute's tail already routed its result and run there, but its head
reset `passLane` for every fresh node, so the pass read as the action's
own pass in the action's flush: served the held staging and joined the
transaction. Inside a memo the lane created, the binding was born held
while the memo's element showed through the lane (`<div>Hello</div>`
with no style until the action landed); directly under the lane pass it
showed the held value early. The head now seats a first pass in its
creator's guess lane, so the binding is the lane's stale reader of the
held write: committed value now, re-derived at the landing (frameRead,
2026-10-01: a lane sees the screen plus its own guesses).

Verdict lanes are not inherited: verdict work reads the staged world and
a mount it makes stays a mainline mount (A29's boundary exemption; the
#3540 keyed-Show-over-latest rows).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Inside an action, `<Show when={latest(x) > 0}>` opens at once, and a
`<Loading>` it mounted showed `content 1` beside the committed `x = 0`.
Three gaps let the proposal through:

- #3835's seat excluded a verdict lane only at recompute's head; the tail
  still routed the first pass to the creator's lane (`passLane ??
  creatorLane`), so the content was staged as verdict-lane work. The
  creator's lane is now filtered once, for both.
- Mainline, the content's first pass ran in the action's own flush and
  read `x`'s staging before the seam held it: neither joined nor created
  by a joined pass, it published directly. A first pass that read a
  staging of a flush with a transaction is now born held like one that
  read a held node (A29) — inside a flush only: an action body's read of
  an unflushed write is served committed (A28, posture-store-parity S6).
- The boundary's output, re-derived at the seam, entered the transaction;
  the verdict-lane render effect read its staging in that flush and its
  run was released with the lane. Verdict-lane work that read the frame's
  stagings now re-derives on the committed world when the frame stays
  parked (`stagedReaders`), as guess-lane work does; a verdict reader
  still answers for itself (#3322, #3540), and a transaction landing at
  the same seam needs no second run (pending-gated-landing-replay).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
#3835's seat (ae59c76) made a guess lane's first pass lane work from its
start, so a `Loading` the lane mounts over a flight read it as lane work:
A31 served the flight's committed value (`data 1`) instead of the
boundary's fallback. It passed on `next`. Lane work with no committed
value yet now reads a pending flight as a mainline mount's memo does — it
enters and throws, and the boundary catches it (A29's boundary exemption,
#3540); render effects keep rule 3's stale read.

Taken from #3843 (brenelz): the `frameRead` and A31 arms, the tail's
`let lane = passLane` (a seated pass never loses its lane mid-pass), and
optimistic-mount-nested-memo-3835.test.ts verbatim.

Co-authored-by: Brenley Dueck <184235+brenelz@users.noreply.github.com>
Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ass-lane

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/signals/docs/RULES-INDEX.md
#	packages/signals/src/core/core.ts
…ead (#3851)

Maintainer ruling (2026-10-07): a transition holds on any unready read,
but a new memo does not join it by being created. The staged-read born-
held clause applied to every first pass inside a flush with a
transaction, so a Loading that a held memo mounted had its content born
held and reported pending while hidden (#3824's pinned
isPending(view) === false). It now applies only to a mount made under a
verdict lane (the #3851 shape); other mounts publish and re-derive if the
flush parks. An action body never runs under a verdict lane, so the
`globalQueue._running` guard (A28: a body's read of an unflushed write
is served committed) is no longer needed: -3 B minified against the
previous clause.

Spec: dated amendments next to A29 (the ruling, quoted) and on the L2
staging-read statement (verdict-lane work re-runs against committed
values when the transaction stays parked).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 95bae2f

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

This PR includes changesets to release 12 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch
todos-server-example 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

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.38 KB +8 B (+0.1%) +33 B −1 B 7.40 KB ✅
signals: + createStore 14.62 KB +14 B (+0.1%) +31 B −1 B 14.62 KB ✅
signals: + isPending/latest 9.57 KB +9 B (+0.1%) +58 B +58 B 9.58 KB ✅
app: render + one signal (the simple-app floor) 9.88 KB −6 B (−0.1%) +32 B −1 B 9.88 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.71 KB −25 B (−0.1%) +32 B +47 B 17.73 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.00 KB −2 B (−0.0%) +59 B +59 B 29.02 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.86 KB −27 B (−0.2%) +32 B −1 B 12.88 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.46 KB +22 B (+0.2%) +32 B +47 B 14.46 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.65 KB −35 B (−0.1%) +32 B +32 B 28.70 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.07 KB +26 B (+0.3%) +31 B −1 B 10.07 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.20 KB −1 B (−0.0%) +32 B −1 B 25.22 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.02 KB +57 B (+0.2%) +32 B −1 B 31.06 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 14.00 KB 0 B 0 B 0 B 14.02 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 45.14 KB +22 B (+0.0%) +32 B −1 B 45.14 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
page: live server components (base + live/GET + action + isPending/latest) 48.89 KB +57 B (+0.1%) +59 B −1 B 48.91 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.41 KB 0 B 0 B 0 B 20.42 KB ✅

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

Raises nine caps to CI brotli + 10 B with their recorded minified, from
Size run 37642512669 against next @ 53ef0e6 — the frozen core,
simple-app and page floors among them. Accepted by the maintainer
2026-10-07.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@codspeed

codspeed Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 186 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ build + consume 695.7 µs 740.5 µs -6.05%
⚡ createStore setter: delete + set one root key (#3044 overlay) 2 ms 1.8 ms +11.09%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/3835-3851-first-pass-lane (95bae2f) with next (537e945)

Open in CodSpeed

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@coveralls

coveralls commented Oct 7, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37655687712

Coverage remained the same at 75.927%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1199
Covered Lines: 964
Line Coverage: 80.4%
Relevant Branches: 932
Covered Branches: 654
Branch Coverage: 70.17%
Branches in Coverage %: Yes
Coverage Strength: 28.03 hits per line

💛 - Coveralls

ryansolid and others added 2 commits October 7, 2026 09:10
recompute's head asked creatorPass(context) for every first pass before
checking for a creator lane. The lane comparison now goes first, with a
loose != that is false when there is no lane (undefined == null), so the
common path never calls it. Same result; -1 B minified.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 11e9fb6 into next Oct 7, 2026
7 of 8 checks passed
ryansolid added a commit that referenced this pull request Oct 7, 2026
…swer into it (#3800) (#3881)

* fix(signals): a new async memo created over a hold lands its first answer into it (#3800)

A memo created while a transaction holds a value, whose first pass reads
that value and returns a promise, was a loading source the seam does not
hold (plan sec. 15.2), so its answer landed as mainline commit #0 and its
readers showed the held future beside the committed page. The same memo
with a synchronous first answer is born held (A29). The first pass now
records the transaction it was born into under the born-held arm's
conditions (a joined read, or a verdict lane's mount reading a staging),
and its first landing is staged into that transaction while it is live.
The hold never waits for the first load (the direction rule, #3820).

Maintainer clarification (2026-10-07): the #3869 ruling covers new memos
that don't read async; a new memo that reads the held world is held,
async first answer included.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(size): size exception for a new async memo's first landing into its hold (#3800)

Raises seventeen caps to CI brotli + 10 B with their recorded minified,
from Size run 37684843282 against next @ dafad1d — the frozen core,
simple-app, hydrating and live-page floors among them. Accepted by the
maintainer 2026-10-07 on the condition hello world stays under 10 KB
(CI: 9,914 B).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Oct 7, 2026
… the lane seam (#3882)

Since #3869, verdict-lane work that read a staging of the flush is
re-derived on the committed world while the frame stays parked
(`stagedReaders`, the lane seam). A node born into the parked frame has
no committed value: the re-run reads the same staging, registers again,
and the seam re-queues it every round until flush throws "Potential
Infinite Loop Detected" (semantic fuzzer, mount-under-hold: a
`latest()`-gated Show mounting a fresh Loading under an action hold,
29 cases across seeds 3289 and 91501).

`stagedRead` keeps REACTIVE_STAGED_READ for such a read but no longer
lists the pass for re-derivation. The frames return to pre-#3869's (the
content shows the held value while held; A29's fallback frame is still
open, pinned as test.fails).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants