fix(signals): lane authority — merged hold, reveal-on-flight, INV-11, override supersession with provenance (#3335 #3334 #3330 #3331) - #3347
Conversation
🦋 Changeset detectedLatest commit: fae8b76 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 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 |
|
AI review based on the context where I found these issues:
|
|
|
|
Made playgrounds for them if you want to try them more easily. https://s.olid.uk/id/UnddL9GdQhOxx7vp3_EU8A Testing the third one made me realize that that one might be working as intended. It is just that we could resolve faster given the observer is removed but there is no inconsistency in not doing it. |
03334af to
b6d266e
Compare
Coverage Report for CI Build 34574149299Warning No base build found for commit Coverage: 71.842%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
b6d266e to
dee5021
Compare
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331) Review on #3347 (GabbeV). Two fixes and a re-ruling. A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch carve-out outright — a stale (render) reader of a node pending in another transaction always held on the flight. That made a new reader of a memo whose input write is ITSELF still held wait for the landing, though showing the committed pair introduces no inconsistency (parallel transactions; effects don't entangle; React 19.3 stopped entangling the same shape). The carve-out returns, gated on input visibility: the reader shows committed, does not entangle, and is recorded for the transaction's commit replay (heldFromStale) — refused, holding instead, when the committed value would tear against the frame: the flight's inputs were PUBLISHED while it was pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed branch when a commit leaves the node in the air, #3305; cleared by notifyStatus when the node next enters pending from a settled state), the node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather than sticky; optimistic / latest, #3334), or the node is uninitialized. The initialized-memo pin and its store twin return to "show v0, then v1". Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs that later recomputes UNDER the recording transaction sees its staged view and is applied by the commit (ownership) — the stale recording published the frame twice. recompute drops it at its start; a lane's committed-view read re-records during the run, so the lane replay is untouched. Provenance (#3331 follow-up): a same-value optimistic write by a newer action took the fast path — entangling the transaction — without renewing _overrideStamp, so the OLDER action's slow source superseded a value the user had just re-confirmed: a corrective downstream refetch and a pending flip for nothing. The fast path now renews the stamp to a newer origin. Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never catches up" and "conditional reader stays hidden" shapes, a replacement flight over published inputs, a retired lane), the same-value provenance twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3. Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled for the lane-authority stack with per-scenario notes. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
dee5021 to
4638b34
Compare
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331) Review on #3347 (GabbeV). Two fixes and a re-ruling. A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch carve-out outright — a stale (render) reader of a node pending in another transaction always held on the flight. That made a new reader of a memo whose input write is ITSELF still held wait for the landing, though showing the committed pair introduces no inconsistency (parallel transactions; effects don't entangle; React 19.3 stopped entangling the same shape). The carve-out returns, gated on input visibility: the reader shows committed, does not entangle, and is recorded for the transaction's commit replay (heldFromStale) — refused, holding instead, when the committed value would tear against the frame: the flight's inputs were PUBLISHED while it was pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed branch when a commit leaves the node in the air, #3305; cleared by notifyStatus when the node next enters pending from a settled state), the node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather than sticky; optimistic / latest, #3334), or the node is uninitialized. The initialized-memo pin and its store twin return to "show v0, then v1". Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs that later recomputes UNDER the recording transaction sees its staged view and is applied by the commit (ownership) — the stale recording published the frame twice. recompute drops it at its start; a lane's committed-view read re-records during the run, so the lane replay is untouched. Provenance (#3331 follow-up): a same-value optimistic write by a newer action took the fast path — entangling the transaction — without renewing _overrideStamp, so the OLDER action's slow source superseded a value the user had just re-confirmed: a corrective downstream refetch and a pending flip for nothing. The fast path now renews the stamp to a newer origin. Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never catches up" and "conditional reader stays hidden" shapes, a replacement flight over published inputs, a retired lane), the same-value provenance twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3. Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled for the lane-authority stack with per-scenario notes. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
4638b34 to
20cd4c7
Compare
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331) Review on #3347 (GabbeV). Two fixes and a re-ruling. A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch carve-out outright — a stale (render) reader of a node pending in another transaction always held on the flight. That made a new reader of a memo whose input write is ITSELF still held wait for the landing, though showing the committed pair introduces no inconsistency (parallel transactions; effects don't entangle; React 19.3 stopped entangling the same shape). The carve-out returns, gated on input visibility: the reader shows committed, does not entangle, and is recorded for the transaction's commit replay (heldFromStale) — refused, holding instead, when the committed value would tear against the frame: the flight's inputs were PUBLISHED while it was pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed branch when a commit leaves the node in the air, #3305; cleared by notifyStatus when the node next enters pending from a settled state), the node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather than sticky; optimistic / latest, #3334), or the node is uninitialized. The initialized-memo pin and its store twin return to "show v0, then v1". Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs that later recomputes UNDER the recording transaction sees its staged view and is applied by the commit (ownership) — the stale recording published the frame twice. recompute drops it at its start; a lane's committed-view read re-records during the run, so the lane replay is untouched. Provenance (#3331 follow-up): a same-value optimistic write by a newer action took the fast path — entangling the transaction — without renewing _overrideStamp, so the OLDER action's slow source superseded a value the user had just re-confirmed: a corrective downstream refetch and a pending flip for nothing. The fast path now renews the stamp to a newer origin. Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never catches up" and "conditional reader stays hidden" shapes, a replacement flight over published inputs, a retired lane), the same-value provenance twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3. Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled for the lane-authority stack with per-scenario notes. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
20cd4c7 to
69bd989
Compare
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331) Review on #3347 (GabbeV). Two fixes and a re-ruling. A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch carve-out outright — a stale (render) reader of a node pending in another transaction always held on the flight. That made a new reader of a memo whose input write is ITSELF still held wait for the landing, though showing the committed pair introduces no inconsistency (parallel transactions; effects don't entangle; React 19.3 stopped entangling the same shape). The carve-out returns, gated on input visibility: the reader shows committed, does not entangle, and is recorded for the transaction's commit replay (heldFromStale) — refused, holding instead, when the committed value would tear against the frame: the flight's inputs were PUBLISHED while it was pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed branch when a commit leaves the node in the air, #3305; cleared by notifyStatus when the node next enters pending from a settled state), the node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather than sticky; optimistic / latest, #3334), or the node is uninitialized. The initialized-memo pin and its store twin return to "show v0, then v1". Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs that later recomputes UNDER the recording transaction sees its staged view and is applied by the commit (ownership) — the stale recording published the frame twice. recompute drops it at its start; a lane's committed-view read re-records during the run, so the lane replay is untouched. Provenance (#3331 follow-up): a same-value optimistic write by a newer action took the fast path — entangling the transaction — without renewing _overrideStamp, so the OLDER action's slow source superseded a value the user had just re-confirmed: a corrective downstream refetch and a pending flip for nothing. The fast path now renews the stamp to a newer origin. Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never catches up" and "conditional reader stays hidden" shapes, a replacement flight over published inputs, a retired lane), the same-value provenance twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3. Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled for the lane-authority stack with per-scenario notes. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
05bcc71 to
451f087
Compare
69bd989 to
e3b08d2
Compare
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331) Review on #3347 (GabbeV). Two fixes and a re-ruling. A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch carve-out outright — a stale (render) reader of a node pending in another transaction always held on the flight. That made a new reader of a memo whose input write is ITSELF still held wait for the landing, though showing the committed pair introduces no inconsistency (parallel transactions; effects don't entangle; React 19.3 stopped entangling the same shape). The carve-out returns, gated on input visibility: the reader shows committed, does not entangle, and is recorded for the transaction's commit replay (heldFromStale) — refused, holding instead, when the committed value would tear against the frame: the flight's inputs were PUBLISHED while it was pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed branch when a commit leaves the node in the air, #3305; cleared by notifyStatus when the node next enters pending from a settled state), the node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather than sticky; optimistic / latest, #3334), or the node is uninitialized. The initialized-memo pin and its store twin return to "show v0, then v1". Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs that later recomputes UNDER the recording transaction sees its staged view and is applied by the commit (ownership) — the stale recording published the frame twice. recompute drops it at its start; a lane's committed-view read re-records during the run, so the lane replay is untouched. Provenance (#3331 follow-up): a same-value optimistic write by a newer action took the fast path — entangling the transaction — without renewing _overrideStamp, so the OLDER action's slow source superseded a value the user had just re-confirmed: a corrective downstream refetch and a pending flip for nothing. The fast path now renews the stamp to a newer origin. Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never catches up" and "conditional reader stays hidden" shapes, a replacement flight over published inputs, a retired lane), the same-value provenance twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3. Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled for the lane-authority stack with per-scenario notes. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
451f087 to
027fda2
Compare
`laneHeld` looked up a lane's `_pendingAsync` nodes in its own transaction's `_asyncReporters`. Lanes merge across transactions (#2912: ownership never travels through lanes), so after a merge the root's transaction holds the observations of only one member — the async the other member's transaction observed no longer held the merged reveal, and the frame tore. The observation is a fact about the node: `waitingTransition(node)` finds the live transaction blocked on it, whichever recorded it, and `laneHeld` asks per node. Same rule the transaction itself uses (unrendered async and fallback-caught async hold nothing). Pinned in tests/lane-hold-on-observation.test.ts; INTERNALS §2 hold rule. Co-authored-by: Claude via Cursor <noreply@cursor.com>
…ess of stamp (#3334) Two halves of one tear. `read()`'s pending branch let a stale (render) reader of another transaction carve out a node pending in a foreign transaction and show its committed value — on the theory that the stamp meant that transaction also held the node's inputs. It does not: the stamp is pending-node bookkeeping, and the inputs may already be on screen (committed #3305, lane-revealed #3334). A render reader landing on a pending node now throws whichever transaction the node is stamped with (A15: observed async settles as one unit). And `handleAsync`'s settle-time re-entry entered the lane owner's transaction for a lane-routed landing, folding every transaction waiting on the flight into the owner's action — a reveal that discovered the flight then waited on the action instead of on the flight. It enters the waiter: the transaction whose blocker this landing clears (`waitingTransition`). Pinned in spec A15 (#3334 describe) and tests/stale-read-uninitialized-cross-transition.test.ts (re-expected). INTERNALS §3. Co-authored-by: Claude via Cursor <noreply@cursor.com>
…#3330) `recompute`'s equality gate compared a lane (OPT-dirty) recompute against `_pendingValue` — a value an earlier action write had staged for the transaction — while the lane publishes to `_value`. An identical result was called "unchanged", the override revealed without its derivation, and the frame showed `v=1 d=0`. INV-11: the gate compares against the slot this run publishes to — the override for an override-covered node, `_value` for a lane direct commit, `_pendingValue` for a transaction-staged run. Found under the same repro: `laneReadsCommitted` recorded every lane reader of a staged node for commit-time replay, including when the staged value already equals the committed one (the lane published it) — the replay re-ran effects against an unchanged frame. Recorded only when the commit will change what the reader saw. Pinned in spec A17 (#3330 pin and re-apply companion); INTERNALS §2, §5 INV-11. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
… provenance (#3331) A18 supersession. An optimistic override used to stay the graph's value until its transaction committed, even after the node's own source had answered with something else — the override's downstream flight ran to completion and only then did the truth's start, doubling the delay the reporter saw. Now a differing arrival marks the node `CONFIG_OVERRIDE_SUPERSEDED`: tracked readers see the staged truth (`read()` → `_supersededRead`), the node's lane cascade is demoted and notified on the plain channel so downstream async restarts immediately as held transaction work, and the override's own downstream flight is inert when it lands. Untracked reads and the applied frame keep the override until the commit. An equal arrival confirms silently (or un-supersedes); the authoritative-observer wake (until()'s predicate, #3164) lives in the same engine hook. `runEffect` hands a lane runner for a lane-less effect to the regular queue so the still-held transaction stashes it. Scope: "the source" is whatever recomputes the node — its own async landing (asyncWrite) or a sync recompute driven by an upstream change (`createOptimistic(() => asyncMemo())`), both recompute branches. Ordering: a source write and an override in the same batch derive nothing new (`_overrideTime` vs clock). Provenance: only the override's own question or a newer one supersedes. Two rapid actions on one node merge into one transaction, and the older action's refetch can land after the newer override — that answer must not leak back in over the user's latest intent ("getting it not to flicker was super important"). Transactions merge, so `_overrideOwner` cannot tell the actions apart; the scheduler carries the running action's invocation sequence (`origin`: set by action() per slice, cleared at the end of flush(); captured per flight in handleAsync and re-armed by asyncWrite for the landing's propagation), the override stamps it (`_overrideStamp`), and a differing arrival from an older action holds silently to the commit. Mainline (no action) is always current. Pinned: spec A18 (#3331 describe: own-async, sync-wrapper, same-batch, provenance, simple graph; entangled pin re-expected); createOptimistic.test.ts (no-double-flicker pin unchanged; "second action while first still in flight" resolver repaired and re-expected). SPEC A17 amendment and the 2026-09-09 re-ruling log for the whole lane-authority series, INTERNALS §1/§3, treeshake NOTE (22,049 / 22,050, no bump). Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…f a hold re-derive at the commit (#3330, #3331) Every rule pinned for signals in #3335/#3334/#3330/#3331 is pinned against the equivalent optimistic-store shape (tests/store/lane-authority-twins). optimistic families too — a sync derive adopting truth under the action's transaction is held truth, not lane business; unheld, handlers read the swapped-in backing early, latest() inverted, and the optimistic write compared equal to it and wrote no override. `notifyOptimisticWrites` judges the write against the view readers see (`heldMaskView(t) ?? t.v`). A held adoption notifies its nodes at the outermost setter exit (`stageHeldAdoptions`), staged under the transaction, so the commit promotes silently instead of delivering the adopted values as fresh writes that re-ran every subscriber against a frame the lane had already published. Plain-store reconcile inside an action holds the same way; a key first read under a held adoption is born holding (one `stageHeldKey` with the #3336 fold case: committed from the view readers see, the held value staged). (setSignal under projectionWriteActive) dispatches to the engine (`_landOnOverride` → supersedeOverride) instead of staging silently on the plain path; `supersededRead` serves the committed truth once a mainline landing has committed ahead of the override's revert. Core, surfaced by the store twin and reproduced with a plain signal: a reader that first links to a node while another transaction holds a staged write read the committed value and never learned of the commit (commits are silent; the staging walk was the notification). `heldFromStale` records such readers for the transaction's commit replay, skipping effects the transaction itself computed (parked run / contested re-derive) — replaying those published the frame twice. On the A28 write path the async landing's supersession decision runs at the write's promotion (promoteUnflushed's override arm hands the node to supersedeOverride, still under the flight's provenance). A settle that reverts optimism re-derives its contested effects (#3322) after the revert, with the gated replay: ahead of the heap run the truth is committed but the overrides still display, and the re-derive composed the two — the #3164 tear, surfaced by deep() over an optimistic store whose held adoption the committing transaction's own readers see. Core floor 22,252 → 22,638 (conscious). Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…unless its inputs are visible; same-value re-prediction renews provenance (A15 re-rule, #3331) Review on #3347 (GabbeV). Two fixes and a re-ruling. A15 reveal corollary, re-ruled: #3334 removed read()'s pending-branch carve-out outright — a stale (render) reader of a node pending in another transaction always held on the flight. That made a new reader of a memo whose input write is ITSELF still held wait for the landing, though showing the committed pair introduces no inconsistency (parallel transactions; effects don't entangle; React 19.3 stopped entangling the same shape). The carve-out returns, gated on input visibility: the reader shows committed, does not entangle, and is recorded for the transaction's commit replay (heldFromStale) — refused, holding instead, when the committed value would tear against the frame: the flight's inputs were PUBLISHED while it was pending (CONFIG_INPUTS_PUBLISHED, set by commitPendingNode's computed branch when a commit leaves the node in the air, #3305; cleared by notifyStatus when the node next enters pending from a settled state), the node rides a live lane (GlobalQueue._laneLive -> resolveLane, exact rather than sticky; optimistic / latest, #3334), or the node is uninitialized. The initialized-memo pin and its store twin return to "show v0, then v1". Replay hygiene the carve-out surfaced: an effect recorded in _gatedSubs that later recomputes UNDER the recording transaction sees its staged view and is applied by the commit (ownership) — the stale recording published the frame twice. recompute drops it at its start; a lane's committed-view read re-records during the run, so the lane replay is untouched. Provenance (#3331 follow-up): a same-value optimistic write by a newer action took the fast path — entangling the transaction — without renewing _overrideStamp, so the OLDER action's slow source superseded a value the user had just re-confirmed: a corrective downstream refetch and a pending flip for nothing. The fast path now renews the stamp to a newer origin. Pinned: tests/reveal-carve-out.test.ts (GabbeV's "revealed reader never catches up" and "conditional reader stays hidden" shapes, a replacement flight over published inputs, a retired lane), the same-value provenance twin in spec-async-semantics. SPEC A15 + re-ruling log, INTERNALS §3. Core floor 22,638 -> 22,737 (conscious). Size-limit scenarios reconciled for the lane-authority stack with per-scenario notes. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Core floor 22,737 -> 22,816 (+27 B hot-path fix, +52 B A28 for optimistic writes, both from the base branch); brotli caps ratcheted to the measured artifacts on six scenarios. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…bcc71 The affects() declaration walk composing the tick's optimistic writes (one argument) lands +44 B brotli on the store-heavy scenario here; 28.30 -> 28.35 KB, measured at 28344 B. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Core floor 22,816 -> 22,866 (+50 B, `next`'s #3350/#3351 via #3337), budget 22,950. Five brotli caps ratcheted with notes for the same bytes under the lane-authority seams (createStore 15.45, isPending/latest 10.98, simple app 11.32, hydrating+stores 28.50, CSR 14.15 KB). Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
e3b08d2 to
fae8b76
Compare
|
… override supersession with provenance (#3335 #3334 #3330 #3331) (#3370) Four lane-authority fixes plus their optimistic-store twins and a review re-rule, all pre-existing on next. #3347 re-based directly onto next (landing dispatch is eager, as next's is; the store twins carry the held-adoption born-holding case only — #3336's fold case stays with #3337). #3335 — merged lane hold is per node, not per transaction. laneHeld looked up a lane's pending nodes in its own transaction's _asyncReporters; lanes merge across transactions, so a merged reveal lost the other member's async. waitingTransition(node) finds the live transaction blocked on a node, whichever recorded it. #3334 — a reveal holds on the lane flight it discovers, regardless of stamp. read()'s pending branch showed a stale reader the committed value of a node pending in another transaction (the stamp is bookkeeping, not evidence the inputs are held); handleAsync's settle re-entry entered the lane owner's transaction instead of the waiter's. #3330 — a lane recompute compares against the slot it publishes (INV-11). An OPT-dirty recompute compared against a transaction-staged _pendingValue while publishing to _value and called an identical result unchanged, revealing the override without its derivation. laneReadsCommitted records a reader only when the commit changes what it read. #3331 — own-source arrival supersedes the override, with action provenance (A18). A differing arrival marks the node CONFIG_OVERRIDE_SUPERSEDED: tracked readers see the staged truth (_supersededRead), the lane cascade is demoted and re-derives as held transaction work, the override's downstream flight is inert; untracked reads and the applied frame keep the override to the commit. Equal arrivals confirm silently (the authoritative-observer wake lives in the same hook). The scheduler carries the running action's sequence (`origin`) through each slice and the landing's propagation; an answer from an older action holds silently instead of superseding. Store twins: a held adoption under a live transaction holds on optimistic families too and stages its nodes at the outermost setter exit (stageHeldAdoptions); a key first read under a held adoption is born holding (stageHeldKey); notifyOptimisticWrites judges against the view readers see; the authoritative landing on an override-covered node dispatches to the engine (_landOnOverride). heldFromStale records a reader served another transaction's committed value for that transaction's commit replay. A settle that reverts optimism re-derives its contested effects after the revert. A15 reveal corollary re-ruled: the pending-branch carve-out returns, gated on input visibility (CONFIG_INPUTS_PUBLISHED, a live lane, or an uninitialized node refuse it); recompute drops a stale _gatedSubs recording it is about to apply; a same-value re-prediction renews _overrideStamp. Floor 21,994 -> 22,457 (+463 B; budget 22,500); .size-limit.js caps re-measured against next @ 4935c7d. Spec A15/A17/A18 amendments and the 2026-09-09 re-ruling log; INTERNALS-ASYNC §1–§3/§5; INTERNALS-STORE §3. Closes #3335, closes #3334, closes #3330, closes #3331. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Four lane-authority fixes in
@solidjs/signals, one commit per issue, each with its changeset, plus a fifth commit that pins every rule against its optimistic-store twin and a sixth from review (the A15 reveal carve-out re-ruled, a provenance gap). All pre-existing onnext. Stacked on #3337 (latest-held-till-flush): the A28 write path and the #3336 store rules are the base these compose with; the base retargets tonextwhen #3337 merges.#3335 — merged lane hold is per node, not per transaction
laneHeldlooked up a lane's_pendingAsyncnodes in its own transaction's_asyncReporters. Lanes merge across transactions (#2912), so after a merge the root's transaction knew only one member's observations and the other member's async no longer held the merged reveal. NewwaitingTransition(node)finds the live transaction blocked on a node, whichever recorded it;laneHeldasks per node.#3334 — a reveal holds on the lane flight it discovers, regardless of stamp
Two halves of one tear:
read()'s pending branch carved out nodes pending in another transaction for stale readers and showed their committed value (the stamp is bookkeeping, not evidence the inputs are held — they may already be lane-revealed); andhandleAsync's settle re-entry entered the lane owner's transaction, folding the waiting reveal into the owner's still-running action. The reader now throws (A15); the landing enters the waiter.#3330 — a lane recompute compares against the slot it publishes (INV-11)
A lane (OPT-dirty) recompute compared against a transaction-staged
_pendingValuewhile publishing to_value, called an identical result "unchanged", and revealed the override without its derivation (v=1 d=0). The gate now compares against the slot the run publishes to. Found alongside:laneReadsCommittedreplayed lane readers at commit even when the staged value already equalled the committed one — a duplicate frame; recorded only when the commit changes what was read.#3331 — own-source arrival supersedes the override, with action provenance
Per the ruling ("a new value from the source should remove the optimism immediately… folded into the parent transition"): a differing arrival marks the node superseded — tracked readers see the staged truth, the lane cascade is demoted and re-derives as held transaction work (downstream async restarts now, no waterfall), the override's own downstream flight is inert — while untracked reads and the applied frame keep the override until commit. Equal arrivals confirm silently. Scope covers the sync-wrapper shape (
createOptimistic(() => asyncMemo())) — "if the source recomputes it doesn't matter if it is async or not" — with a same-batch ordering guard.Provenance. Two rapid actions on one node merge into one transaction, and the older action's refetch can land after the newer override. That answer must not leak back in ("getting it not to flicker was super important").
_overrideOwneris post-merge and can't tell the actions apart, so the scheduler carries the running action's invocation sequence (origin) through each action slice and the flush that ends its window; every flight captures it at registration and its landing propagates under it. The override stamps it; a differing arrival from an older action holds silently to the commit. Mainline (no action) is always current. The original no-double-flicker pin is unchanged and green; the "second action while first still in flight" pin had its second resolver repaired (it used to orphan action 1, so its final assertions passed against a transaction that never closed) and is expected to the same rule.Store twins (fifth commit)
Every rule above is pinned against the equivalent optimistic-store shape (
tests/store/lane-authority-twins.test.ts). #3335 and #3334 already held; #3330 and #3331 did not:latest()inverted, and the optimistic write compared equal to it and wrote no override;notifyOptimisticWritesnow judges against the view readers see. A held adoption stages its nodes under the transaction at the outermost setter exit (stageHeldAdoptions) so the commit promotes silently instead of re-running every subscriber against a frame the lane already published. Plain-storereconcileinside an action holds the same way; a key first read under a held adoption is born holding (onestageHeldKeyshared with 2.0.0-rc.7 spooky action at a distance because of lazy latest computed creation #3336's fold case).setSignalunderprojectionWriteActive) dispatches to the engine (_landOnOverride→supersedeOverride) instead of staging silently;supersededReadserves committed truth once a mainline landing has committed ahead of the revert.heldFromStalerecords such readers for the transaction's commit replay, skipping effects the transaction itself computed.promoteUnflushed's override arm hands the node tosupersedeOverride, still under the flight's provenance). A settle that reverts optimism re-derives its contested effects (2.0.0-rc.7 render effect depending on non async values from two concurrent updates commit with the wrong data #3322) after the revert, with the gated replay — ahead of the heap run the truth is committed but the overrides still display, and the re-derive composed the two (the 2.0.0-rc until doesnt entangle the update resolving it with the action update #3164 tear; surfaced bydeep()over an optimistic store).Review follow-ups (sixth commit)
From GabbeV's review on this PR:
read()'s pending-branch carve-out outright, so a new reader of a memo whose input write is itself still held waited for the landing — though showing the committed pair introduces no inconsistency (parallel transactions; effects don't entangle; React 19.3 stopped entangling the same shape). The carve-out returns, gated on input visibility: a stale reader of a node pending in another transaction shows committed, does not entangle, and is recorded for that transaction's commit replay (heldFromStale). It is refused — the reader holds — when the committed value would tear against the frame: the flight's inputs were published while it was pending (CONFIG_INPUTS_PUBLISHED, set bycommitPendingNodewhen a commit leaves the node in the air, fix(signals): hold conditional reveals on existing async work #3305; cleared when the node next enters pending fresh), the node rides a live lane (_laneLive→resolveLane; optimistic /latest, 2.0.0-rc.7 Issue 3305 can still happen for optimistic and latest values while fixed for normal signals #3334), or the node is uninitialized. The initialized-memo pin and its store twin return to "showv0, thenv1"; every 2.0.0-rc.7 Issue 3305 can still happen for optimistic and latest values while fixed for normal signals #3334/fix(signals): hold conditional reveals on existing async work #3305 hold pin still passes._gatedSubsthat later recomputes under the recording transaction is applied by the commit itself — the stale recording published the frame twice.recomputedrops it at its start; a lane's committed-view read re-records during the run._overrideStamp, so the older action's slow source superseded a value the user had just re-confirmed (corrective downstream refetch, pending flip). The fast path renews the stamp to a newerorigin.heldFromStalerecording and are now pinned intests/reveal-carve-out.test.ts, alongside a replacement-flight-over-published-inputs shape and a retired-lane shape.Docs / bytes
SPEC A15 lanes corollary, A17 amendment, A18 supersession + scope/ordering/provenance, 2026-09-09 re-ruling log; INTERNALS-ASYNC §1–§3, §5 INV-11, late readers, contested post-revert; INTERNALS-STORE §3 adoption under a live transaction. Core floor: 22,866 / 22,950 (22,737 before the rebase onto #3337's hot-path fix and A28-for-optimistic commits, +79 B from the base) — a conscious bump over #3337's 22,331 (+~160 B lane-authority seams, +126 B store-twin seams (
_landOnOverridedispatch,heldFromStale), +~40 B contested post-revert, +99 B gated carve-out)..size-limit.jsscenarios reconciled for the stack with per-scenario notes (e.g. core floor 8.32 → 8.54 KB brotli; the isPending/latest scenario carries the engine, +450 B; hydrating + every store family 28.30 → 28.35 KB after #3337'saffectswalk fix, +44 B here).Verification
Rebased on #3337 @ 027fda2 (itself on
next@ 4935c7d). #3337'spromoteUnflushedmoved intocore.ts; this branch's override arm (_supersedeOverride) is re-applied there, after the A28(5)_promoteOverridearm for parked optimistic writes. signals 1767 passed / 1 skipped · solid 595 · web 734 ·tscclean · fullpnpm build·npm run sizegreen locally on both branches.