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
5 changes: 5 additions & 0 deletions .changeset/keyed-guess-folds-correction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

A keyed optimistic store's arrangement guess is no longer voided when the source answers with something different while the guess's action is open: the answer folds beneath it until the hold commits, and rows then match by key. Two overlapping actions on one list used to leave the store torn after the first one's echo (a hole at its index, `length` from the guess, a raw target served for a row), and the confirmed row remounted twice at the commit, taking an open editor and its draft with it. Now the screen keeps both guesses whole, nothing remounts, and a row another tab added shows when the write settles. Memos and effects outside the optimistic lane still derive from the answer right away and are held with it, and `latest()` shows it.
5 changes: 5 additions & 0 deletions .changeset/slot-records-join-sweep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/web": patch
---

A live server component's slot record that a commit changes now leaves in the same write as the holes that commit moved, with every arg current. Before, each arg of the occurrence re-sent the record as it updated, so the first copy went out alone with the other args still at their previous values, ahead of the hole. A client fill reading those args could show a state no commit had: the chat's pending message left the outbox before the thread showed its row.
2 changes: 2 additions & 0 deletions packages/signals/docs/RULES-DERIVED.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ An override's lifetime is that of the hold it was made over — in the usual sha

When an override's own source answers (its own landing, or a sync recompute from an upstream change): an equal answer confirms silently — nothing re-runs; a different answer corrects — derivations re-derive from the truth now as the hold's work (downstream refetches restart at once, no waterfall), the screen and untracked reads keep what the screen showed until the commit, `latest()` answers the truth and `isPending()` reads `true` while they differ, and the correction reveals with the hold. "Keep the override" means keep what _showed_: a guess the screen never displayed — its reveal held on its own downstream flight, including through a shown `Loading` — is void at the correction, and untracked reads see the committed value, as the screen does. A later landing equal to the override restores it as the graph's value.

A keyed store's arrangement guess (rows by key) that the screen showed is not voided by a correction: the truth folds beneath it (one the screen never showed is void, as above). Derivations outside the lane re-derive from the truth now and join the hold, `until`/`resolve` keep their postures over it (an `until` outside the lane reads the truth and joins the hold, D-31), and `latest()` shows the hold's world whole. Lane passes, untracked reads and the screen keep the guess until the commit, and at the commit rows match by key. So a row the truth returns keeps its identity, a row another tab added shows at the commit, and the first of two overlapping actions confirming does not void the second's guess (2026-10-21). A folded lane's list pass inside a frame that already has a transaction reads that frame's stagings as the screen, as a verdict reader does, so it never pairs the truth's length with the guess's rows.

**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.

### 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:213` | — | 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:249` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×4 | 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 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 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 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×2 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 treeshake.test.ts×4 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:221` | scheduler.ts×1 | 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 verdict.ts×1 map.ts×1 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×7 scheduler.ts×2 verdict.ts×1 map.ts×1 optimistic.ts×1 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… |
| 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×19 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:149` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | create-deferred-3710.test.ts×2 fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.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 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:408` | — | 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
19 changes: 17 additions & 2 deletions packages/signals/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,13 @@ export function joinPass(c: Computed<any>, el: Signal<any> | Computed<any>): voi
);
}

/** `joinPass` into the hold `t` (a folded guess's: store/optimistic.ts
* `foldedRead`, D-34) — a reader is never a render effect there. */
export function joinPassAt(c: Computed<any>, t: Transaction): void {
c._flags |= REACTIVE_JOINED;
joinPassTx(t, (!(c as any)._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c));
}

/** A15's stale reader (shared-hole and reveal corollaries): a render effect
* reading a node held by a transaction that is not the flush's. It is served
* the committed value — a pending one's too — publishes mainline, and is
Expand Down Expand Up @@ -1810,7 +1817,12 @@ export function read<T>(el: Signal<T> | Computed<T>): T {
if (!c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN)
return el._value as T;
// (A plain signal is never uninitialized — the carve-out is `serve`'s.)
if (observeFlightHook && c._config & CONFIG_VERDICT && stagedScreen(c as Computed<any>))
// Lane work over a folded lane (D-34), once the frame has a
// transaction, reads the screen too.
if (
((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) &&
stagedScreen(c as Computed<any>)
)
return el._value as T;
stagedRead(c as Computed<any>);
return el._pendingValue as T;
Expand Down Expand Up @@ -2028,7 +2040,10 @@ export function serve(el: Signal<any> | Computed<any>, c: Computed<any> | null):

/** A pass read a staging of this flush: it derives from what the flush may
* yet hold (REACTIVE_STAGED_READ — the seam decides). Lane work too (plan sec. 28, a
* lane sees the screen plus its own guesses): the staging is the screen if
* lane sees the screen plus its own guesses) — a folded lane's only before
* the frame has a transaction (after, it reads the screen like a verdict
* reader, `stagedScreen` — a list pass never pairs a held length with the
* guess's rows, D-34): the staging is the screen if
* the frame commits — one pass, the common case — and a held write if it
* parks, which the seam repairs (`stagedReaders`): the pass re-derives on
* the committed world and its lane's runs wait that round, so the held
Expand Down
17 changes: 14 additions & 3 deletions packages/signals/src/core/lanes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,12 +553,21 @@ function dissolveLane(l: Transaction, into: Transaction | null, except?: Signal<
* member throws like any. */
export function laneRead(c: Computed<any> | null, el: Signal<any> | Computed<any>): unknown {
const guess = el._config & CONFIG_GUESS;
const l = txOf(el);
// A lane a correction folded under (D-34): store/optimistic.ts reads it.
// Before the authoritative arm: an `until` outside the lane reads the
// same truth there, and joins the hold it folded into — its posture over
// a foreign hold (D-31), which the truth read alone would not take.
const folded = c !== null && l.folded;
if (folded) {
const v = folded(c, el);
if (v !== NOT_PENDING) return v;
}
if (c !== null && c._config & CONFIG_AUTHORITATIVE) return GlobalQueue._truth!(el);
const status = (el as Computed<any>)._statusFlags;
// An errored member's lane value predates its error; the error is the answer (#4106).
if (!guess && status & STATUS_ERROR) return NOT_PENDING;
if (c === null) return laneValueOf(el);
const l = txOf(el);
if (!tracking || c._config & CONFIG_CHILDREN_FORBIDDEN) {
// An untracked read inside lane work derives from the lane like a
// tracked one, minus the subscription — `untrack` is about
Expand Down Expand Up @@ -733,10 +742,12 @@ function laneWrite<T>(el: Signal<T> | Computed<T>, v: T): T {
// question is the one being asked (`question`: a slice's, a derive's
// continuation's — the flight's); unstamped writes and mainline are
// current.
if (stale(el, question)) {
// A store re-bases a keyed arrangement guess over an older truth, and
// folds a current one beneath it (D-34): true.
const older = stale(el, question);
if (GlobalQueue._laneRebase?.(el, v, older) || older) {
el._pendingValue = v;
el._config |= CONFIG_HELD;
GlobalQueue._laneRebase?.(el, v);
schedule();
return v;
}
Expand Down
21 changes: 17 additions & 4 deletions packages/signals/src/core/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ export interface Transaction {
* leaf reading a held lane that has not shown is a stale reader of it
* (#3460); one reading a lane the seam has not judged yet is its work. */
_held: boolean;
/** Lane: a current truth that differs folded beneath one of its keyed
* guesses (lanes.ts `laneWrite`, D-34) — the store's reader of it: a
* deriving pass outside the lane, or `latest()` (`c` null), is served
* the hold's world from its guesses; NOT_PENDING otherwise
* (store/optimistic.ts `foldedRead`). Unset until then; not
* `_`-prefixed: a new mangled name shifts every later one
* (scripts/mangle-props.mjs), which costs the floors bytes for a path
* only optimistic stores reach. */
folded?: (c: Computed<any> | null, el: Signal<any> | Computed<any>) => unknown;
/** `affects()` marks declared in it (affects.ts): released at the landing.
* Not nodes — a mark holds nothing (`blocked` never sees one). */
_marks: Signal<any>[] | null;
Expand Down Expand Up @@ -807,10 +816,14 @@ export class GlobalQueue implements IQueue {
declare static _storeWrappable: ((v: unknown) => boolean) | undefined;
declare static _mark: ((node: Signal<any> | Computed<any>) => void) | undefined;
declare static _witnessMark: (() => void) | undefined;
/** An older question's truth was held beneath a guess (lanes.ts
* `laneWrite`): the store re-bases an arrangement guess over it
* (store/optimistic.ts). */
declare static _laneRebase: ((el: Signal<any>, truth: unknown) => void) | undefined;
/** A truth landing on a guess (lanes.ts `laneWrite`), `older` when it
* answers an older question: the store re-bases an arrangement guess over
* it; a current one differing from a shown keyed arrangement guess folds
* beneath it instead of voiding it (D-34) — true: the lane stages it
* beneath (store/optimistic.ts). */
declare static _laneRebase:
| ((el: Signal<any>, truth: unknown, older: boolean) => boolean | void)
| undefined;
// Lanes (lanes.ts).
declare static _verdictLane: ((t: Transaction) => Transaction) | undefined;
// Observe tier (attribution.ts): the guesses of `t`'s lanes (lanes.ts).
Expand Down
3 changes: 3 additions & 0 deletions packages/signals/src/core/verdict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ function verdictValue(el: Signal<any> | Computed<any>, c: Computed<any> | null):
(!el._equals || !el._equals(el._x!._lane as any, el._pendingValue as any))))
)
probeFound = true;
// A lane a correction folded under (D-34): `latest()` shows the hold's
// world whole — the truth beneath each guess, else what it covered.
if (latestActive && l.folded) return l.folded(null, el);
return el._x!._lane;
}
if (probing && flying) probeFound = true;
Expand Down
Loading
Loading