Skip to content
Closed
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/store-override-survives-unobserved.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

An optimistic store override now survives its key becoming unobserved. The property node was released with the override on it the moment its last reader left, so an untracked read of the key (`s.n`) returned the committed value while the action was still live; the release now waits for the flush that resolves the override, as an optimistic signal keeps its override whether or not anything reads it.
25 changes: 13 additions & 12 deletions packages/signals/docs/DESIGN-CONSOLIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@

## 0. Ledger (2026-09-17)

| Move | Status | Where | What it found |
| -------------------------------------------------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 3a — `retireReporter`, deps scan at `_depsTail` | **merged** | #3496 and follow-ups (O3, fuzzer P1) | Fuzzer 984 / 4 / 12 → 994 / 0 / 6. |
| 3b step 1 — one `unflushed` | **merged** | #3510 (`CONFIG_ADOPTED_UNFLUSHED`) | Closed S1 / O4 for the verdict channels. |
| 3b step 1 — shared predicates | PR #3515 | `readerSeesCommitted` (read's slow tail), `visibleOverride` / one `hasActiveOverride` (core, lanes, verdict ×3, store ×2) | Pure refactor. Matrix, oracles, fuzzer identical; full bundle −72 B minified. |
| 3b step 2 — store node reads select by Rule 1 | PR #3516 | `nodeValue` → `readerSeesCommitted` behind the two store tunnels (`latest()`, `authoritativeServe`) | **S4:** the store's stale-of-foreign twin served committed but skipped the replay registration — a render effect's untracked read of a held key stayed on the pre-action value forever. Invisible to the suite, both oracles, the 621-cell matrix and the fuzzer. |
| 3b step 3 — backing-level replay | PR #3517 | `recordStaleReplay` (lifted from `heldFromStale`) called by `heldFromReader` and the adoption-hold view | S4's no-node and `reconcile`-inside-an-action forms (four store shapes, all failing on `next`). |
| 3b step 4 — A29 at the store's untracked paths | this PR | `nodeValue`, `pendingBackingVisible`, the adoption-hold view call `enterStagedRead` on the arm that serves the staged value; `enterStagedRead(null, txn)` for a backing | **S5:** a mainline memo's / user effect's untracked read of a held store key **published the unrevealed write** while the same read of a signal was born held. Matrix gained `memoUntracked` / `effectUntracked` reader kinds (621 → 851 cells) — the kind that sees it. |
| 3b — recorded, not ruled | open | `posture-store-parity` S6, S7 | **S6:** staged-ambient write, derivation created inside a foreign action: signal publishes committed (A28 / #3510), store publishes pending. **S7:** optimistic store override, only reader gated away: signal still reads the override, store reads committed. |
| 3b step 5 — `pendingBackingVisible`'s own arms, the store tunnels, `overrideRead` / `latestRead` / lane arms → one `serve` | not started | | Needs the lane ruling (§6). |
| 3c — cargo lifecycle | not started | | Wait for the first new "decided at the pass" fix, or for 3b to settle. |
| Move | Status | Where | What it found |
| -------------------------------------------------------------------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 3a — `retireReporter`, deps scan at `_depsTail` | **merged** | #3496 and follow-ups (O3, fuzzer P1) | Fuzzer 984 / 4 / 12 → 994 / 0 / 6. |
| 3b step 1 — one `unflushed` | **merged** | #3510 (`CONFIG_ADOPTED_UNFLUSHED`) | Closed S1 / O4 for the verdict channels. |
| 3b step 1 — shared predicates | PR #3515 | `readerSeesCommitted` (read's slow tail), `visibleOverride` / one `hasActiveOverride` (core, lanes, verdict ×3, store ×2) | Pure refactor. Matrix, oracles, fuzzer identical; full bundle −72 B minified. |
| 3b step 2 — store node reads select by Rule 1 | PR #3516 | `nodeValue` → `readerSeesCommitted` behind the two store tunnels (`latest()`, `authoritativeServe`) | **S4:** the store's stale-of-foreign twin served committed but skipped the replay registration — a render effect's untracked read of a held key stayed on the pre-action value forever. Invisible to the suite, both oracles, the 621-cell matrix and the fuzzer. |
| 3b step 3 — backing-level replay | PR #3517 | `recordStaleReplay` (lifted from `heldFromStale`) called by `heldFromReader` and the adoption-hold view | S4's no-node and `reconcile`-inside-an-action forms (four store shapes, all failing on `next`). |
| 3b step 4 — A29 at the store's untracked paths | this PR | `nodeValue`, `pendingBackingVisible`, the adoption-hold view call `enterStagedRead` on the arm that serves the staged value; `enterStagedRead(null, txn)` for a backing | **S5:** a mainline memo's / user effect's untracked read of a held store key **published the unrevealed write** while the same read of a signal was born held. Matrix gained `memoUntracked` / `effectUntracked` reader kinds (621 → 851 cells) — the kind that sees it. |
| 3b step 5 — store follows signal: S7 | PR #3521 (on #3520) | **Ruling 2026-09-17: "store rules should follow signal rules."** An optimistic override survives its key becoming unobserved (`deferSlotRelease` — the slot hook released the node with the override on it; the scheduler's transient-node sweep, unfed since the monorepo absorb, releases it when the override resolves). +55 B minified store. | **S7:** optimistic store key read 0 the moment its only reader gated away while the action was live. |
| S6 — ruled, fix deferred to step 6 | open | A28 at the backing: a `setStore` from imperative mainline is invisible to a derivation created inside a foreign action until the carrying flush; the signal already is. Ruled store-follows-signal. Done at the store twin (first cut of #3521) it cost **+402 B minified** — a node born in the unflushed window has to stage the write, and every arm existed twice (core's, the twin's). Pinned at the store's current value in `posture-store-parity` S6; closes when the store's value selection shares core's `serve`. | The byte cost of fixing a rule at its twin before the twin collapses: the argument for step 6 over more twin fixes. |
| 3b step 6 — `pendingBackingVisible`'s own arms, the store tunnels, `overrideRead` / `latestRead` / lane arms → one `serve` | not started | | Needs the lane ruling (§6). |
| 3c — cargo lifecycle | not started | | Wait for the first new "decided at the pass" fix, or for 3b to settle. |

**Method that is working:** the pure-refactor step finds nothing; handing a twin the shared predicate and asking what changed finds the precondition the twin dropped. Every 3b finding so far (S4, S5) was a store site restating a core rule minus one half of it (the replay registration, the transaction entry) — the pattern §1 predicted, in store code the matrix could not see until it had a reader kind for "an untracked read inside a derivation."

**Bytes so far (minified):** step 1 −72 B full bundle; step 3 +31 B core / +123 B store; step 4 +1 B core / +130 B store. The fixes cost bytes; the refactors do not.
**Bytes so far (minified):** step 1 −72 B full bundle; step 3 +31 B core / +123 B store; step 4 +1 B core / +130 B store; step 5 +0 B core / +55 B store (S6 at the twin: +402 B, declined). The fixes cost bytes; the refactors do not — and a fix at a twin costs its rule twice, which is what step 6 exists to stop.

## 1. Why

Expand Down
Loading