Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lane-mount-binding-reads-screen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

An element an optimistic guess mounts now lands together with its bindings (#3835). A first pass created by a guess's lane reads as the lane's, not just routes as it: a binding the lane mounts reads a write the action still holds as the screen (its committed value) and re-derives when the action lands. Before, a binding inside a memo the lane created was born held while the memo's element showed, so `<div>Hello</div>` appeared without its `style` until the action finished; directly under the lane pass the binding showed the held value early instead.
5 changes: 5 additions & 0 deletions .changeset/lane-mount-boundary-over-flight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

A `Loading` mounted by an optimistic write over a flight shows its fallback again (#3835's seat regressed it). Lane work with no committed value yet reads a pending flight the way a mainline mount's memo does: it enters and throws, so the boundary it mounts catches the pending (A29's boundary exemption), instead of being served the flight's committed value. A render effect is still a stale reader of a held flight (rule 3). Approach and tests from #3843 by @brenelz.
5 changes: 5 additions & 0 deletions .changeset/verdict-mount-first-pass-mainline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

A mount made by a verdict reader stays mainline (#3851). Inside an action, `<Show when={latest(x) > 0}>` opens at once, but a `<Loading>` it mounts used to show `content 1` beside the committed `x = 0`: the content's first pass was staged in the verdict lane its creator was in, and in the action's own flush it read the staged `x` before the seam held it. A first pass now takes its creator's lane only for an optimistic lane (head and tail of its pass alike); a mount a verdict lane makes whose first pass reads a staging of a flush with a transaction is born held like one that read a held node (other new memos do not join the transaction by being created); and verdict-lane work that read the frame's stagings re-derives on the committed world when the frame stays parked. The boundary shows its fallback and `content 1` at the landing.
82 changes: 41 additions & 41 deletions packages/signals/docs/RULES-INDEX.md

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t

1. **One frame concept.** A node is committed or staged for this flush; a flush that goes async _parks_ — its stagings are held by a transaction, its passes' children keep computing and die with the commit that replaces them (ruling A, O2: a held pass's children compute but do not effect). There is no store-side or lane-side transition state: a transaction holds **nodes**; a store's container, leaf and presence nodes are nodes (A22 per key, A29 per pass).
2. **Reading the future joins it** (A29 generalized). A pass served a held node's staging derives from that transaction and is its work — tracked or untracked (`untrack` is about dependencies, not about which world a pass derives from). **Frame readers** — a render effect, a verdict reader (`isPending`/`latest`), lane work — see the screen: the committed value, and are re-derived at the holder's landing (A15's stale reader). A children-forbidden reader sees the frame, never the graph (A32).
3. **A staging read is the frame's** (REACTIVE_STAGED_READ). A pass that read this flush's staging as the screen re-derives on the committed world if the flush parks; a verdict read cannot break it out. A verdict reader whose probe was answered before the flush had a transaction runs again at the seam even if a later read routed it into the verdict lane — that was not the probe's answer.
3. **A staging read is the frame's** (REACTIVE_STAGED_READ). A pass that read this flush's staging as the screen re-derives on the committed world if the flush parks; a verdict read cannot break it out. A verdict reader whose probe was answered before the flush had a transaction runs again at the seam even if a later read routed it into the verdict lane — that was not the probe's answer. **Amended 2026-10-07 (#3851, maintainer ruling):** verdict-lane work that is not itself a verdict reader is a staging reader like guess-lane work — when the transaction stays parked it re-runs against the committed values (it no longer reads the frame's proposal); a transaction landing at the same seam makes what it read the screen, and it does not run twice. A verdict reader still answers for itself and re-derives at the holder's landing (#3322, #3540).
4. **Lanes** (A17/A18 on L2). A lane is a new base of a transition, made by a guess; it sees the screen plus its own guesses, breaks out of the parent's hold (its effects run now), and holds itself while a derivation of a guess is in flight. **Lane contract 2:** a guess written with no parent transaction is void. A correction dissolves the lane — the guess and what derived from it are void, the truth is the parent's held write, re-derived there; a corrected lane that had shown keeps its runs. A confirmation is silent. The body's end starts the correction for a guess whose own truth is not in flight (A18 body-end, #3427) — **for a store guess exactly as for a signal's** (2026-10-03, #3662 re-pinned).
5. **Provenance** (Q-D, 2026-10-03, #3331 generalized to stores). Every flight carries the question it answers (`question` / `MAINLINE_QUESTION`). A landing asking an **older** question than the guess it lands beneath is not its answer: held beneath the guess for the commit, judged by the guess's own question; nothing moves on screen. A keyed store landing beneath an arrangement guess is re-based over the older truth by row key (`_laneRebase`; the positional residue of an unkeyed list is documented, not ruled). One predicate for both paths (`stale(el, q)`).
6. **Verdicts are display-ahead** (A19/A24 on L2). A verdict reader of a held node is the holder's verdict lane's — shown now, re-derived at the landing. A24 stands: `isPending` is TRUE over a differing stale truth held beneath a guess (the older action's landing). A quiet re-ask (`refresh()`, a poll) stays quiet through its reveal, on a store's leaves as on a memo (`_reask` stamped onto a held landing's leaf stagings); a `refresh()` during a flight asking a **new** question does not launder it.
Expand Down Expand Up @@ -116,12 +116,14 @@ Sync derivations of transition-held sources are visible through `latest()`/`isPe

### A29. A tracked read served a live transaction's staged value enters that transaction

**Status:** **ruled, amended in place** 2026-09-13 (#3408) — maintainer ruling, Cluster 4 triage: a value derived from the held world is that transaction's work, whichever path first read it; amended 2026-09-14 (creation-time form — "born held"; the entry is the pass's, never the mainline block's); amended 2026-09-18 (#3540, maintainer ruling — boundaries are exempt: a pass under a loading boundary that has not revealed is born held into the transaction and the boundary shows its fallback now); **generalized 2026-10-01 (L2, ruling A)** — every pass served a held staging joins, tracked or untracked (`untrack` is about dependencies, not about which world a pass derives from); a held pass's children compute but do not effect and die with the commit that replaces their frame (so a derive's `onCleanup` from a run a staged re-run never committed fires when a frame replaces it — #3585 re-pinned 2026-10-03)
**Status:** **ruled, amended in place** 2026-09-13 (#3408) — maintainer ruling, Cluster 4 triage: a value derived from the held world is that transaction's work, whichever path first read it; amended 2026-09-14 (creation-time form — "born held"; the entry is the pass's, never the mainline block's); amended 2026-09-18 (#3540, maintainer ruling — boundaries are exempt: a pass under a loading boundary that has not revealed is born held into the transaction and the boundary shows its fallback now); **generalized 2026-10-01 (L2, ruling A)** — every pass served a held staging joins, tracked or untracked (`untrack` is about dependencies, not about which world a pass derives from); a held pass's children compute but do not effect and die with the commit that replaces their frame (so a derive's `onCleanup` from a run a staged re-run never committed fires when a frame replaces it — #3585 re-pinned 2026-10-03); amended 2026-10-07 (#3851, maintainer ruling — a transition holds on any unready read, and a new memo does not join it by being created: a verdict lane's mount that read a staging of a flush with a transaction is born held; no other mount joins for that read)
**Pinned by:** `tests/held-conditional-memo.test.ts` (#3408: a memo whose branch flips mainline and starts reading a held signal reveals with it, not before); `tests/born-held.test.ts` (creation-time form: fresh memo + effect held, fresh direct effect shows committed, unrelated write after the mount stays mainline, untracked read throws until commit); `tests/visibility-oracle.test.ts` (published column, held and superseded states); `tests/boundary-not-born-held-3540.test.ts` (boundary exemption: a `Loading` mounted mainline over a held value shows its fallback and reveals at the commit; content created behind a fallback is collected, not published; the `on` key lands where its change lands)
**Mechanism (L2, 2026-10-04):** `read()`: a reader served a held node's staging **joins** — `joinPass(c, el)` marks the pass `REACTIVE_JOINED` and, for a non-render reader, `joinPassTx(txOf(el))` (inside a flush `joinFuture`; outside, `passTx` for the flush the join schedules). Tracked or untracked (`untrack` is about dependencies). A render effect, a verdict reader or lane work is a **frame reader** instead (`frameRead`: served `_value`, `staleReader` → re-derived at the landing); a children-forbidden reader sees `_value` and joins nothing (A32); a node born into the future (uninitialized, never committed) has no committed value, so every reader derives from it — a render effect included — and joins. **Creation-time form (born held):** a first pass that read the future is held with its result (`CONFIG_STAGED`, `holdNode` at the seam; an effect's first run is the commit's, A29 — `commitPendingNode` queues it). **Ruling A / O2:** a held pass's children keep computing from the heap (`REACTIVE_ZOMBIE`, `deferredZombies`) and die with the commit that replaces their frame (`commitPendingNode` disposes the parked frame; a frame never has a gap). **Boundary exemption (#3540):** `boundaries.ts` — a pass under a loading boundary that has not revealed is born held and the boundary shows its fallback (`_hidden`: a reader behind a fallback is not on screen for `blocked`).

A tracked computation served a node's staged `_pendingValue` — a value a live transaction holds — derives from that transaction's world, so its pass enters the transaction and its result is held with it. This is the read twin of the two entries that already existed, `setSignal` on a stamped node and `recompute` of a stamped node, and closes the gap between them: a conditional memo whose branch flipped mainline (`panel = show() ? count() : "hidden"`) started reading a held `count`, was served the staged value (non-stale readers keep speculation), and published a value derived from the held world into the mainline frame — `Panel: 1` beside `Count: 0`. A stale (render) reader is unchanged: the reveal carve-out (A15) serves it the committed value with no entanglement, which is why the same shape written as a plain JSX expression already showed a coherent frame. A probe (`isPending(() => x())`) observes, it does not derive, so it enters nothing (A23). **Creation-time form (amended 2026-09-14, "born held"):** the same rule for a memo or effect CREATED from mainline code while the hold is live — a component mounting on a click while an action is in flight. Its creation pass is served the staged value and derives from the transaction's world, so its result is the transaction's: staged into it, committed with it, and — for an effect — first run by its commit; it has no committed value until then (an untracked read of it throws `NotReadyError`, A19 exception 1; a stale reader of it cannot fall back to the committed frame and enters instead). A fresh reader that reads the held node DIRECTLY is a stale reader of a parallel transaction and shows the committed value as before (A15) — so a mainline mount shows the committed frame for direct bindings and holds derived ones until the commit reveals both. The entry is the pass's alone: `activeTransition` and the ambient batch are never touched from creation code, so a write made after the mount is a mainline write. (Maintainer, on the earlier direct-commit: "new creation wouldn't be on screen anyway… we could just be preemptive" — true for nodes the transaction itself creates, which stay inside it; a mainline mount IS on screen.) Before this the creation pass direct-committed the staged value into the mainline frame, and the ambient entry swallowed the handler's later writes into the action. **Boundaries are exempt (amended 2026-09-18, #3540):** born held is right for a plain memo or effect — published, its value would tear the frame. A loading boundary that has not revealed yet is the exception by definition: its job is to catch what is not ready under it rather than let it hold. A pass created under such a boundary that derives from a held value is staged into the transaction as above, and the boundary is told: it shows its fallback NOW and reveals the staged result at the commit (a `Loading` mounted mainline over a held `count()` shows its fallback beside `Count: 0`, then `count 1` beside `Count: 1`). The hold stays with readers that have content to keep — a boundary already showing content is not told, and holds like any reader. Entering instead adopted the whole flush into the hold, and the creation-time form had an in-flush escape: content created behind a fallback over a held value direct-committed the held value and ran its effect behind the fallback (`posture-store-parity` S2, closed by this amendment — nothing publishes until the commit). The boundary's `on` key is the same rule seen from outside (`createLoadingBoundary` docblock): a tracked expression evaluated as the condition of a `<Show keyed>` wrapping the boundary would be — it lives outside the boundary, holds like a plain reader, and a change resets the boundary where the change lands (`on={count()}` with the write's commit, `on={latest(count)}` ahead of it).

**A verdict lane's mount over a staging (amended 2026-10-07, #3851).** Maintainer ruling: "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." A transition holds on any unready read; a memo does not join it because it is new. Inside an action, `<Show when={latest(x) > 0}>` opens ahead of the action (A19, display-ahead) and its condition is verdict-lane work; the `<Loading>` it mounts is a mainline mount (the creator's verdict lane is not inherited). Its content's first pass runs in the action's own flush and reads `x`'s staging before the seam holds it — the same world the transaction is about to hold, so the pass is born held as if it had read the held node, and the boundary shows its fallback beside the committed `x = 0` and `content 1` at the landing (`tests/verdict-mount-first-pass-3851.test.ts`). Only a verdict lane's mount: any other first pass that read a staging of the flush publishes as before and, if the flush parks, re-derives on the committed world (the staging-read rule, L2 statement 3). In particular a boundary a held memo mounts in the same flush is not made the transaction's by its creation — its content does not join, the boundary reports no pending of its own, and it appears with the memo at the commit (`tests/loading-fallback-in-flush-3540.test.ts`, "a boundary mounted by the hold"). An action body's read of an unflushed write is served committed (A28) and never counts.

### A31. A memo computes under its own lane posture, never its puller's

**Status:** **live** 2026-09-14 (#3442) — stated by the fix; the lane-side twin of A29's "a value derived from the held world is that transaction's work"
Expand Down
Loading
Loading