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: 3 additions & 2 deletions packages/solid-signals/src/core/async.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CONFIG_OPTIMISTIC,
CONFIG_SYNC,
EFFECT_TRACKED,
EFFECT_USER,
Expand Down Expand Up @@ -219,7 +220,7 @@ export function handleAsync<T>(
if (setter) {
setter(value);
if (wasUninitialized) clearStatus(el, true);
} else if (el._overrideValue !== undefined) {
} else if (el._config & CONFIG_OPTIMISTIC) {
// Optimistic node — resting OR covered by an active override — holds
// through the shared pending-node path, exactly like a plain async memo,
// so the commit clears STATUS_UNINITIALIZED (#2806) and elevation to
Expand Down Expand Up @@ -440,7 +441,7 @@ export function notifyStatus(
if (markSourced && el._statusFlags & STATUS_ERROR) return;
const isSource = pendingSource === el;
const isOptimisticBoundary =
status === STATUS_PENDING && el._overrideValue !== undefined && !isSource;
status === STATUS_PENDING && !!(el._config & CONFIG_OPTIMISTIC) && !isSource;
const startsBlocking = isOptimisticBoundary && hasActiveOverride(el);

if (!blockStatus) {
Expand Down
24 changes: 10 additions & 14 deletions packages/solid-signals/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export const CONFIG_IN_SNAPSHOT_SCOPE = 1 << 3;
export const CONFIG_CHILDREN_FORBIDDEN = 1 << 4;
export const CONFIG_AUTO_DISPOSE = 1 << 5;
export const CONFIG_SYNC = 1 << 6;
/**
* Brands a node as optimistic (optimisticSignal/optimisticComputed callers and
* optimistic store leaves). The brand used to live in the `_overrideValue`
* slot itself (`undefined` = not optimistic), which made an optimistic write
* of literal `undefined` erase the node's optimistic identity unless wrapped
* in a sentinel (#2898). As a config bit the brand is write-once static state,
* so `_overrideValue` is a plain two-state slot: `NOT_PENDING` = at rest,
* anything else — including literal `undefined` — is the active override.
*/
export const CONFIG_OPTIMISTIC = 1 << 7;

export const STATUS_NONE = 0;
export const STATUS_PENDING = 1 << 0;
Expand All @@ -40,21 +50,7 @@ export const EFFECT_TRACKED = 3;

export const NOT_PENDING = {};
export const NO_SNAPSHOT = {};
/**
* Stand-in stored in `_overrideValue` for an optimistic write of literal
* `undefined` (#2898). The slot doubles as the optimistic-node brand
* (`undefined` = not optimistic, `NOT_PENDING` = at rest), so the raw value
* would erase the node's optimistic identity: the write turns invisible and
* follow-up writes route off the optimistic path and commit permanently.
* Same shape as NO_SNAPSHOT. Sites that surface the override VALUE unwrap
* via `visibleOverrideValue`; slot identity tests stay raw.
*/
export const OVERRIDE_UNDEFINED = {};

/** Unwrap an active override's stored value for surfacing to readers (#2898). */
export function unwrapOverride<T = any>(v: unknown): T {
return (v === OVERRIDE_UNDEFINED ? undefined : v) as T;
}
export const STORE_SNAPSHOT_PROPS = "sp";

export const SUPPORTS_PROXY = typeof Proxy === "function";
Expand Down
23 changes: 12 additions & 11 deletions packages/solid-signals/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {
EFFECT_USER,
NO_SNAPSHOT,
NOT_PENDING,
OVERRIDE_UNDEFINED,
unwrapOverride,
CONFIG_OPTIMISTIC,
REACTIVE_CHECK,
REACTIVE_DIRTY,
REACTIVE_DISPOSED,
Expand Down Expand Up @@ -194,7 +193,7 @@ export function recompute(el: Computed<any>, create: boolean = false): void {
}

let isOptimisticDirty = !!(el._flags & REACTIVE_OPTIMISTIC_DIRTY);
const hasOverride = el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING;
const hasOverride = !!(el._config & CONFIG_OPTIMISTIC) && el._overrideValue !== NOT_PENDING;
const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
// Re-ask classification lives in the verdict module; capture the flag before
// the recompute wipes _flags below.
Expand Down Expand Up @@ -288,7 +287,7 @@ export function recompute(el: Computed<any>, create: boolean = false): void {
if (!el._error) {
trimStaleDeps(el);
const compareValue = hasOverride
? unwrapOverride(el._overrideValue)
? el._overrideValue
: el._pendingValue === NOT_PENDING
? el._value
: el._pendingValue;
Expand Down Expand Up @@ -333,7 +332,7 @@ export function recompute(el: Computed<any>, create: boolean = false): void {
// own reveal schedule; drop any superseded older hold so its queued
// commit can't clobber the fresh value.
if (hasOverride && isOptimisticDirty) {
el._overrideValue = value === undefined ? OVERRIDE_UNDEFINED : value;
el._overrideValue = value;
el._pendingValue = NOT_PENDING;
}
} else {
Expand Down Expand Up @@ -628,6 +627,7 @@ export function signal<T>(

export function optimisticSignal<T>(v: T, options?: NodeOptions<T>): Signal<T> {
const s = signal(v, options);
s._config |= CONFIG_OPTIMISTIC;
s._overrideValue = NOT_PENDING;
return s;
}
Expand All @@ -637,6 +637,7 @@ export function optimisticComputed<T>(
options?: NodeOptions<T>
): Computed<T> {
const c = computed(fn, options);
c._config |= CONFIG_OPTIMISTIC;
c._overrideValue = NOT_PENDING;
return c;
}
Expand Down Expand Up @@ -739,7 +740,7 @@ export function read<T>(el: Signal<T> | Computed<T>): T {
if (
!computed._fn &&
owner === el &&
el._overrideValue === undefined &&
!(el._config & CONFIG_OPTIMISTIC) &&
el._snapshotValue === undefined &&
activeTransition === null &&
currentOptimisticLane === null &&
Expand Down Expand Up @@ -860,11 +861,11 @@ export function read<T>(el: Signal<T> | Computed<T>): T {
nodeName: (owner as any)?._name
});

if (el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING) {
if (el._config & CONFIG_OPTIMISTIC && el._overrideValue !== NOT_PENDING) {
// An active override means the engine is installed (A17: the override IS
// the value for every reader — that check itself stays right here).
if (c && stale && GlobalQueue._readStashed!(el as Signal<any>)) return el._value as T;
return unwrapOverride<T>(el._overrideValue);
return el._overrideValue as T;
}

// Entanglement gate: a reader recomputing under an optimistic lane that reads
Expand Down Expand Up @@ -936,10 +937,10 @@ export function setSignal<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T
globalQueue.initTransition(el._transition);

// The optimistic write path lives with the engine: only optimisticSignal /
// optimisticComputed callers and optimistic store nodes carry an
// _overrideValue slot, and every module that creates one installs the
// optimisticComputed callers and optimistic store nodes carry the
// CONFIG_OPTIMISTIC brand, and every module that sets it installs the
// engine first.
if (el._overrideValue !== undefined && !projectionWriteActive)
if (el._config & CONFIG_OPTIMISTIC && !projectionWriteActive)
return GlobalQueue._optimisticWrite!(el, v);

const currentValue = el._pendingValue === NOT_PENDING ? el._value : (el._pendingValue as T);
Expand Down
28 changes: 11 additions & 17 deletions packages/solid-signals/src/core/invariants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
CONFIG_OPTIMISTIC,
NOT_PENDING,
unwrapOverride,
REACTIVE_CHECK,
REACTIVE_DIRTY,
REACTIVE_DISPOSED,
Expand Down Expand Up @@ -131,7 +131,7 @@ export function devCheckActiveOverrides(isRegisteredForRevert: (node: AnyNode) =
optimisticNodes.delete(node);
continue;
}
if (node._overrideValue === undefined || node._overrideValue === NOT_PENDING) continue;
if (!(node._config & CONFIG_OPTIMISTIC) || node._overrideValue === NOT_PENDING) continue;
assertInvariant(
isRegisteredForRevert(node),
"INV-2",
Expand Down Expand Up @@ -183,7 +183,7 @@ export function devCensusCompanions(isQueuedForCommit?: (node: AnyNode) => boole
for (const node of companionOwners) {
if (isDisposed(node)) continue;
const comp = node as Computed<any>;
const hasOverride = node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING;
const hasOverride = !!(node._config & CONFIG_OPTIMISTIC) && node._overrideValue !== NOT_PENDING;
const resting = node._overrideValue === NOT_PENDING;
const held = node._pendingValue !== NOT_PENDING;
// A held write already queued in the global commit queue lands on the
Expand All @@ -203,8 +203,8 @@ export function devCensusCompanions(isQueuedForCommit?: (node: AnyNode) => boole
// IS the value), not the raw committed slot — mid-transition the
// verdict legitimately lives in the companion's override.
const cached =
pendingSignal._overrideValue !== undefined && pendingSignal._overrideValue !== NOT_PENDING
? unwrapOverride(pendingSignal._overrideValue)
pendingSignal._config & CONFIG_OPTIMISTIC && pendingSignal._overrideValue !== NOT_PENDING
? pendingSignal._overrideValue
: pendingSignal._value;
const fresh = oracle(node);
if (cached !== fresh) {
Expand All @@ -221,16 +221,12 @@ export function devCensusCompanions(isQueuedForCommit?: (node: AnyNode) => boole
) {
// Latest-view oracle: override if active, else the held in-flight
// value, else the committed value (A17/A20 read order) — on both sides.
const expected = hasOverride
? unwrapOverride(node._overrideValue)
: held
? node._pendingValue
: node._value;
const expected = hasOverride ? node._overrideValue : held ? node._pendingValue : node._value;
const shadowOverride =
shadow._overrideValue !== undefined && shadow._overrideValue !== NOT_PENDING;
!!(shadow._config & CONFIG_OPTIMISTIC) && shadow._overrideValue !== NOT_PENDING;
const shadowHeld = shadow._pendingValue !== NOT_PENDING;
const effective = shadowOverride
? unwrapOverride(shadow._overrideValue)
? shadow._overrideValue
: shadowHeld
? shadow._pendingValue
: shadow._value;
Expand Down Expand Up @@ -284,7 +280,7 @@ export function devCheckQuiescent(isQueuedForCommit: (node: AnyNode) => boolean)
continue;
}
assertInvariant(
node._overrideValue === undefined || node._overrideValue === NOT_PENDING,
!(node._config & CONFIG_OPTIMISTIC) || node._overrideValue === NOT_PENDING,
"INV-6",
"an optimistic override survived to quiescence — resolveOptimisticNodes missed the node (its transition completed without reverting it)"
);
Expand Down Expand Up @@ -325,15 +321,13 @@ export function devCheckQuiescent(isQueuedForCommit: (node: AnyNode) => boolean)
const settled =
!((node as Computed<any>)._statusFlags & STATUS_PENDING) &&
node._pendingValue === NOT_PENDING &&
(node._overrideValue === undefined || node._overrideValue === NOT_PENDING);
(!(node._config & CONFIG_OPTIMISTIC) || node._overrideValue === NOT_PENDING);
if (!settled) continue;

const pendingSignal = node._pendingSignal;
if (pendingSignal) {
assertInvariant(
pendingSignal._value === false &&
(pendingSignal._overrideValue === undefined ||
pendingSignal._overrideValue === NOT_PENDING),
pendingSignal._value === false && pendingSignal._overrideValue === NOT_PENDING,
"INV-4",
"isPending companion reports pending for a fully settled node at quiescence — a clear path skipped updatePendingSignal (#2831 class)"
);
Expand Down
8 changes: 4 additions & 4 deletions packages/solid-signals/src/core/lanes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NOT_PENDING } from "./constants.js";
import { CONFIG_OPTIMISTIC, NOT_PENDING } from "./constants.js";
import { activeTransition, type QueueCallback, type Transition } from "./scheduler.js";
import type { Computed, Signal } from "./types.js";

Expand Down Expand Up @@ -125,16 +125,16 @@ export function resolveTransition(el: {
/**
* Check if a node has an active optimistic override.
*/
export function hasActiveOverride(el: { _overrideValue?: any }): boolean {
return !!(el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING);
export function hasActiveOverride(el: { _overrideValue?: any; _config?: number }): boolean {
return !!(el._config! & CONFIG_OPTIMISTIC && el._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.
*/
export function assignOrMergeLane(
el: { _optimisticLane?: OptimisticLane; _overrideValue?: any },
el: { _optimisticLane?: OptimisticLane; _overrideValue?: any; _config?: number },
sourceLane: OptimisticLane
): void {
const sourceRoot = findLane(sourceLane);
Expand Down
19 changes: 7 additions & 12 deletions packages/solid-signals/src/core/optimistic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* import one of those APIs never retain any of this.
*
* Core call sites fire the hooks behind guards on state only this module can
* create (`_overrideValue !== undefined`, `currentOptimisticLane !== null`,
* create (`_config & CONFIG_OPTIMISTIC`, `currentOptimisticLane !== null`,
* `_optimisticNodes.length`, `activeLanes.size`), so `!` invocations are safe
* once the gate holds — the same late-binding contract as verdict.ts.
*/
Expand All @@ -16,10 +16,9 @@ import {
EFFECT_RENDER,
EFFECT_TRACKED,
EFFECT_USER,
CONFIG_OPTIMISTIC,
NOT_PENDING,
OVERRIDE_UNDEFINED,
REACTIVE_MANUAL_WRITE,
unwrapOverride,
REACTIVE_OPTIMISTIC_DIRTY,
REACTIVE_ZOMBIE,
STATUS_PENDING,
Expand Down Expand Up @@ -61,10 +60,10 @@ type OptimisticNode = Signal<any> | Computed<any>;
// committed-view rerun. Keep that override local to the stash flush.
let stashedOptimisticReads: Set<Signal<any>> | null = null;

/** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */
/** The optimistic half of setSignal, fired when `_config & CONFIG_OPTIMISTIC`. */
function optimisticWrite<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T)): T {
const hasOverride = el._overrideValue !== NOT_PENDING;
const currentValue = hasOverride ? unwrapOverride<T>(el._overrideValue) : el._value;
const currentValue = hasOverride ? (el._overrideValue as T) : el._value;

if (typeof v === "function") v = (v as (prev: T) => T)(currentValue);

Expand All @@ -91,10 +90,7 @@ function optimisticWrite<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T)
const lane = getOrCreateLane(el as Signal<any>);
el._optimisticLane = lane;

// Literal undefined must not land raw: the slot doubles as the optimistic
// brand, and erasing it makes the write invisible and routes follow-up
// writes off the optimistic path into permanent commits (#2898).
el._overrideValue = v === undefined ? (OVERRIDE_UNDEFINED as T) : v;
el._overrideValue = v;
if (__DEV__) devTrackOptimistic(el);

GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, v);
Expand Down Expand Up @@ -176,8 +172,7 @@ function resolveOptimisticNodes(nodes: OptimisticNode[]): void {
(node as any)._statusFlags &= ~STATUS_UNINITIALIZED;
const prevOverride = node._overrideValue;
node._overrideValue = NOT_PENDING;
if (prevOverride !== NOT_PENDING && node._value !== unwrapOverride(prevOverride))
insertSubs(node, true);
if (prevOverride !== NOT_PENDING && node._value !== prevOverride) insertSubs(node, true);
node._transition = null;
}
// Settlement checkpoint (#2838): companions caught in this batch (or owned
Expand Down Expand Up @@ -265,7 +260,7 @@ function gatedRead(el: Signal<any>, owner: OptimisticNode, c: Computed<any>): bo
*/
function laneReadsCommitted(el: OptimisticNode, owner: OptimisticNode, c: Computed<any>): boolean {
return (
el._overrideValue !== undefined ||
!!(el._config & CONFIG_OPTIMISTIC) ||
!!(el as any)._optimisticLane ||
(owner === el && stale && (c as Computed<any>)._parentSource !== el) ||
!!((owner as Computed<any>)._statusFlags & STATUS_PENDING)
Expand Down
3 changes: 2 additions & 1 deletion packages/solid-signals/src/core/scheduler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
CONFIG_IN_SNAPSHOT_SCOPE,
CONFIG_OPTIMISTIC,
EFFECT_RENDER,
EFFECT_TRACKED,
EFFECT_USER,
Expand Down Expand Up @@ -94,7 +95,7 @@ function sweepTransientStoreNodes(): void {
continue;
}
if (node._pendingValue !== NOT_PENDING) continue;
if (node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING) continue;
if (node._config & CONFIG_OPTIMISTIC && node._overrideValue !== NOT_PENDING) continue;
// A live affects() mark keeps the node addressable: sweeping it would
// detach the refcount from the slot (a fresh probe would upsert a new,
// unmarked node for the same property).
Expand Down
14 changes: 5 additions & 9 deletions packages/solid-signals/src/core/verdict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* never import isPending/latest never pay for any of it.
*/
import {
CONFIG_OPTIMISTIC,
NOT_PENDING,
unwrapOverride,
REACTIVE_CHECK,
REACTIVE_DIRTY,
REACTIVE_DISPOSED,
Expand Down Expand Up @@ -123,7 +123,7 @@ function computePendingState(el: Signal<any> | Computed<any>): boolean {
}
if (el._pendingValue !== NOT_PENDING && !(comp._statusFlags & STATUS_UNINITIALIZED)) {
if (hasActiveOverride(el))
return !el._equals || !el._equals(el._pendingValue as any, unwrapOverride(el._overrideValue));
return !el._equals || !el._equals(el._pendingValue as any, el._overrideValue as any);
return true;
}
return newQuestionInFlight(comp);
Expand Down Expand Up @@ -171,7 +171,7 @@ function repollDownstreamVerdicts(el: Computed<any>): void {

function snapCompanionsToState(owner: Signal<any> | Computed<any>): void {
const sig = owner._pendingSignal;
if (sig && (sig._overrideValue === undefined || sig._overrideValue === NOT_PENDING)) {
if (sig && sig._overrideValue === NOT_PENDING) {
const pending = computePendingState(owner);
if (sig._value !== pending || sig._pendingValue !== NOT_PENDING) {
sig._value = pending;
Expand All @@ -184,7 +184,7 @@ function snapCompanionsToState(owner: Signal<any> | Computed<any>): void {
const shadow = owner._latestValueComputed;
if (shadow && !(shadow._flags & REACTIVE_DISPOSED)) {
if (
(shadow._overrideValue === undefined || shadow._overrideValue === NOT_PENDING) &&
shadow._overrideValue === NOT_PENDING &&
shadow._pendingValue === NOT_PENDING &&
!Object.is(shadow._value, owner._value) &&
!(shadow._flags & (REACTIVE_DIRTY | REACTIVE_CHECK))
Expand Down Expand Up @@ -221,11 +221,7 @@ function latestRead<T>(el: Signal<T> | Computed<T>): T {
const pendingComputed = getLatestValueComputed(el);
const prevPending = latestReadActive;
setLatestReadActive(false);
const visibleValue = (
el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING
? unwrapOverride(el._overrideValue)
: el._value
) as T;
const visibleValue = (hasActiveOverride(el) ? el._overrideValue : el._value) as T;
let value: T;
try {
value = read(pendingComputed);
Expand Down
Loading
Loading