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
9 changes: 9 additions & 0 deletions .changeset/fix-optimistic-lane-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@solidjs/signals": patch
---

Optimistic frames release when nothing authoritative is left to wait on, and a shared render effect no longer entangles unrelated updates (#3426, #3427, #3407).

- The last async reader of an optimistic value unmounting mid-action releases the frame (#3426): the lane's hold check prunes dead reporters itself (`sourceObserved`, shared with the settle verdict), instead of waiting for a flight nobody observes to land.
- The action body ending starts the correction (#3427): with the bodies over and no authoritative work in flight — no override node's own source, no held plain load the action asked for — each override's truth supersedes it now and the graph re-derives from it as the transaction's held work, settling when that lands. Before, the settle waited for the obsolete lane-derived flight, flashed the obsolete optimistic frame, then reverted and re-asked. A co-written flag stays through a plain load; optimistic store edits keep the settle-then-revert order.
- A render effect's pass belongs to whatever dirtied it (#3407): a sync `action` write to a signal that shared a hole with a held async (`{b()}:{detailsA()}`) merged into the async's transaction and waited (`0:0 → 2:1`) while the same plain write passed through. `recompute` re-enters a stamped node's transaction for memos only; the landing of a flight enters every transaction waiting on it (`enterWaiting`), which is where the effect re-entry's one legitimate job — completing the reveals that discovered the flight — now lives. Two independent flights read in one hole land at their own times.
13 changes: 7 additions & 6 deletions packages/signals/docs/INTERNALS-ASYNC-STATE.md

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions packages/signals/docs/RULES-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:117` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… |
| A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:125` | async.ts×1 | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … |
| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:133` | — | spec-async-semantics.test.ts×2 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… |
| A15 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:167` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | [ruled 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work is observed by a s… |
| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:167` | async.ts×1 core.ts×3 lanes.ts×1 scheduler.ts×2 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … |
| A16 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:141` | verdict.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 | [ruled 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read uninitialized asy… |
| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:31` | async.ts×3 constants.ts×2 core.ts×7 invariants.ts×3 optimistic.ts×3 scheduler.ts×2 verdict.ts×1 signals.ts×2 optimistic.ts×1 store.ts×3 | optimistic-undefined-override.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×1 treeshake.test.ts×1 until.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… |
| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:41` | async.ts×2 constants.ts×1 core.ts×4 optimistic.ts×3 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… |
| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:31` | async.ts×3 constants.ts×2 core.ts×7 invariants.ts×3 optimistic.ts×5 scheduler.ts×2 verdict.ts×1 signals.ts×2 optimistic.ts×1 store.ts×3 | optimistic-undefined-override.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×1 treeshake.test.ts×1 until.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… |
| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:41` | async.ts×2 constants.ts×1 core.ts×4 optimistic.ts×4 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… |
| A19 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:69` | async.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×3 uninitialized-visibility.test.ts×1 | [ruled 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPending(x)` ≡ the value… |
| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:247` | invariants.ts×1 | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… |
| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:254` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… |
Expand Down Expand Up @@ -102,17 +102,17 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul

| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
| INV-1 | live | `docs/INTERNALS-ASYNC-STATE.md:140` | invariants.ts×2 | — | - **INV-1 (high)** `pendingProbe` is non-null only inside an `isPending()` call |
| INV-2 | live | `docs/INTERNALS-ASYNC-STATE.md:142` | invariants.ts×2 | — | - **INV-2 (high)** A node with an _active_ override (`hasActiveOverride`) is |
| INV-3 | live | `docs/INTERNALS-ASYNC-STATE.md:146` | core.ts×1 invariants.ts×2 lanes.ts×1 scheduler.ts×2 | lane-hold-on-observation.test.ts×1 | - **INV-3 (high)** `_asyncReporters` gains entries only inside |
| INV-4 | live | `docs/INTERNALS-ASYNC-STATE.md:153` | invariants.ts×3 | — | - **INV-4 (medium)** After any of the three write paths completes for node `el` |
| INV-5 | live | `docs/INTERNALS-ASYNC-STATE.md:157` | invariants.ts×2 lanes.ts×1 | — | - **INV-5 (medium)** A lane in `activeLanes` has `_mergedInto === null` |
| INV-6 | live | `docs/INTERNALS-ASYNC-STATE.md:163` | invariants.ts×2 | — | - **INV-6 (medium)** At the end of a completing-transition flush: every node in |
| INV-7 | live | `docs/INTERNALS-ASYNC-STATE.md:166` | core.ts×1 invariants.ts×2 | action-completion-race.test.ts×2 | - **INV-7 (medium)** `_pendingValue !== NOT_PENDING` on a non-optimistic node |
| INV-8 | retired | `docs/INTERNALS-ASYNC-STATE.md:169` | invariants.ts×1 | rules-index.test.ts×1 | - **INV-8 (RETIRED 2026-07-07b, §5e)** Hold-provenance: a `_pendingValue` on an |
| INV-9 | live | `docs/INTERNALS-ASYNC-STATE.md:180` | invariants.ts×1 owner.ts×1 | — | - **INV-9 (high)** An `isPending` companion of a DISPOSED owner reads `false` |
| INV-10 | live | `docs/INTERNALS-ASYNC-STATE.md:185` | invariants.ts×2 | action-done-window.test.ts×1 | - **INV-10 (high)** Affects-count balance (question-scoped model, 2026-07-13; |
| INV-11 | live | `docs/INTERNALS-ASYNC-STATE.md:190` | core.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×1 treeshake.test.ts×1 | - **INV-11 (high, structural — pinned, not asserted)** A recompute's equality |
| INV-1 | live | `docs/INTERNALS-ASYNC-STATE.md:142` | invariants.ts×2 | — | - **INV-1 (high)** `pendingProbe` is non-null only inside an `isPending()` call |
| INV-2 | live | `docs/INTERNALS-ASYNC-STATE.md:144` | invariants.ts×2 | — | - **INV-2 (high)** A node with an _active_ override (`hasActiveOverride`) is |
| INV-3 | live | `docs/INTERNALS-ASYNC-STATE.md:148` | core.ts×1 invariants.ts×2 lanes.ts×1 scheduler.ts×2 | lane-hold-on-observation.test.ts×1 | - **INV-3 (high)** `_asyncReporters` gains entries only inside |
| INV-4 | live | `docs/INTERNALS-ASYNC-STATE.md:155` | invariants.ts×3 | — | - **INV-4 (medium)** After any of the three write paths completes for node `el` |
| INV-5 | live | `docs/INTERNALS-ASYNC-STATE.md:159` | invariants.ts×2 lanes.ts×1 | — | - **INV-5 (medium)** A lane in `activeLanes` has `_mergedInto === null` |
| INV-6 | live | `docs/INTERNALS-ASYNC-STATE.md:165` | invariants.ts×2 | — | - **INV-6 (medium)** At the end of a completing-transition flush: every node in |
| INV-7 | live | `docs/INTERNALS-ASYNC-STATE.md:168` | core.ts×1 invariants.ts×2 | action-completion-race.test.ts×2 | - **INV-7 (medium)** `_pendingValue !== NOT_PENDING` on a non-optimistic node |
| INV-8 | retired | `docs/INTERNALS-ASYNC-STATE.md:171` | invariants.ts×1 | rules-index.test.ts×1 | - **INV-8 (RETIRED 2026-07-07b, §5e)** Hold-provenance: a `_pendingValue` on an |
| INV-9 | live | `docs/INTERNALS-ASYNC-STATE.md:182` | invariants.ts×1 owner.ts×1 | — | - **INV-9 (high)** An `isPending` companion of a DISPOSED owner reads `false` |
| INV-10 | live | `docs/INTERNALS-ASYNC-STATE.md:187` | invariants.ts×2 | action-done-window.test.ts×1 | - **INV-10 (high)** Affects-count balance (question-scoped model, 2026-07-13; |
| INV-11 | live | `docs/INTERNALS-ASYNC-STATE.md:192` | core.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×1 treeshake.test.ts×1 | - **INV-11 (high, structural — pinned, not asserted)** A recompute's equality |
## RUL — store rulings

| id | status | defined | cited in src | cited in tests | statement (at definition) |
Expand Down
Loading
Loading