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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

An optimistic guess corrected by an outside re-ask now stays the value for effects and untracked reads until its action's own refresh lands (#4690). When a `refresh()` from outside the action landed under a pending guess (`createOptimistic`, or a `createOptimisticStore` keyed, unkeyed or object-shaped), the action's hold stopped waiting on the guessed source's next flight. Where nothing rendered the source, the hold landed with the action's write, and effects and untracked reads showed the old truth until the action's `refresh()` answered, while rendered readers (a render effect, a text node under `<Loading>`, an effect beside one) kept the guess. Rendered output never flickered; effect-only readers disagreed with it. A shown guess that was corrected now keeps its hold open while its own source is in flight, as an uncorrected guess does.
2 changes: 1 addition & 1 deletion packages/signals/docs/RULES-DERIVED.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ A keyed store's arrangement guess (rows by key) that the screen showed is not vo

A new question is not the guess's own source answering. When a guess's node re-asks because an input changed outside the guess's own action (a plain `setSel(1)` while a post that guessed over `sel() = 3` is out), the guess cannot stand beside the new input — that frame would show the new input over the old question's answer and the guess (F-1) — so the frame that changed the input is held with the guess's action (2026-10-28, #4336). The screen keeps the old input and the guess until that action commits, then shows the new input with its answer as one frame, however the reads are grouped into render effects (#4284). Meanwhile `isPending` reads `true` while the new question is in flight or held (F-9), `latest()` answers the newest truth (F-6), and untracked reads see the screen. A `refresh()` re-asks the same question and holds nothing new; the guess's own action changing the input is the correction above. A store's derive is one node asking one question for every key, so the hold is decided where the guess is read: a frame still reading a guess when the flush ends is held with it, and a read of a key with no guess is an ordinary pending read (F-2) — `comments[1]` waits for issue 1's answer, not for a post that guessed on `comments[3]`. An input set through its own optimistic write asks the new question in its own lane: the other action's guess does not answer it, so the lane waits for its own answer (F-6) and then shows it, with the guessed node (for a store, every guessed slot of the target its derive writes) as the lane's derivation from there (F-11); the guess's readers wait with the lane, however the reads are grouped into render effects. The other action's guess was only displaced: when the input's action ends without writing the truth, its lane's world is void, the revert re-asks the old question as the revert's work, and the displaced guess comes back in the revert's frame over that answer — `#1: ` then `#3: dana | mine (sending)`, nothing between — and lives until its own action settles (D-33). If the input's action commits, the displaced guess is gone, as for any correction.

**Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#a18-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-commit) supersession (#3331, 2026-09-09/10) and (d) before the first commit, "Correction display is what showed" (2026-10-05), lane contract 2, lane ruling 5, [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (2026-09-09), OS-R39. **Status:** standing.
**Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#a18-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-commit) supersession (#3331, 2026-09-09/10) and (d) before the first commit, "Correction display is what showed" (2026-10-05), lane contract 2, lane ruling 5, [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (2026-09-09), OS-R39. **Status:** standing. Fixed 2026-11-05 (#4690): a shown guess a `refresh()` from outside its action corrected keeps the action's hold open while its own source is in flight, as an uncorrected guess does (`lanes.ts` `lanesBlocked`). With nothing rendering the source, the hold landed with the action's write, so effects and untracked reads showed the re-ask's answer until the action's own `refresh()` landed while rendered readers kept the guess — a tear between effect-only and rendered readers, not a flicker on screen; `tests/optimistic-outside-reask-4690.test.ts`.

### D-35. Provenance: an older question's answer never corrects a newer guess; the body's end corrects what nothing authoritative is answering

Expand Down
2 changes: 1 addition & 1 deletion packages/signals/docs/RULES-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:215` | — | spec-async-semantics.test.ts×1 | [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 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:251` | async.ts×2 constants.ts×1 core.ts×4 effect.ts×1 lanes.ts×7 scheduler.ts×18 seat.ts×1 verdict.ts×5 store.ts×1 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 create-deferred-3710.test.ts×2 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 hold-recovers-landing-reruns-4469.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 latest-memo-inline-parity.test.ts×1 latest-stale-branch-hold-4661.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 optimistic-lane-seam-loop-4643.test.ts×1 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 show-stale-child-in-action-4137.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 tracked-effect-staged-dep-4606-4607.test.ts×1 treeshake.test.ts×4 unread-derive-midflight-mount-4491.test.ts×1 verdict-memo-flight-no-reask.test.ts×1 verdict-memo-held-branch-4544.test.ts×1 verdict-probe-committed-4648.test.ts×2 verdict-shortcut-pending-source-liveness.test.ts×1 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [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 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:223` | scheduler.ts×1 | caught-reactive-read-diagnostic.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×2 spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 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… |
| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:71` | async.ts×3 constants.ts×1 lanes.ts×7 scheduler.ts×2 seat.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×2 store.ts×3 | create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.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… |
| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:71` | async.ts×3 constants.ts×1 lanes.ts×8 scheduler.ts×2 seat.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×2 store.ts×3 | create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-outside-reask-4690.test.ts×1 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.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:81` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×17 visibility-oracle.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 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:151` | boundaries.ts×1 async.ts×1 constants.ts×1 core.ts×6 lanes.ts×1 scheduler.ts×2 seat.ts×1 types.ts×1 verdict.ts×6 store.ts×2 | create-deferred-3710.test.ts×2 derived-store-first-landing-verdict.test.ts×1 errored-latest-obsolete-flight-4568.test.ts×1 fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 latest-memo-inline-parity.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 verdict-memo-flight-no-reask.test.ts×2 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… |
| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:410` | — | 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… |
Expand Down
31 changes: 23 additions & 8 deletions packages/signals/src/core/lanes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,14 @@ let guessed = false;
/** Guesses another action's lane took over (#4336): the node, its guess,
* the guess's lane, its question, the lane that took it. */
const kept: any[] = [];
/** Lanes a revert dissolved: the guesses they took come back at their
* parent's landing (`endLanes`). */
const reverted = /* @__PURE__ */ new WeakSet<Transaction>();
/** Two kinds of member, one set (a lane is never a node). Lanes a revert
* dissolved: the guesses they took come back at their parent's landing
* (`endLanes`). Shown guesses a correction re-homed into their hold
* (`supersede`, `dissolveLane`): the slot's value for the screen and
* untracked reads until that hold lands (D-34), each keeps it open on its
* own flight as it did while a guess (`lanesBlocked`, #4690); the hold's
* landing clears them (`endLanes`). */
const marked = /* @__PURE__ */ new WeakSet<Transaction | Signal<any>>();
/** Lane `l` takes `n`'s guess (another action's), kept to come back if `l`
* reverts. Its readers saw the guess: they re-derive at the lane's reveal
* (and, a flight's, hold the lane while they would show it). `d`: a guess
Expand Down Expand Up @@ -526,10 +531,10 @@ export function supersede(n: Signal<any> | Computed<any>, value: unknown, change
if (__OBSERVE__ && l._shown && attrHooks !== null)
attrHooks.optimisticReverted(n, n._x!._lane, value, reverting ? "reverted" : "superseded");
dissolveLane(l, parent, n);
if (guessed && reverting) reverted.add(l);
if (guessed && reverting) marked.add(l);
// What the screen showed stays the screen until the parent lands (A18
// (c): display and untracked reads keep the override until the commit).
if (l._shown) n._value = n._x!._lane;
if (l._shown) ((n._value = n._x!._lane), guessed && marked.add(n));
n._config &= ~(CONFIG_OVERRIDE | CONFIG_GUESS);
n._x!._lane = NOT_PENDING;
holdNode(n, parent);
Expand Down Expand Up @@ -651,7 +656,7 @@ function dissolveLane(l: Transaction, into: Transaction | null, except?: Signal<
// re-base) over a truth staged on it is no derivation anything
// re-derives: it lands that truth the same way (D-34, #4642).
if (n._pendingValue === NOT_PENDING) n._pendingValue = covered(n);
if (l._shown) n._value = slot;
if (l._shown) ((n._value = slot), guessed && marked.add(n));
holdNode(n, into);
insertSubs(n);
} else {
Expand Down Expand Up @@ -915,6 +920,7 @@ function laneSeam(l: Transaction, leaked: boolean): void {
/** The parent landed: its lanes end, nested first. (None is blocked: a
* blocked lane blocks its parent.) */
function endLanes(u: Transaction): void {
if (guessed) for (const n of u._nodes) marked.delete(n);
// A revert landing (#4336): the guesses its lane took from another action
// that is still out come back in this frame, over the truth it lands —
// its readers re-derive with the landing (one frame, no bare truth
Expand All @@ -925,7 +931,7 @@ function endLanes(u: Transaction): void {
let [n, v, g, q, t] = kept.splice(i, 5);
// (A store slot's node now: the one taken, or a read's since.)
if (
reverted.has(t) &&
marked.has(t) &&
lanes.includes(g) &&
(!(n._config & CONFIG_SLOT_NODE) || (n = GlobalQueue._slotTake!(n, true)))
) {
Expand All @@ -952,8 +958,17 @@ function endLanes(u: Transaction): void {
* pending work derives from the frame's write through the guess, so the
* frame is not complete without it (A15) — landing it would commit the write
* beside a derivation still showing the old one. A guess whose own flight is
* up likewise: the question was asked in this frame. */
* up likewise: the question was asked in this frame. So does a shown guess
* a correction re-homed into `t` (`marked`): still the screen's and the
* untracked reads' value (F-11, D-34), its flight — the action's `refresh()`
* after an outside re-ask corrected it — is the answer it waits for, as
* before the correction (D-7, D-35). With no render effect pending on that
* flight, `t` landed at the body's end: effects and untracked reads saw the
* re-ask's answer until the refresh landed, while rendered readers kept the
* guess (#4690). Not a lane's: guessed again there, its flight blocks the
* parent, not the lane (A17, `linkBlocked`). */
function lanesBlocked(t: Transaction): boolean {
if (guessed && !t._lane && t._nodes.some(n => marked.has(n) && inFlight(n))) return true;
return (t._lane && linkBlocked(t)) || nestedBlocked(t);
}
/** Parents whose lane revealed this seam with readers to re-derive there
Expand Down
Loading
Loading