fix(signals): hold conditional reveals on existing async work - #3305
Conversation
🦋 Changeset detectedLatest commit: 98d457f 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 |
29cacdb to
6ea30eb
Compare
…lError branch Same semantics, fewer bytes: the new check and the reporter registration share one actualError test, read the module-level currentBatch instead of this._batch, and the error fallback is `??` (every caller passes nothing or this node's own _x._error, so a null falls back to the same null). Measured against next: signals + createStore 14358 B brotli, under both the cap and the pre-PR baseline; the PR as written was 18 B over on Linux. Co-authored-by: Cursor <cursoragent@cursor.com>
6ea30eb to
98d457f
Compare
|
…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>
…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>
…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>
…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>
…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>
…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>
…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>
…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>
…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>
…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>
…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>
…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>
…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>
Summary
showcommitted asfalsewithisPending(show) === trueuntil the details can reveal with it, without retroactively holding the already committedvalueupdate.@solidjs/signalspatch changeset. No standalone scratch tests.Reproduction
https://s.olid.uk/id/1xogoHinT9uZhdrpGNy7LQ
The show-line indicator in the playground uses
isPending(value); the DOM regression usesisPending(show). The runtime bug remains independently of that typo:show() ? details() : "hidden"discovers the existing flight only after the reveal write.Verification
JSX_COMPILER=babel: 21 passed.git diff --checkpassed.