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

Consolidate value-selection predicates: `readerSeesCommitted` (the full committed-vs-staged arm read()'s slow tail used to inline) and `visibleOverride` / `hasActiveOverride` (one definition each, previously duplicated between the core, lanes, verdict channels and the store) — a zero-semantic-change refactor toward one implementation per rule (DESIGN-CONSOLIDATION, move 3b).
725 changes: 357 additions & 368 deletions packages/signals/docs/RULES-INDEX.md

Large diffs are not rendered by default.

85 changes: 64 additions & 21 deletions packages/signals/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,50 @@ export function enterStagedRead(
globalQueue.initTransition(t);
}

/**
* Rule 1 (value selection), the full arm: does this reader see a STAGED
* node's COMMITTED value? One implementation of the rule the fast paths
* (readNodeFast, read's fast block) carry as their trivial ternary and that
* every slow site — read's tail, the store's backing selection, the lane and
* verdict arms — used to restate by hand (DESIGN-CONSOLIDATION, move 3b). In order:
* - no reader at all (an untracked read) — the committed frame;
* - a reader under an optimistic lane the engine says reads committed
* (laneReadsCommitted: another lane's hold, #3460);
* - nothing staged;
* - a children-forbidden reader (createTrackedEffect / onSettled: the frame,
* never the graph — A32);
* - a stale reader (render effect) of a FOREIGN transaction's staged write —
* committed, no entanglement (heldFromStale registers the replay; a node
* born held has no committed frame to fall back to, `noCommitted`);
* - A17 for HELD truth (#3164, CONFIG_HELD_TRUTH): staged confirming truth —
* fold-staged onto an armed family, or entangle-stolen by an awaited
* until() — is masked from ordinary readers until its transaction's
* reveal, the retaining transaction's own speculative recomputes included
* (partial override coverage would otherwise compose override + staged
* truth into a state no timeline contains). Authoritative readers
* (until()'s predicate) and latest() see the staged truth — the tunnel that
* keeps the hold deadlock-free.
* False means the reader derives from the staged value and enters its
* transaction (enterStagedRead, A29).
*/
export function readerSeesCommitted(
el: Signal<any> | Computed<any>,
c: Computed<any> | null,
owner: Signal<any> | Computed<any>,
noCommitted: boolean
): boolean {
return !!(
!c ||
(currentOptimisticLane !== null && GlobalQueue._laneReadsCommitted!(el, owner, c)) ||
el._pendingValue === NOT_PENDING ||
c._config & CONFIG_CHILDREN_FORBIDDEN ||
(stale && !noCommitted && heldFromStale(el, c)) ||
(el._config & CONFIG_HELD_TRUTH &&
!latestReadActive &&
!(c._config & CONFIG_AUTHORITATIVE_READ))
);
}

/** A28 — set when a node is staged (queuePendingNode) or a held node rewritten
* (stashHeldRewrite) OUTSIDE a flush; cleared when the next flush begins. The
* read sites test this one module boolean instead of `globalQueue._running`:
Expand Down Expand Up @@ -1685,6 +1729,22 @@ export function unflushedOverride(el: Signal<any> | Computed<any>): boolean {
// Companions are optimistic signals written by the engine (see unflushed).
return !globalQueue._running && el._x?._overrideTime === clock && !el._x?._parentSource;
}
/** Active optimistic override on an armed node (an armed slot idles at
* NOT_PENDING; undefined = unarmed plain node). The writer's own channels —
* the draft, `in`/keys inside the setter — compose on this regardless of
* flush state. */
export function hasActiveOverride(el: Signal<any> | Computed<any>): boolean {
const x = el._x;
return x !== null && x._overrideValue !== undefined && x._overrideValue !== NOT_PENDING;
}
/** The override a READER sees: installed, and carried by a flush (A28 (5) —
* an optimistic write is a write; until its flush no reader sees it). One
* implementation for read()'s override arm, the verdict channels
* (latestRead, computePendingState) and the store's selection
* (DESIGN-CONSOLIDATION, move 3b). */
export function visibleOverride(el: Signal<any> | Computed<any>): boolean {
return hasActiveOverride(el) && !unflushedOverride(el);
}
/** A derivation served the committed value because of an unflushed write
* (A28) must run again in the flush that carries it — the late-linker case
* (#3337's reason to defer the walk): it linked after the write walked. */
Expand Down Expand Up @@ -1933,7 +1993,7 @@ export function read<T>(el: Signal<T> | Computed<T>): T {
nodeName: (owner as any)?._name
});

if (el._x?._overrideValue !== undefined && el._x?._overrideValue !== NOT_PENDING) {
if (hasActiveOverride(el)) {
// A17: the override IS the value for every reader — except an authoritative
// reader (until()'s predicate carries CONFIG_AUTHORITATIVE_READ): it must
// observe independently-arriving truth, and serving it the caller's own
Expand Down Expand Up @@ -1997,26 +2057,9 @@ export function read<T>(el: Signal<T> | Computed<T>): T {
if (pendingCheckActive) GlobalQueue._recordFresh!(el, u);
return u as T;
}
const value =
!c ||
(currentOptimisticLane !== null &&
GlobalQueue._laneReadsCommitted!(el, owner, c as Computed<any>)) ||
el._pendingValue === NOT_PENDING ||
c._config & CONFIG_CHILDREN_FORBIDDEN ||
(stale && !noCommitted && heldFromStale(el, c as Computed<any>)) ||
// A17 for HELD truth (#3164, see CONFIG_HELD_TRUTH): staged confirming
// truth — fold-staged onto an armed family, or entangle-stolen by an
// awaited until() — is masked from ordinary readers until its
// transaction's reveal; the retaining transaction's own speculative
// recomputes included (partial override coverage would otherwise
// compose override + staged truth into a state no timeline contains).
// Authoritative readers (until()'s predicate) and latest() see the
// staged truth — the tunnel that keeps the hold deadlock-free.
(el._config & CONFIG_HELD_TRUTH &&
!latestReadActive &&
!((c as Computed<any>)._config & CONFIG_AUTHORITATIVE_READ))
? el._value
: (enterStagedRead(el), el._pendingValue as T);
const value = readerSeesCommitted(el, c as Computed<any> | null, owner, noCommitted)
? el._value
: (enterStagedRead(el), el._pendingValue as T);
// Record that this isPending() probe observed the fresh pending value, so
// the probe doesn't pair "pending" with the new value (#2831).
if (pendingCheckActive) GlobalQueue._recordFresh!(el, value);
Expand Down
11 changes: 2 additions & 9 deletions packages/signals/src/core/lanes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
NOT_PENDING,
REACTIVE_DISPOSED
} from "./constants.js";
import { currentOptimisticLane, ext } from "./core.js";
import { currentOptimisticLane, ext, hasActiveOverride } from "./core.js";
export { hasActiveOverride };
import { enqueueSub } from "./heap.js";
import {
activeTransition,
Expand Down Expand Up @@ -201,14 +202,6 @@ export function resolveTransition(el: Signal<any> | Computed<any>): Transition |
return resolveLane(el)?._transition ?? el._transition;
}

/**
* Check if a node has an active optimistic override.
*/
export function hasActiveOverride(el: Signal<any> | Computed<any>): boolean {
const x = el._x;
return x !== null && x._overrideValue !== undefined && x._overrideValue !== NOT_PENDING;
}

/**
* Assign or merge a lane onto a node. At convergence points (node already has
* a different active lane), merge unless the node has an active override.
Expand Down
11 changes: 4 additions & 7 deletions packages/signals/src/core/verdict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
setSignal,
unflushed,
unflushedCompanions,
unflushedOverride,
visibleOverride,
unflushedValue,
setStrictRead,
stale,
Expand Down Expand Up @@ -292,8 +292,7 @@ function computePendingState(el: Signal<any> | Computed<any>): boolean {
if (
el._config & CONFIG_OVERRIDE_SUPERSEDED &&
el._pendingValue === NOT_PENDING &&
hasActiveOverride(el) &&
!unflushedOverride(el)
visibleOverride(el)
)
return !el._equals || !el._equals(el._value as any, unwrapOverride(el._x?._overrideValue));
// A28 (2): an unflushed write is not yet observable — the verdict answers
Expand All @@ -306,7 +305,7 @@ function computePendingState(el: Signal<any> | Computed<any>): boolean {
// non-final"; an override is one (a node whose first landing was held
// by a reveal it never got to commit, then superseded under its
// override, read false here).
if (hasActiveOverride(el) && !unflushedOverride(el))
if (visibleOverride(el))
return !el._equals || !el._equals(staged as any, unwrapOverride(el._x?._overrideValue));
// A quiet re-ask's held landing still answers the same question: the
// classification survives the landing (asyncWrite) and dies with the
Expand Down Expand Up @@ -503,9 +502,7 @@ function latestRead<T>(el: Signal<T> | Computed<T>): T {
const prevPending = latestReadActive;
setLatestReadActive(false);
const visibleValue = (
hasActiveOverride(el) && !unflushedOverride(el)
? unwrapOverride(el._x?._overrideValue)
: el._value
visibleOverride(el) ? unwrapOverride(el._x?._overrideValue) : el._value
) as T;
// A28: an unflushed write is not the staged value latest() serves. The
// shadow was written at the source's write to mirror it (A8) — consult it
Expand Down
18 changes: 4 additions & 14 deletions packages/signals/src/store/next/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import {
isEqual,
latestReadActive,
stale,
unflushedOverride,
hasActiveOverride,
visibleOverride,
prepareComputed,
read as readNode,
READ_SLOW,
Expand Down Expand Up @@ -1757,18 +1758,7 @@ export function runAuthoritative<T>(fn: () => T): T {
}
}

/** Active optimistic override on an armed node (armed slot idles at
* NOT_PENDING; undefined = unarmed plain node). */
export function hasActiveOverride(node: Signal<any>): boolean {
return node._x?._overrideValue !== undefined && node._x?._overrideValue !== NOT_PENDING;
}
/** The override a READER sees: installed, and carried by a flush (A28 (5) —
* an optimistic write is a write; until its flush no reader sees it). The
* writer's own channels (the draft, `in`/keys inside the setter) compose on
* the installed override regardless — they use hasActiveOverride. */
export function visibleOverride(node: Signal<any>): boolean {
return hasActiveOverride(node) && !unflushedOverride(node);
}
export { hasActiveOverride, visibleOverride };

/** The reading computation is until()'s authoritative-view predicate — same
* source of truth as core read()'s A17 carve-out (`context`, which persists
Expand Down Expand Up @@ -1806,7 +1796,7 @@ function nodeValue(node: Signal<any>, backing: any): any {
// only: staged pending values are authoritative, overrides are the
// caller's optimism.
const v =
!authoritativeServe() && hasActiveOverride(node) && !unflushedOverride(node)
!authoritativeServe() && visibleOverride(node)
? unwrapOverride(node._x?._overrideValue)
: node._pendingValue !== NOT_PENDING &&
(latestReadActive ||
Expand Down
25 changes: 22 additions & 3 deletions scripts/size/.size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,11 @@ module.exports = [
// Hydration claim-path trim (#3513, 2026-09-17): measured at 16,517 B against
// current `next`'s 16,495 (+22). The fixed-shape `_snapshotValue` cleanup
// replaces `delete` with assignment; the pure core floor shrinks by 1 B.
// Shared read predicates (DESIGN-CONSOLIDATION move 3b step 1, 2026-09-17):
// readerSeesCommitted / visibleOverride / one hasActiveOverride. Minified
// signals: core +13 B, +createStore -38 B, full bundle -72 B; brotli on the
// pure-signals fixtures -4 / -29 / -5 B. This scenario's esbuild bundle
// measured at 16,509 B rebased over #3507, against `next`'s 16,517 (-8 B).
limit: "16.55 KB",
modifyEsbuildConfig
},
Expand Down Expand Up @@ -925,7 +930,12 @@ module.exports = [
// hydrating body that marks the snapshot root; a resume window records its
// boundary owner and `sharedConfig.isClaiming` walks `_parent` to it;
// @solidjs/web's isHydrating consults it. 0 B in the signals floor.
limit: "20.05 KB",
// Shared read predicates (DESIGN-CONSOLIDATION move 3b step 1, 2026-09-17):
// readerSeesCommitted / visibleOverride / one hasActiveOverride. Minified
// signals: core +13 B, +createStore -38 B, full bundle -72 B; brotli on the
// pure-signals fixtures -4 / -29 / -5 B. This scenario's esbuild bundle
// measured at 20,054 B rebased over #3507, against `next`'s 20,044 (+10 B).
limit: "20.10 KB",
modifyEsbuildConfig
},
{
Expand Down Expand Up @@ -1229,7 +1239,12 @@ module.exports = [
// 15,170 B before the #3496 rebase (+30 over its base); 0 B minified in
// the signals floor (24,578 flat). Combined with `_parent` mangling:
// 15,250 B; cap ratcheted to the measured output.
limit: "15.25 KB",
// Shared read predicates (DESIGN-CONSOLIDATION move 3b step 1, 2026-09-17):
// readerSeesCommitted / visibleOverride / one hasActiveOverride. Minified
// signals: core +13 B, +createStore -38 B, full bundle -72 B; brotli on the
// pure-signals fixtures -4 / -29 / -5 B. This scenario's esbuild bundle
// measured at 15,251 B rebased over #3507, against `next`'s 15,238 (+13 B).
limit: "15.30 KB",
modifyEsbuildConfig
},
{
Expand Down Expand Up @@ -1472,7 +1487,11 @@ module.exports = [
// consumer (enable, subscribe, formatRerun) and ships none of them. The
// formatters stay in the engine because the `log` option prints through
// them. Ratcheted down to pin the reduction.
limit: "27.25 KB",
// Shared read predicates (DESIGN-CONSOLIDATION move 3b step 1, 2026-09-17):
// measured at 27,256 B rebased over #3507, against `next`'s 27,196 (+60 B).
// The source change is in the signals core; this scenario's attribution
// modules only alter the compressor layout.
limit: "27.30 KB",
modifyEsbuildConfig: observeEsbuildConfig
},
{
Expand Down
Loading