@solidjs/signals
Advanced tools
| import { forEachDependent, notifyStatus, addPendingSource, setPendingError, settlePendingSource } from "./core/async.js"; | ||
| import { $REFRESH, STATUS_PENDING } from "./core/constants.js"; | ||
| import { NotReadyError } from "./core/error.js"; | ||
| import "./core/core.js"; | ||
| import { globalQueue, schedule, GlobalQueue, shiftAffectsMarks } from "./core/scheduler.js"; | ||
| import "./core/verdict.js"; | ||
| import "./core/effect.js"; | ||
| import "./core/invariants.js"; | ||
| import { $TARGET, getStoreAffectsNodes } from "./store/store.js"; | ||
| /** | ||
| * The pending-source identity of a live `affects()` mark on `node` (lazy, | ||
| * one per node, shared by overlapping registrations via the refcount). | ||
| * | ||
| * A mark rides the SAME status rails as real in-flight async — downstream | ||
| * subscribers hold the sentinel in `_pendingSources` — but under its own | ||
| * identity so the two channels can't clear each other: | ||
| * - `_reask` is permanently `false`: a mark is by definition a declared | ||
| * value change, so `quietPending` never silences a window it participates | ||
| * in — even when the mark rides over an otherwise-quiet `refresh()` | ||
| * re-ask of the same node (the whole point of declaring one). | ||
| * - A landing on the marked node settles only the node's OWN source entry; | ||
| * the sentinel entry survives until the mark's transaction releases it. | ||
| * - The sentinel itself never carries `STATUS_PENDING`, so | ||
| * `transitionComplete` never counts a mark as a blocker of its own | ||
| * transaction (release happens AT settle — self-blocking would deadlock), | ||
| * and reads of the marked node never throw (marks are value-transparent | ||
| * at the source; pendingness is what propagates). | ||
| */ function getAffectsSentinel(e) { | ||
| return e.t ??= { | ||
| o: undefined, | ||
| // Brand + backref: lets the scheduler's settlement checks recognize | ||
| // mark-sourced pending (which must never block its own transaction — | ||
| // release happens AT settle). | ||
| l: e, | ||
| u: 0, | ||
| i: 0, | ||
| A: false, | ||
| k: undefined, | ||
| p: null, | ||
| S: null | ||
| }; | ||
| } | ||
| /** | ||
| * Push a live mark's pendingness downstream from the marked node through the | ||
| * normal status rails. Runs on every registration (dedup in `notifyStatus` | ||
| * stops re-descent at already-covered subscribers). Subscribers that | ||
| * recompute mid-window shed this via `clearStatus` and re-acquire it through | ||
| * the read path (`applyAffectsReads` for direct readers of the marked node, | ||
| * `collectMarkSources` transitively) — the mark analogue of real async's | ||
| * re-throw-on-read. Subscribers are deliberately NOT queued as pending nodes | ||
| * (#2893): they hold no value needing a transition-scheduled commit, and | ||
| * queueing would stamp the marking action's transaction onto them — from then | ||
| * on ANY write dirtying one of them (including writes to unmarked signals | ||
| * that merely share a downstream memo) would be captured and frozen until the | ||
| * action settles. | ||
| */ function propagateAffectsMark(e) { | ||
| if (!e.p && !e.G) return; | ||
| const t = getAffectsSentinel(e); | ||
| const r = new NotReadyError(t); | ||
| forEachDependent(e, e => { | ||
| if (e.m !== t && !e.M?.has(t)) { | ||
| notifyStatus(e, STATUS_PENDING, r); | ||
| } | ||
| }); | ||
| } | ||
| /** | ||
| * Re-establish mark pendingness on a computed that read marked sources | ||
| * during its recompute (`clearStatus` at the top of the commit path wiped | ||
| * any sentinel entries it held). Called by `recompute` after the commit — | ||
| * not before, because setting `_error` earlier would make the commit path | ||
| * treat the node as errored and skip the value write. | ||
| */ function applyAffectsReads(e, t) { | ||
| let r = false; | ||
| for (let f = 0; f < t.length; f++) { | ||
| const o = t[f]; | ||
| if (!o.P) continue; | ||
| const s = getAffectsSentinel(o); | ||
| if (addPendingSource(e, s)) { | ||
| e.i |= STATUS_PENDING; | ||
| setPendingError(e, s); | ||
| r = true; | ||
| } | ||
| } | ||
| if (r && GlobalQueue._ !== null) GlobalQueue._(e); | ||
| } | ||
| /** | ||
| * The counting half of a mark, shared by direct registration and store-scope | ||
| * inheritance (a node created inside a live keyless mark's identity scope): | ||
| * bumps the refcount and pokes the node's verdict companions so an | ||
| * already-materialized `false` flips reactively. | ||
| */ function markAffects(e) { | ||
| e.P = (e.P || 0) + 1; | ||
| shiftAffectsMarks(1); | ||
| // Companions only exist once the verdict layer (isPending/latest) loaded; | ||
| // without them there is no materialized verdict to flip. | ||
| if (e.P === 1 && GlobalQueue._ !== null) GlobalQueue._(e); | ||
| } | ||
| /** | ||
| * Registers one `affects()` mark on a node: counts it, records the | ||
| * registration with the current transaction (after initTransition the queue's | ||
| * array aliases the active transition's, mirroring `_optimisticNodes`), and | ||
| * propagates STATUS_PENDING downstream on the status rails so everything | ||
| * DERIVED from the marked data reads pending too. Propagation runs on every | ||
| * registration (not just the first): subscribers gained since an earlier | ||
| * overlapping registration get covered, and dedup stops re-descent early. | ||
| */ function registerAffectsMark(e) { | ||
| markAffects(e); | ||
| globalQueue.N.push(e); | ||
| propagateAffectsMark(e); | ||
| schedule(); | ||
| } | ||
| /** | ||
| * Releases one registration. When the node's last mark drops, settles the | ||
| * mark's sentinel out of every downstream `_pendingSources` (waking blocked | ||
| * nodes and re-deriving verdicts along the walk). Companion writes go through | ||
| * the settlement snap (committed, not transition-scoped) so releasing a mark | ||
| * can't open a fresh override window that would itself need settlement. | ||
| */ function releaseAffectsMark(e) { | ||
| shiftAffectsMarks(-1); | ||
| e.P--; | ||
| if (!e.P) { | ||
| const t = e.t; | ||
| if (t) settlePendingSource(e, t, true); | ||
| GlobalQueue.R !== null && GlobalQueue.R(e); | ||
| GlobalQueue.T?.(e); | ||
| } | ||
| } | ||
| /** | ||
| * Releases one batch of affects marks (a settling transaction's, or the | ||
| * ambient batch at a plain flush end). | ||
| */ function releaseAffectsMarks(e) { | ||
| for (let t = 0; t < e.length; t++) releaseAffectsMark(e[t]); | ||
| e.length = 0; | ||
| } | ||
| /** | ||
| * True when a node's pending status comes ONLY from affects() sentinels. A | ||
| * mark is a promise of change, not an absence of value: reads of mark-pended | ||
| * derived nodes stay value-transparent (verdicts report pending; the read | ||
| * path must not suspend). Any real async source among the pending sources | ||
| * keeps normal suspension semantics. Core's read path reaches this through | ||
| * `GlobalQueue._onlyMarkPending`, gated on the `activeAffectsMarks` counter. | ||
| */ function onlyMarkPending(e) { | ||
| const t = e.M; | ||
| if (t) { | ||
| for (const e of t) if (!e.l) return false; | ||
| return true; | ||
| } | ||
| return !!e.m?.l; | ||
| } | ||
| /** | ||
| * Collect the still-live marked nodes behind a pended owner's sentinel | ||
| * sources into a recompute's `affectsReads`. This is the transitive half of | ||
| * read-path re-establishment (#2893): real async re-establishes at every | ||
| * derivation level because its re-throw on read re-registers the source, but | ||
| * mark-pended reads are value-transparent — without this, pendingness dies on | ||
| * the first mid-window recompute past depth one, and the isPending() probe | ||
| * itself (whose prepare step recomputes retryable NotReady holders) strips | ||
| * the very status it reports on. Reached through | ||
| * `GlobalQueue._collectMarkSources`, gated on `activeAffectsMarks`. | ||
| */ function collectMarkSources(e, t) { | ||
| const r = e.m; | ||
| if (r) { | ||
| const e = r.l; | ||
| if (e && e.P) t.push(e); | ||
| } else if (e.M) { | ||
| for (const r of e.M) { | ||
| const e = r.l; | ||
| if (e && e.P) t.push(e); | ||
| } | ||
| } | ||
| } | ||
| // Late installation (same pattern as `GlobalQueue._update`): the mark engine | ||
| // lives with the feature so graphs that never declare a mark never ship it. | ||
| // Each call site is gated by state only this module creates (`markedReads` | ||
| // collection under `activeAffectsMarks`, a non-empty `_affectsNodes` batch, | ||
| // a live scope in the store's `affectsScopes`), so the hooks are installed | ||
| // before the first time any of them can fire. | ||
| GlobalQueue.j = applyAffectsReads; | ||
| GlobalQueue.h = releaseAffectsMarks; | ||
| GlobalQueue.D = markAffects; | ||
| GlobalQueue.F = releaseAffectsMark; | ||
| GlobalQueue.$ = onlyMarkPending; | ||
| GlobalQueue.I = collectMarkSources; | ||
| function affects(e, t) { | ||
| const r = e?.[$TARGET]; | ||
| if (r) { | ||
| const e = getStoreAffectsNodes(r, t); | ||
| for (let t = 0; t < e.length; t++) registerAffectsMark(e[t]); | ||
| return; | ||
| } | ||
| const f = e?.[$REFRESH]; | ||
| if (f) { | ||
| registerAffectsMark(f); | ||
| return; | ||
| } | ||
| } | ||
| export { affects }; |
| import { runWithOwner, signal, untrack, read, computed, setSignal, recompute } from "./core/core.js"; | ||
| import { NotReadyError, unwrapStatusError } from "./core/error.js"; | ||
| import { createOwner, cleanup } from "./core/owner.js"; | ||
| import { Queue, haltReactivity, schedule } from "./core/scheduler.js"; | ||
| import { getContext, setContext, createContext } from "./core/context.js"; | ||
| import { STATUS_PENDING, STATUS_ERROR, REACTIVE_DISPOSED, CONFIG_AUTO_DISPOSE } from "./core/constants.js"; | ||
| import "./core/invariants.js"; | ||
| import "./core/verdict.js"; | ||
| import "./core/effect.js"; | ||
| import { accessor } from "./signals.js"; | ||
| function boundaryComputed(e, t) { | ||
| const r = computed(e, { | ||
| lazy: true | ||
| }); | ||
| r.C = (e, t) => { | ||
| // Use passed values if provided, otherwise read from node | ||
| const n = e !== undefined ? e : r.i; | ||
| const s = t !== undefined ? t : r.k; | ||
| // Notify both status dimensions like a render effect does; the queue chain | ||
| // consumes this boundary's own type and forwards the remainder upward until | ||
| // a boundary that handles it is found. | ||
| r.i &= ~r.O; | ||
| const i = r.v.notify(r, STATUS_PENDING | STATUS_ERROR, n, s); | ||
| // The queue is the only propagation channel: a foreign status must not stay | ||
| // reader-visible on the tree, or reads re-throw it across the boundary and | ||
| // link unrelated ambient contexts (the #2809 nested-boundary loop). Deps are | ||
| // untouched, so the tree still recomputes when the foreign source settles. | ||
| const o = n & ~r.O & (STATUS_PENDING | STATUS_ERROR); | ||
| if (o) { | ||
| r.i &= ~o; | ||
| if (r.k === s && !(r.i & (STATUS_PENDING | STATUS_ERROR))) r.k = undefined; | ||
| } | ||
| // An ERROR the chain could not deliver to any boundary is uncaught. The | ||
| // scrub above already removed it from reader-visible state, so without | ||
| // escalation here it would vanish entirely (#2884) — halt-and-throw, | ||
| // exactly like an unhandled effect error. | ||
| if (!i && n & STATUS_ERROR) { | ||
| haltReactivity(unwrapStatusError(s)); | ||
| throw s; | ||
| } | ||
| }; | ||
| r.O = t; | ||
| r.U &= ~CONFIG_AUTO_DISPOSE; | ||
| recompute(r, true); | ||
| return r; | ||
| } | ||
| function createBoundChildren(e, t, r, n) { | ||
| const s = e.v; | ||
| s.addChild(e.v = r); | ||
| cleanup(() => s.removeChild(e.v)); | ||
| return runWithOwner(e, () => { | ||
| const e = computed(t); | ||
| return boundaryComputed(() => flatten(read(e)), n); | ||
| }); | ||
| } | ||
| const ON_INIT = Symbol(); | ||
| const RevealControllerContext = /* @__PURE__ */ createContext(null); | ||
| let _revealUsed = false; | ||
| const FALSE_ACCESSOR = () => false; | ||
| const SEQUENTIAL_ACCESSOR = () => "sequential"; | ||
| function isRevealController(e) { | ||
| return e instanceof RevealController; | ||
| } | ||
| function isSlotReady(e) { | ||
| return isRevealController(e) ? e.B() : e.W.size === 0 && !e.L; | ||
| } | ||
| function isSlotMinimallyReady(e) { | ||
| return isRevealController(e) ? e.q() : isSlotReady(e); | ||
| } | ||
| function setSlotState(e, t, r, n) { | ||
| setSignal(e.V, r); | ||
| setSignal(e.H, n); | ||
| if (isRevealController(e)) { | ||
| if (!r && e.J === t) e.J = undefined; | ||
| return e.K(r, n); | ||
| } | ||
| if (!r && e.X === t && e.Y) e.X = undefined; | ||
| } | ||
| class RevealController { | ||
| Z; | ||
| ee; | ||
| te=[]; | ||
| J; | ||
| V=signal(false, { | ||
| ownedWrite: true, | ||
| re: true | ||
| }); | ||
| H=signal(false, { | ||
| ownedWrite: true, | ||
| re: true | ||
| }); | ||
| ne=true; | ||
| se=true; | ||
| ie=false; | ||
| constructor(e, t) { | ||
| this.Z = e; | ||
| this.ee = t; | ||
| } | ||
| oe(e) { | ||
| for (let t = 0; t < this.te.length; t++) { | ||
| const r = this.te[t]; | ||
| if ((isRevealController(r) ? r.J : r.X) !== this) continue; | ||
| if (e(r) === false) return false; | ||
| } | ||
| return true; | ||
| } | ||
| B() { | ||
| return this.oe(isSlotReady); | ||
| } | ||
| /** | ||
| * "Minimally ready" = this group has something visible to show under its own policy. | ||
| * Used by an enclosing `together` group to decide when it can release. | ||
| * - `together`: every direct slot is minimally ready. | ||
| * - `sequential`: the first owned slot is minimally ready (frontier can advance). | ||
| * - `natural`: any owned slot is minimally ready. | ||
| */ q() { | ||
| const e = untrack(this.Z); | ||
| if (e === "together") return this.oe(isSlotMinimallyReady); | ||
| if (e === "natural") { | ||
| let e = false; | ||
| let t = false; | ||
| this.oe(r => { | ||
| e = true; | ||
| if (isSlotMinimallyReady(r)) { | ||
| t = true; | ||
| return false; | ||
| } | ||
| }); | ||
| return !e || t; | ||
| } | ||
| // sequential: only the first owned slot matters. | ||
| let t = true; | ||
| this.oe(e => { | ||
| t = isSlotMinimallyReady(e); | ||
| return false; | ||
| }); | ||
| return t; | ||
| } | ||
| le(e) { | ||
| if (this.te.includes(e)) return; | ||
| this.te.push(e); | ||
| const t = untrack(this.Z); | ||
| setSignal(e.V, true), setSignal(e.H, t === "sequential" ? !!untrack(this.ee) : false); | ||
| untrack(() => this.K()); | ||
| } | ||
| ae(e) { | ||
| const t = this.te.indexOf(e); | ||
| if (t >= 0) this.te.splice(t, 1); | ||
| untrack(() => this.K()); | ||
| } | ||
| K(e, t) { | ||
| if (this.ie) return; | ||
| this.ie = true; | ||
| const r = this.ne; | ||
| const n = this.se; | ||
| try { | ||
| const r = e ?? read(this.V), n = untrack(this.Z), s = n === "sequential" && !!untrack(this.ee), i = t ?? s; | ||
| if (r) { | ||
| // Held by an outer group. Propagate the hold (and whatever collapsed policy | ||
| // the outer asked for) down the whole subtree. Inner order is ignored while | ||
| // held; it resumes once the outer releases us. | ||
| this.oe(e => setSlotState(e, this, true, i)); | ||
| } else if (n === "natural") { | ||
| // Each child reveals based on its own readiness. A nested controller slot | ||
| // is released to run its own order locally — we bypass setSlotState for it | ||
| // so the parent backpointer survives for upward readiness notifications. | ||
| this.oe(e => { | ||
| if (isRevealController(e)) { | ||
| setSignal(e.H, false); | ||
| setSignal(e.V, false); | ||
| e.K(false, false); | ||
| } else { | ||
| setSlotState(e, this, !isSlotReady(e), false); | ||
| } | ||
| }); | ||
| } else if (n === "together") { | ||
| // Release when every direct slot is minimally ready (has something to show | ||
| // under its own order). A fully-ready inner together is minimally ready; | ||
| // sequential's first slot being ready is minimally ready; natural having any | ||
| // ready child is minimally ready. This lets `together` guarantee a single | ||
| // cohesive reveal without waiting for every grandchild. | ||
| const e = this.oe(isSlotMinimallyReady); | ||
| this.oe(t => setSlotState(t, this, !e, false)); | ||
| } else { | ||
| let e = false; | ||
| this.oe(t => { | ||
| if (e) return setSlotState(t, this, true, s); | ||
| if (isSlotReady(t)) return setSlotState(t, this, false, false); | ||
| e = true; | ||
| // Frontier slot. For a leaf, holding `_disabled=true` is what keeps its | ||
| // fallback visible. For a composite, we instead release it so it runs | ||
| // its own order locally — its leaves will each show their own fallback | ||
| // until their data lands. Outer still waits on full readiness before | ||
| // advancing past this slot, and we bypass setSlotState so the parent | ||
| // backpointer survives for upward readiness notifications. | ||
| if (isRevealController(t)) { | ||
| setSignal(t.H, false); | ||
| setSignal(t.V, false); | ||
| t.K(false, false); | ||
| } else { | ||
| setSlotState(t, this, true, false); | ||
| } | ||
| }); | ||
| } | ||
| } finally { | ||
| this.ne = this.B(); | ||
| this.se = this.q(); | ||
| this.ie = false; | ||
| } | ||
| if (this.J && (r !== this.ne || n !== this.se)) this.J.K(); | ||
| } | ||
| } | ||
| class CollectionQueue extends Queue { | ||
| ue; | ||
| W=new Set; | ||
| ce; | ||
| L=true; | ||
| V=signal(false, { | ||
| ownedWrite: true, | ||
| re: true | ||
| }); | ||
| k; | ||
| H=signal(false, { | ||
| ownedWrite: true, | ||
| re: true | ||
| }); | ||
| X; | ||
| Y=false; | ||
| fe; | ||
| he=ON_INIT; | ||
| constructor(e) { | ||
| super(); | ||
| this.ue = e; | ||
| } | ||
| run(e) { | ||
| if (!e || read(this.V) && (!_revealUsed || read(this.H))) return; | ||
| return super.run(e); | ||
| } | ||
| notify(e, t, r, n) { | ||
| if (!(t & this.ue)) return super.notify(e, t, r, n); | ||
| if (this.Y && this.fe) { | ||
| const e = untrack(() => { | ||
| try { | ||
| return this.fe(); | ||
| } catch { | ||
| return ON_INIT; | ||
| } | ||
| }); | ||
| if (e !== this.he) { | ||
| this.he = e; | ||
| this.Y = false; | ||
| this.W.clear(); | ||
| } | ||
| } | ||
| // Routing is dimension-independent: each boundary consumes only its own | ||
| // status dimension from the mask (`type &= ~collectionType` below) and | ||
| // forwards the remainder up the queue chain. An error inside a `Loading` | ||
| // needs no special rule — the ERROR dimension survives consumption here and | ||
| // reaches the `Errored` that catches it natively, and `flags & collectionType` | ||
| // keeps this boundary from collecting a node that isn't actually pending. | ||
| // Symmetrically, a pending inside an `Errored` forwards on the PENDING | ||
| // dimension, while a status already caught by an inner boundary arrives with | ||
| // its dimension consumed from the mask and is correctly not re-routed | ||
| // (the Loading > Errored > content composition escape, #2856). | ||
| if (this.ue & STATUS_PENDING && this.Y) return super.notify(e, t, r, n); | ||
| if (r & this.ue) { | ||
| this.L = true; | ||
| const t = n?.source || e.k?.source; | ||
| if (t) { | ||
| const e = this.W.size === 0; | ||
| this.W.add(t); | ||
| if (e) setSignal(this.V, true); | ||
| if (this.ue & STATUS_ERROR) { | ||
| setSignal(this.k, unwrapStatusError(t.k)); | ||
| } | ||
| } | ||
| } | ||
| t &= ~this.ue; | ||
| return t ? super.notify(e, t, r, n) : true; | ||
| } | ||
| Se() { | ||
| for (const e of this.W) { | ||
| if (e.u & REACTIVE_DISPOSED || !(e.i & this.ue) && !(this.ue & STATUS_ERROR && e.i & STATUS_PENDING)) this.W.delete(e); | ||
| } | ||
| if (!this.W.size) { | ||
| if (this.ue & STATUS_PENDING && this.L && !this.Y && this.ce) { | ||
| this.L = !!(this.ce.i & this.ue); | ||
| } else { | ||
| this.L = false; | ||
| } | ||
| if (!this.L) { | ||
| setSignal(this.V, false); | ||
| if (this.fe) { | ||
| try { | ||
| this.he = untrack(() => this.fe()); | ||
| } catch { | ||
| /* value not yet committed — _prevOn stays stale, next notify will reset */} | ||
| } | ||
| } | ||
| } | ||
| if (_revealUsed) this.X?.K(); | ||
| } | ||
| } | ||
| function createCollectionBoundary(e, t, r, n) { | ||
| const s = createOwner(); | ||
| if (_revealUsed) setContext(RevealControllerContext, null, s); | ||
| const i = new CollectionQueue(e); | ||
| if (e === STATUS_ERROR) i.k = signal(undefined, { | ||
| ownedWrite: true, | ||
| re: true | ||
| }); | ||
| if (n) i.fe = n; | ||
| const o = i.ce = createBoundChildren(s, t, i, e); | ||
| // Prime source tracking so reveal registration sees pending sources. | ||
| untrack(() => { | ||
| let t = false; | ||
| try { | ||
| read(o); | ||
| } catch (e) { | ||
| if (e instanceof NotReadyError) t = true; else throw e; | ||
| } | ||
| i.L = t || !!(o.i & e) || o.k instanceof NotReadyError; | ||
| }); | ||
| const l = _revealUsed && e === STATUS_PENDING ? getContext(RevealControllerContext) : null; | ||
| if (l) { | ||
| i.X = l; | ||
| l.le(i); | ||
| cleanup(() => l.ae(i)); | ||
| } | ||
| return accessor(computed(() => { | ||
| if (!read(i.V)) { | ||
| const e = read(o); | ||
| if (!untrack(() => read(i.V))) return i.Y = true, e; | ||
| } | ||
| // Collapsed reveal slots suppress their own output entirely; the | ||
| // renderer treats the hole as empty, so the cast never leaks to users | ||
| // outside a `createRevealOrder` scope. | ||
| if (_revealUsed && read(i.H)) return undefined; | ||
| return r(i); | ||
| }, | ||
| // Boundary structure, not a user source: its value is fallback-or-content and | ||
| // legitimately swaps mid-hydration (reveal/resume), so it must never be frozen | ||
| // by snapshot capture. The tree no longer carries foreign status flags, so | ||
| // capture can't rely on PENDING to skip this node the way it used to. | ||
| { | ||
| re: true | ||
| })); | ||
| } | ||
| /** | ||
| * Lower-level primitive that backs the `<Loading>` flow control. Catches | ||
| * pending async reads inside `fn` and renders `fallback` until they settle. | ||
| * | ||
| * App code should use `<Loading fallback={...}>` instead — reach for this only | ||
| * when authoring custom boundary components. | ||
| * | ||
| * @param fn the tracked subtree | ||
| * @param fallback the fallback shown while async reads in `fn` are unresolved | ||
| * @param options `on` — accessor whose value scopes the boundary; when set, | ||
| * transitions caused by writes to other reactive sources are *not* caught | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // Custom boundary component built on top of the primitive. | ||
| * function MyLoading(props: { fallback: JSX.Element; children: JSX.Element }) { | ||
| * return createLoadingBoundary( | ||
| * () => props.children, | ||
| * () => props.fallback | ||
| * ) as unknown as JSX.Element; | ||
| * } | ||
| * ``` | ||
| */ function createLoadingBoundary(e, t, r) { | ||
| return createCollectionBoundary(STATUS_PENDING, e, () => t(), r?.on); | ||
| } | ||
| /** | ||
| * Lower-level primitive that backs the `<Errored>` flow control. Catches | ||
| * thrown errors inside `fn` and invokes `fallback(error, reset)` instead. | ||
| * `error` is an accessor for the latest captured error; `reset()` recomputes | ||
| * the failing sources so the boundary can attempt to recover. | ||
| * | ||
| * App code should use `<Errored fallback={...}>` instead — reach for this only | ||
| * when authoring custom boundary components. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // Custom boundary that wraps the primitive and adds telemetry. | ||
| * function TracedErrored(props: { fallback: (e: () => unknown) => JSX.Element; children: JSX.Element }) { | ||
| * return createErrorBoundary( | ||
| * () => props.children, | ||
| * (err, reset) => { | ||
| * reportError(err()); | ||
| * return props.fallback(err); | ||
| * } | ||
| * ) as unknown as JSX.Element; | ||
| * } | ||
| * ``` | ||
| */ function createErrorBoundary(e, t) { | ||
| return createCollectionBoundary(STATUS_ERROR, e, e => t(accessor(e.k), () => { | ||
| for (const t of e.W) recompute(t); | ||
| schedule(); | ||
| })); | ||
| } | ||
| /** | ||
| * Coordinate the reveal timing of sibling loading boundaries. | ||
| * | ||
| * Accepts reactive accessors: | ||
| * - `order`: `"sequential"` (default) | `"together"` | `"natural"`. | ||
| * - `"sequential"` — classic frontier reveal: siblings reveal in registration order | ||
| * as each resolves; later siblings stay hidden until earlier ones complete. | ||
| * - `"together"` — every direct slot stays on its fallback until the whole group | ||
| * is "minimally ready" (each direct slot has produced its own first visible | ||
| * content under its own order), then the whole group releases at once. | ||
| * - `"natural"` — children reveal independently (as each resolves). At the top | ||
| * level this is a no-op compared to not using `createRevealOrder`; the mode | ||
| * exists for nesting, where the group registers as a single composite slot to | ||
| * any enclosing `createRevealOrder`. | ||
| * - `collapsed`: only meaningful when `order === "sequential"`. When set, tail siblings | ||
| * past the frontier suppress their own fallback output. Ignored under `"together"` | ||
| * and `"natural"` — those orders have no frontier. | ||
| * | ||
| * Nested `createRevealOrder` groups compose: the inner controller registers as a | ||
| * single slot in the outer controller and is held on its fallbacks until the outer | ||
| * releases that slot. Once released, the inner controller runs its own order locally | ||
| * over anything still pending. There is no opt-out from an outer hold. | ||
| * | ||
| * "Minimally ready" is what an order considers its first visible content: | ||
| * - `sequential` — frontier-0 is minimally ready (leaf: on resolve; nested: via its | ||
| * own minimal signal). | ||
| * - `together` — every direct slot is minimally ready. | ||
| * - `natural` — any direct slot has visible content (leaves on resolve; nested | ||
| * composites via their own minimal signal). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Primitive form of `<Reveal>` — coordinate sibling loading boundaries | ||
| * // programmatically. App code uses the JSX `<Reveal>` component instead. | ||
| * // Both options are accessors so they can react to state changes. | ||
| * createRevealOrder( | ||
| * () => renderSiblings(), | ||
| * { order: () => mode(), collapsed: () => true } | ||
| * ); | ||
| * ``` | ||
| */ function createRevealOrder(e, t) { | ||
| _revealUsed = true; | ||
| const r = createOwner(); | ||
| const n = getContext(RevealControllerContext); | ||
| const s = t?.order || SEQUENTIAL_ACCESSOR, i = t?.collapsed || FALSE_ACCESSOR; | ||
| const o = new RevealController(s, i); | ||
| setContext(RevealControllerContext, o, r); | ||
| return runWithOwner(r, () => { | ||
| const t = e(); | ||
| computed(() => { | ||
| s(); | ||
| i(); | ||
| o.K(); | ||
| }); | ||
| if (n) { | ||
| o.J = n; | ||
| n.le(o); | ||
| cleanup(() => n.ae(o)); | ||
| } | ||
| return t; | ||
| }); | ||
| } | ||
| /** | ||
| * Resolves a children value to its renderable form: unwraps zero-arg functions | ||
| * (accessors), recursively flattens arrays, and optionally skips | ||
| * non-rendering values (`null`, `undefined`, `true`, `false`, `""`). | ||
| * | ||
| * Used internally by flow components and by the renderer to walk a children | ||
| * tree. App code rarely needs this directly — see `children()` in `solid-js` | ||
| * for the user-facing helper that memoizes the result. | ||
| * | ||
| * @param children value or array of values to flatten | ||
| * @param options | ||
| * - `skipNonRendered` — drop values that won't render | ||
| * - `doNotUnwrap` — leave function children as-is (caller will resolve) | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Custom renderer walking a children tree manually. Most authors should | ||
| * // use `children()` from solid-js, which memoizes the resolved value. | ||
| * function renderChildren(value: unknown): unknown { | ||
| * return flatten(value, { skipNonRendered: true }); | ||
| * } | ||
| * ``` | ||
| */ function flatten(e, t) { | ||
| if (typeof e === "function" && !e.length) { | ||
| if (t?.doNotUnwrap) return e; | ||
| do { | ||
| e = e(); | ||
| } while (typeof e === "function" && !e.length); | ||
| } | ||
| if (t?.skipNonRendered && (e == null || e === true || e === false || e === "")) return; | ||
| if (Array.isArray(e)) { | ||
| let r = []; | ||
| if (flattenArray(e, r, t)) { | ||
| return () => { | ||
| let e = []; | ||
| flattenArray(r, e, { | ||
| ...t, | ||
| doNotUnwrap: false | ||
| }); | ||
| return e; | ||
| }; | ||
| } | ||
| return r; | ||
| } | ||
| return e; | ||
| } | ||
| function flattenArray(e, t = [], r) { | ||
| let n = null; | ||
| let s = false; | ||
| for (let i = 0; i < e.length; i++) { | ||
| try { | ||
| let n = e[i]; | ||
| if (typeof n === "function" && !n.length) { | ||
| if (r?.doNotUnwrap) { | ||
| t.push(n); | ||
| s = true; | ||
| continue; | ||
| } | ||
| do { | ||
| n = n(); | ||
| } while (typeof n === "function" && !n.length); | ||
| } | ||
| if (Array.isArray(n)) { | ||
| s = flattenArray(n, t, r); | ||
| } else if (r?.skipNonRendered && (n == null || n === true || n === false || n === "")) { | ||
| // skip | ||
| } else t.push(n); | ||
| } catch (e) { | ||
| if (!(e instanceof NotReadyError)) throw e; | ||
| n = e; | ||
| } | ||
| } | ||
| if (n) throw n; | ||
| return s; | ||
| } | ||
| export { CollectionQueue, RevealController, createErrorBoundary, createLoadingBoundary, createRevealOrder, flatten }; |
| import { globalQueue, activeTransition, currentTransition, setActiveTransition, schedule, flush } from "./scheduler.js"; | ||
| import { isThenable } from "./async.js"; | ||
| import "./core.js"; | ||
| function restoreTransition(e, n) { | ||
| globalQueue.initTransition(e); | ||
| const t = n(); | ||
| flush(); | ||
| return t; | ||
| } | ||
| /** | ||
| * Wraps a generator function so each invocation runs as a single transaction | ||
| * (a "transition") that batches every signal/store write between yields. The | ||
| * surrounding UI sees one atomic update per yielded step; nothing is committed | ||
| * until the action either completes or the next `yield` resolves. | ||
| * | ||
| * Yield promises (or any awaitable) inside the generator — the action waits | ||
| * for each before continuing, but the writes you made beforehand are already | ||
| * visible (or held by `<Loading>` if optimistic). Yield bare values for | ||
| * synchronous batched steps. | ||
| * | ||
| * Each call returns a `Promise` that resolves with the generator's return | ||
| * value, or rejects if it throws. Pair with `createOptimistic` / | ||
| * `createOptimisticStore` to apply tentative writes that auto-revert if the | ||
| * action fails. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const [todos, setTodos] = createOptimisticStore<Todo[]>([]); | ||
| * | ||
| * const addTodo = action(function* (text: string) { | ||
| * const tempId = crypto.randomUUID(); | ||
| * setTodos(t => { t.push({ id: tempId, text, pending: true }); }); // optimistic | ||
| * const saved = yield api.createTodo(text); // network round-trip | ||
| * setTodos(t => { | ||
| * const i = t.findIndex(x => x.id === tempId); | ||
| * if (i >= 0) t[i] = saved; | ||
| * }); | ||
| * return saved; | ||
| * }); | ||
| * | ||
| * await addTodo("buy milk"); | ||
| * ``` | ||
| */ function action(e) { | ||
| return (...n) => new Promise((t, r) => { | ||
| const i = e(...n); | ||
| globalQueue.initTransition(); | ||
| let o = activeTransition; | ||
| o.Te.push(i); | ||
| const done = (e, n, s = false) => { | ||
| o = currentTransition(o); | ||
| const u = o.Te.indexOf(i); | ||
| if (u >= 0) o.Te.splice(u, 1); | ||
| setActiveTransition(o); | ||
| schedule(); | ||
| s ? r(n) : t(e); | ||
| }; | ||
| const step = (e, n) => { | ||
| let t; | ||
| try { | ||
| t = n ? i.throw(e) : i.next(e); | ||
| } catch (e) { | ||
| return done(undefined, e, true); | ||
| } | ||
| // A rejected iterator result (async generators) means the error already | ||
| // escaped the generator body — it is completed, and throwing back in | ||
| // would just reject again forever. Settle instead. | ||
| if (isThenable(t)) return void t.then(run, e => done(undefined, e, true)); | ||
| run(t); | ||
| }; | ||
| const run = e => { | ||
| if (e.done) return done(e.value); | ||
| if (isThenable(e.value)) return void e.value.then(e => restoreTransition(o, () => step(e)), e => restoreTransition(o, () => step(e, true))); | ||
| restoreTransition(o, () => step(e.value)); | ||
| }; | ||
| step(); | ||
| }); | ||
| } | ||
| export { action }; |
| import { STATUS_UNINITIALIZED, STATUS_ERROR, STATUS_PENDING, REACTIVE_DIRTY, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING } from "./constants.js"; | ||
| import { untrack, context, setSignal } from "./core.js"; | ||
| import "./invariants.js"; | ||
| import { NotReadyError, StatusError } from "./error.js"; | ||
| import { trimStaleDeps } from "./graph.js"; | ||
| import { enqueueSub } from "./heap.js"; | ||
| import { resolveTransition, hasActiveOverride, assignOrMergeLane, resolveLane } from "./lanes.js"; | ||
| import { cleanup } from "./owner.js"; | ||
| import { globalQueue, GlobalQueue, clock, schedule, queuePendingNode, flush, insertSubs } from "./scheduler.js"; | ||
| function addPendingSource(e, n) { | ||
| if (e.m === n || e.M?.has(n)) return false; | ||
| // Once the Set exists it is THE container — the singular slot stays empty | ||
| // from migration until removePendingSource collapses back to one entry. | ||
| // Landing a third source in the singular slot instead created dual state | ||
| // that removePendingSource refused to clear, stranding the Set members' | ||
| // pending forever (#2893). | ||
| if (e.M) e.M.add(n); else if (!e.m) e.m = n; else { | ||
| e.M = new Set([ e.m, n ]); | ||
| e.m = undefined; | ||
| } | ||
| return true; | ||
| } | ||
| function removePendingSource(e, n) { | ||
| if (e.m) { | ||
| if (e.m !== n) return false; | ||
| e.m = undefined; | ||
| return true; | ||
| } | ||
| if (!e.M?.delete(n)) return false; | ||
| if (e.M.size === 1) { | ||
| e.m = e.M.values().next().value; | ||
| e.M = undefined; | ||
| } else if (e.M.size === 0) { | ||
| e.M = undefined; | ||
| } | ||
| return true; | ||
| } | ||
| function clearPendingSources(e) { | ||
| e.m = undefined; | ||
| e.M?.clear(); | ||
| e.M = undefined; | ||
| } | ||
| function setPendingError(e, n, r) { | ||
| if (!n) { | ||
| e.k = null; | ||
| return; | ||
| } | ||
| if (r instanceof NotReadyError && r.source === n) { | ||
| e.k = r; | ||
| return; | ||
| } | ||
| const t = e.k; | ||
| if (!(t instanceof NotReadyError) || t.source !== n) { | ||
| e.k = new NotReadyError(n); | ||
| } | ||
| } | ||
| function forEachDependent(e, n) { | ||
| for (let r = e.p; r !== null; r = r.de) n(r.Ee, r); | ||
| // `?? null`: affects() marks route plain signals (no `_child` slot) through here. | ||
| for (let r = e.G ?? null; r !== null; r = r.Ne) { | ||
| for (let e = r.p; e !== null; e = e.de) n(e.Ee, e); | ||
| } | ||
| } | ||
| // Queue a node to re-run on the next flush (used both when a pending source | ||
| // settles and when an `isPending` observer must re-evaluate after a real error): | ||
| // shared scheduling helper in heap.ts (tracked effects bypass the heap). | ||
| function settlePendingSource(e, n = e, | ||
| // Mark release runs inside queue finalization: companion writes must go | ||
| // through the settlement snap (committed), because a setSignal here would | ||
| // open a fresh transition-scoped override window that nothing reverts. | ||
| r = false) { | ||
| let t = false; | ||
| const u = new Set; | ||
| // Companion updates no-op without the verdict layer (null hooks). | ||
| const o = r ? GlobalQueue.R : GlobalQueue._; | ||
| const settle = e => { | ||
| if (u.has(e) || !removePendingSource(e, n)) return; | ||
| u.add(e); | ||
| e.Ie = clock; | ||
| const r = e.m ?? e.M?.values().next().value; | ||
| if (r) { | ||
| setPendingError(e, r); | ||
| o !== null && o(e); | ||
| } else { | ||
| e.i &= ~STATUS_PENDING; | ||
| setPendingError(e); | ||
| o !== null && o(e); | ||
| if (e.Re) { | ||
| enqueueSub(e); | ||
| t = true; | ||
| } | ||
| e.Re = false; | ||
| } | ||
| forEachDependent(e, settle); | ||
| }; | ||
| forEachDependent(e, settle); | ||
| if (t) schedule(); | ||
| } | ||
| // Object-thenable detection (Promises/A+ shape). | ||
| function isThenable(e) { | ||
| return e != null && typeof e === "object" && typeof e.then === "function"; | ||
| } | ||
| function handleAsync(e, n, r) { | ||
| let t = false; | ||
| let u = false; | ||
| if (typeof n === "object" && n !== null) { | ||
| untrack(() => { | ||
| t = n[Symbol.asyncIterator]; | ||
| u = !t && isThenable(n); | ||
| }); | ||
| } | ||
| if (!u && !t) { | ||
| e.Ae = null; | ||
| return n; | ||
| } | ||
| e.Ae = n; | ||
| let o; | ||
| const handleError = r => { | ||
| if (e.Ae !== n) return; | ||
| globalQueue.initTransition(resolveTransition(e)); | ||
| // NotReadyError from rejected promises should be treated as pending, not error | ||
| notifyStatus(e, r instanceof NotReadyError ? STATUS_PENDING : STATUS_ERROR, r); | ||
| e.Ie = clock; | ||
| }; | ||
| const asyncWrite = (t, u) => { | ||
| if (e.Ae !== n) return; | ||
| // If the node was dirtied by a newer write (optimistic override or regular), | ||
| // skip this stale async result — the upcoming flush will recompute the node | ||
| // with the new value, creating a fresh Promise that supersedes this one. | ||
| if (e.u & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return; | ||
| globalQueue.initTransition(resolveTransition(e)); | ||
| const o = !!(e.i & STATUS_UNINITIALIZED); | ||
| trimStaleDeps(e); | ||
| clearStatus(e); | ||
| const l = resolveLane(e); | ||
| if (l) l.Pe.delete(e); | ||
| if (r) { | ||
| r(t); | ||
| if (o) clearStatus(e, true); | ||
| } else if (e.be !== undefined) { | ||
| // 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 | ||
| // _value happens on this value's OWN transition schedule (A18 as | ||
| // re-ruled 2026-07-07: _value only changes at commit points). With an | ||
| // override active the hold and its eventual commit are unobservable | ||
| // (A17 — every reader sees the override); the revert reveals whatever | ||
| // has committed by then, so corrections reveal atomically with their | ||
| // transition rather than escaping it. | ||
| if (e.ge === NOT_PENDING) queuePendingNode(e); | ||
| e.ge = t; | ||
| // The hold is a companion-visible write like any other (A13/A19): the | ||
| // clearStatus() above computed its verdict before the hold existed, so | ||
| // isPending must re-derive (the value is not final until commit — V1) | ||
| // and latest() must see the fresh in-flight value (V2). Subscribers are | ||
| // only notified when the hold is visible to them: under an active | ||
| // override every reader sees the override (A17), so waking subs would | ||
| // re-show an unchanged view — the revert is the notification point. | ||
| GlobalQueue._e !== null && GlobalQueue._e(e, t); | ||
| if (!hasActiveOverride(e)) insertSubs(e); | ||
| e.Ie = clock; | ||
| } else if (l) { | ||
| // Route through lane's effect queue for independent flushing | ||
| const n = e.De; | ||
| const r = e.Ue; | ||
| const u = e.Ge; | ||
| try { | ||
| if (!n && o || !u || !u(t, r)) { | ||
| e.Ue = t; | ||
| e.Ie = clock; | ||
| // The latest() shadow write gives latest() effects independent lanes; the | ||
| // _pendingSignal update is a no-op repeat of the clearStatus() call above | ||
| // (computePendingState doesn't read _value). | ||
| GlobalQueue._e !== null && GlobalQueue._e(e, t); | ||
| insertSubs(e, true); | ||
| } | ||
| } catch (n) { | ||
| // A user comparator throwing during async resolution has no caller to | ||
| // surface to (we're in promise machinery) — route it through the node's | ||
| // error status so boundaries contain it instead of an unhandled | ||
| // rejection (#2837). | ||
| notifyStatus(e, STATUS_ERROR, n); | ||
| } | ||
| } else { | ||
| try { | ||
| setSignal(e, () => t); | ||
| } catch (n) { | ||
| // Same containment as above: setSignal's comparator throw is the only | ||
| // pre-commit failure here, and there is no user callsite to throw to. | ||
| notifyStatus(e, STATUS_ERROR, n); | ||
| } | ||
| } | ||
| settlePendingSource(e); | ||
| schedule(); | ||
| flush(); | ||
| u?.(); | ||
| }; | ||
| if (u) { | ||
| let r = false, t = false, u, l = true; | ||
| n.then(e => { | ||
| if (l) { | ||
| o = e; | ||
| r = true; | ||
| } else asyncWrite(e); | ||
| }, e => { | ||
| if (l) { | ||
| u = e; | ||
| t = true; | ||
| } else handleError(e); | ||
| }); | ||
| l = false; | ||
| if (t) { | ||
| // Settle through the same status path an async rejection uses, then | ||
| // unwind the in-progress synchronous read so the errored node isn't | ||
| // momentarily read as `undefined`. | ||
| handleError(u); | ||
| throw u; | ||
| } else if (!r) { | ||
| globalQueue.initTransition(resolveTransition(e)); | ||
| throw new NotReadyError(context); | ||
| } | ||
| } | ||
| if (t) { | ||
| const r = n[Symbol.asyncIterator](); | ||
| let t = false; | ||
| let u = false; | ||
| let l = true; | ||
| cleanup(() => { | ||
| if (u) return; | ||
| u = true; | ||
| try { | ||
| const e = r.return?.(); | ||
| if (isThenable(e)) e.then(undefined, () => {}); | ||
| } catch {} | ||
| }); | ||
| const iterate = () => { | ||
| let i, s, f = false, a = false, c = true; | ||
| r.next().then(r => { | ||
| if (c) { | ||
| i = r; | ||
| f = true; | ||
| if (r.done) u = true; | ||
| } else if (e.Ae !== n) { | ||
| return; | ||
| } else if (!r.done) { | ||
| t = true; | ||
| asyncWrite(r.value, iterate); | ||
| } else { | ||
| u = true; | ||
| if (t) { | ||
| schedule(); | ||
| flush(); | ||
| } else { | ||
| // Empty completion settles like the immediately-done sync path. | ||
| asyncWrite(undefined); | ||
| } | ||
| } | ||
| }, r => { | ||
| if (c) { | ||
| s = r; | ||
| a = true; | ||
| } else if (e.Ae === n) { | ||
| u = true; | ||
| handleError(r); | ||
| } | ||
| }); | ||
| c = false; | ||
| if (a) { | ||
| // Match the promise branch, but only rethrow during the initial read. | ||
| u = true; | ||
| handleError(s); | ||
| if (l) throw s; | ||
| return true; | ||
| } | ||
| if (f && !i.done) { | ||
| o = i.value; | ||
| t = true; | ||
| return iterate(); | ||
| } | ||
| return f && i.done; | ||
| }; | ||
| const i = iterate(); | ||
| // Later iterate() calls run from asyncWrite, where rethrowing would be unhandled. | ||
| l = false; | ||
| if (!t && !i) { | ||
| globalQueue.initTransition(resolveTransition(e)); | ||
| throw new NotReadyError(context); | ||
| } | ||
| } | ||
| return o; | ||
| } | ||
| function clearStatus(e, n = false) { | ||
| if (e.m || e.M) clearPendingSources(e); | ||
| if (e.Re) e.Re = false; | ||
| // The pending window is over; its quiet classification dies with it. | ||
| // (Unconditional: _reask is baked into the node literals, so this is a | ||
| // plain store to an existing slot — no shape change.) | ||
| e.A = false; | ||
| e.i = n ? 0 : e.i & STATUS_UNINITIALIZED; | ||
| if (e.k) setPendingError(e); | ||
| // Update pending signal for isPending() reactivity (companions only exist | ||
| // once the verdict layer created them, which installs the hooks). | ||
| if (e.ye || e.pe) GlobalQueue._(e); | ||
| if (e.G && GlobalQueue.me !== null) GlobalQueue.me(e); | ||
| if (e.C) e.C(); | ||
| } | ||
| function notifyStatus(e, n, r, t, u) { | ||
| // Wrap regular errors to track source node | ||
| if (n === STATUS_ERROR && !(r instanceof StatusError) && !(r instanceof NotReadyError)) r = new StatusError(e, r); | ||
| const o = n === STATUS_PENDING && r instanceof NotReadyError ? r.source : undefined; | ||
| // Mark-sourced propagation must not capture subscribers into the marking | ||
| // action's transaction (#2893): they carry no held value needing a | ||
| // transition-scheduled commit, and stamping `_transition` on them would | ||
| // freeze unrelated writes that share a downstream memo until the action | ||
| // settles. Real async keeps queuing (its commits ride the transition). | ||
| const l = o?.l !== undefined; | ||
| // A real error is a settled verdict a mark must not erase (#2893): landing | ||
| // STATUS_PENDING here would clobber `_error` with the sentinel's | ||
| // NotReadyError, and unlike real async there is no arriving value whose | ||
| // recompute would surface the error again. Descent stops too — everything | ||
| // downstream holds the propagated error for the same reason. | ||
| if (l && e.i & STATUS_ERROR) return; | ||
| const i = o === e; | ||
| const s = n === STATUS_PENDING && e.be !== undefined && !i; | ||
| const f = s && hasActiveOverride(e); | ||
| if (!t) { | ||
| if (n === STATUS_PENDING && o) { | ||
| addPendingSource(e, o); | ||
| e.i = STATUS_PENDING | e.i & STATUS_UNINITIALIZED; | ||
| // Preserve the current source on this propagation so render-effect notification | ||
| // can register every distinct pending source with the transition. | ||
| setPendingError(e, o, r); | ||
| } else { | ||
| clearPendingSources(e); | ||
| e.i = n | (n !== STATUS_ERROR ? e.i & STATUS_UNINITIALIZED : 0); | ||
| e.k = r; | ||
| } | ||
| GlobalQueue._ !== null && GlobalQueue._(e); | ||
| if (e.G && GlobalQueue.me !== null) GlobalQueue.me(e); | ||
| } | ||
| if (u && !t) { | ||
| assignOrMergeLane(e, u); | ||
| } | ||
| const a = t || f; | ||
| const c = t || s ? undefined : u; | ||
| if (e.C) { | ||
| if (t && n === STATUS_PENDING) { | ||
| return; | ||
| } | ||
| if (a) { | ||
| e.C(n, r); | ||
| } else { | ||
| e.C(); | ||
| } | ||
| return; | ||
| } | ||
| forEachDependent(e, (e, t) => { | ||
| e.Ie = clock; | ||
| if (n === STATUS_PENDING && o && e.m !== o && !e.M?.has(o) || n !== STATUS_PENDING && (e.k !== r || e.m || e.M)) { | ||
| // A pending-observer link is the subscription an `isPending` read created. | ||
| // It exists so the observer re-runs when the source settles, but it must | ||
| // not carry a real (non-NotReadyError) error — the synchronous `isPending` | ||
| // read swallows those, and the async path must match. Re-run the observer | ||
| // so `isPending` re-evaluates (to not-pending) instead of forwarding. | ||
| if (t.Qe && n !== STATUS_PENDING && !(r instanceof NotReadyError)) { | ||
| enqueueSub(e); | ||
| schedule(); | ||
| return; | ||
| } | ||
| if (!a && !l && !e.ve) queuePendingNode(e); | ||
| notifyStatus(e, n, r, a, c); | ||
| } | ||
| }); | ||
| } | ||
| export { addPendingSource, clearStatus, forEachDependent, handleAsync, isThenable, notifyStatus, setPendingError, settlePendingSource }; |
| const REACTIVE_NONE = 0; | ||
| const REACTIVE_CHECK = 1 << 0; | ||
| const REACTIVE_DIRTY = 1 << 1; | ||
| const REACTIVE_RECOMPUTING_DEPS = 1 << 2; | ||
| const REACTIVE_IN_HEAP = 1 << 3; | ||
| const REACTIVE_IN_HEAP_HEIGHT = 1 << 4; | ||
| const REACTIVE_ZOMBIE = 1 << 5; | ||
| const REACTIVE_DISPOSED = 1 << 6; | ||
| const REACTIVE_OPTIMISTIC_DIRTY = 1 << 7; | ||
| const REACTIVE_SNAPSHOT_STALE = 1 << 8; | ||
| const REACTIVE_LAZY = 1 << 9; | ||
| const REACTIVE_MANUAL_WRITE = 1 << 10; | ||
| /** | ||
| * The pending recompute is a re-ask of the same question: `refresh()` dirtied | ||
| * the node while no tracked input changed value. Cleared whenever a real | ||
| * value-change notification arrives (`insertSubs`), and consumed by | ||
| * `recompute` into the node's `_reask` classification — a quiet (re-ask) | ||
| * pending window does not read as pending (question-scoped pending model). | ||
| */ const REACTIVE_REASK = 1 << 11; | ||
| // Static configuration bits packed into Owner/Computed/Signal _config. | ||
| const CONFIG_OWNED_WRITE = 1 << 0; | ||
| const CONFIG_NO_SNAPSHOT = 1 << 1; | ||
| const CONFIG_TRANSPARENT = 1 << 2; | ||
| const CONFIG_IN_SNAPSHOT_SCOPE = 1 << 3; | ||
| const CONFIG_CHILDREN_FORBIDDEN = 1 << 4; | ||
| const CONFIG_AUTO_DISPOSE = 1 << 5; | ||
| const CONFIG_SYNC = 1 << 6; | ||
| const STATUS_PENDING = 1 << 0; | ||
| const STATUS_ERROR = 1 << 1; | ||
| const STATUS_UNINITIALIZED = 1 << 2; | ||
| const EFFECT_RENDER = 1; | ||
| const EFFECT_USER = 2; | ||
| const EFFECT_TRACKED = 3; | ||
| const NOT_PENDING = {}; | ||
| const NO_SNAPSHOT = {}; | ||
| const STORE_SNAPSHOT_PROPS = "sp"; | ||
| const SUPPORTS_PROXY = typeof Proxy === "function"; | ||
| const defaultContext = {}; | ||
| /** | ||
| * Brand symbol used by `Refreshable<T>` values (projection stores, async | ||
| * memos) to expose their underlying computation to `refresh()`. Not part of | ||
| * the user-facing API. | ||
| * | ||
| * @internal | ||
| */ const $REFRESH = Symbol("refresh"); | ||
| export { $REFRESH, CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, CONFIG_IN_SNAPSHOT_SCOPE, CONFIG_NO_SNAPSHOT, CONFIG_OWNED_WRITE, CONFIG_SYNC, CONFIG_TRANSPARENT, EFFECT_RENDER, EFFECT_TRACKED, EFFECT_USER, NOT_PENDING, NO_SNAPSHOT, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_DISPOSED, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_LAZY, REACTIVE_MANUAL_WRITE, REACTIVE_NONE, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, REACTIVE_SNAPSHOT_STALE, REACTIVE_ZOMBIE, STATUS_ERROR, STATUS_PENDING, STATUS_UNINITIALIZED, STORE_SNAPSHOT_PROPS, SUPPORTS_PROXY, defaultContext }; |
| import { NoOwnerError, ContextNotFoundError } from "./error.js"; | ||
| import { getOwner } from "./owner.js"; | ||
| /** | ||
| * Context provides a form of dependency injection. It is used to save from needing to pass | ||
| * data as props through intermediate components. This function creates a new context object | ||
| * that can be used with `getContext` and `setContext`. | ||
| * | ||
| * A default value can be provided here which will be used when a specific value is not provided | ||
| * via a `setContext` call. | ||
| */ function createContext(e, t) { | ||
| return { | ||
| id: Symbol(t), | ||
| defaultValue: e | ||
| }; | ||
| } | ||
| /** | ||
| * Low-level owner-targeted context read. The user-facing read API is | ||
| * `useContext` (in `solid-js`), which wraps this primitive. Exposed here for | ||
| * cross-package wiring (e.g. hydration-aware context plumbing). | ||
| * | ||
| * @throws `NoOwnerError` if there's no owner at the time of call. | ||
| * @throws `ContextNotFoundError` if a context value has not been set yet. | ||
| * | ||
| * @internal | ||
| */ function getContext(e, t = getOwner()) { | ||
| if (!t) { | ||
| throw new NoOwnerError; | ||
| } | ||
| const n = hasContext(e, t) ? t.we[e.id] : e.defaultValue; | ||
| if (isUndefined(n)) { | ||
| throw new ContextNotFoundError; | ||
| } | ||
| return n; | ||
| } | ||
| /** | ||
| * Low-level owner-targeted context write. The user-facing API is | ||
| * `createContext` (in `solid-js`); its provider component wraps this | ||
| * primitive. Exposed here for cross-package wiring. | ||
| * | ||
| * @throws `NoOwnerError` if there's no owner at the time of call. | ||
| * | ||
| * @internal | ||
| */ function setContext(e, t, n = getOwner()) { | ||
| if (!n) { | ||
| throw new NoOwnerError; | ||
| } | ||
| // We're creating a new object to avoid child context values being exposed to parent owners. If | ||
| // we don't do this, everything will be a singleton and all hell will break lose. | ||
| n.we = { | ||
| ...n.we, | ||
| [e.id]: isUndefined(t) ? e.defaultValue : t | ||
| }; | ||
| } | ||
| function hasContext(e, t) { | ||
| return !isUndefined(t?.we[e.id]); | ||
| } | ||
| function isUndefined(e) { | ||
| return typeof e === "undefined"; | ||
| } | ||
| export { createContext, getContext, setContext }; |
| import { handleAsync, clearStatus, notifyStatus } from "./async.js"; | ||
| import { $REFRESH, REACTIVE_DISPOSED, REACTIVE_MANUAL_WRITE, REACTIVE_DIRTY, REACTIVE_CHECK, REACTIVE_IN_HEAP, REACTIVE_REASK, REACTIVE_LAZY, REACTIVE_NONE, defaultContext, CONFIG_TRANSPARENT, CONFIG_OWNED_WRITE, CONFIG_AUTO_DISPOSE, CONFIG_SYNC, CONFIG_NO_SNAPSHOT, CONFIG_IN_SNAPSHOT_SCOPE, NOT_PENDING, STATUS_UNINITIALIZED, STATUS_PENDING, NO_SNAPSHOT, REACTIVE_SNAPSHOT_STALE, EFFECT_TRACKED, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_RECOMPUTING_DEPS, STATUS_ERROR, REACTIVE_IN_HEAP_HEIGHT, STORE_SNAPSHOT_PROPS, EFFECT_USER } from "./constants.js"; | ||
| import { NotReadyError } from "./error.js"; | ||
| import { trimStaleDeps, link, unobserved } from "./graph.js"; | ||
| import { insertIntoHeap, queueFor, deleteFromHeap, insertIntoHeapHeight, markNode, markHeap } from "./heap.js"; | ||
| import { schedule, GlobalQueue, globalQueue, clock, activeTransition, queuePendingNode, insertSubs, dirtyQueue, activeAffectsMarks, runInTransition, projectionWriteActive, armReaskClear } from "./scheduler.js"; | ||
| import "./invariants.js"; | ||
| import { inheritId, disposeChildren, markDisposal } from "./owner.js"; | ||
| GlobalQueue.Ce = recompute; | ||
| GlobalQueue.Oe = disposeChildren; | ||
| let tracking = false; | ||
| /** @internal verdict-module glue */ function setPendingCheckActive(e) { | ||
| pendingCheckActive = e; | ||
| } | ||
| /** @internal verdict-module glue */ function setLatestReadActive(e) { | ||
| latestReadActive = e; | ||
| } | ||
| /** @internal verdict-module glue */ function setContextInternal(e) { | ||
| context = e; | ||
| } | ||
| let stale = false; | ||
| let pendingCheckActive = false; | ||
| let latestReadActive = false; | ||
| let context = null; | ||
| let currentOptimisticLane = null; | ||
| /** | ||
| * Marked sources read by the recompute currently on the stack (saved/restored | ||
| * per recompute, like `context`). A computed that reads a node with a live | ||
| * `affects()` mark inherits the mark's pendingness — `recompute` applies the | ||
| * collected sources through `applyAffectsReads` after its commit, because the | ||
| * `clearStatus` at the top of the commit path wipes whatever sentinel entries | ||
| * the node held from the registration-time push. This is the pull half of | ||
| * mark propagation (real async re-establishes itself by re-throwing on read; | ||
| * marks are value-transparent, so they re-establish here instead). | ||
| */ let affectsReads = null; | ||
| let snapshotCaptureActive = false; | ||
| let snapshotSources = null; | ||
| function ownerInSnapshotScope(e) { | ||
| while (e) { | ||
| if (e.ke) return true; | ||
| e = e.He; | ||
| } | ||
| return false; | ||
| } | ||
| function setSnapshotCapture(e) { | ||
| snapshotCaptureActive = e; | ||
| if (e && !snapshotSources) snapshotSources = new Set; | ||
| } | ||
| function markSnapshotScope(e) { | ||
| e.ke = true; | ||
| } | ||
| function releaseSnapshotScope(e) { | ||
| e.ke = false; | ||
| releaseSubtree(e); | ||
| schedule(); | ||
| } | ||
| function releaseSubtree(e) { | ||
| let t = e.Fe; | ||
| while (t) { | ||
| if (t.ke) { | ||
| t = t.Ve; | ||
| continue; | ||
| } | ||
| if (t.xe) { | ||
| const e = t; | ||
| e.U &= ~CONFIG_IN_SNAPSHOT_SCOPE; | ||
| if (e.u & REACTIVE_SNAPSHOT_STALE) { | ||
| e.u &= ~REACTIVE_SNAPSHOT_STALE; | ||
| e.u |= REACTIVE_DIRTY; | ||
| if (dirtyQueue.Le > e.qe) dirtyQueue.Le = e.qe; | ||
| insertIntoHeap(e, dirtyQueue); | ||
| } | ||
| } | ||
| releaseSubtree(t); | ||
| t = t.Ve; | ||
| } | ||
| } | ||
| function clearSnapshots() { | ||
| if (snapshotSources) { | ||
| for (const e of snapshotSources) { | ||
| delete e.Ye; | ||
| delete e[STORE_SNAPSHOT_PROPS]; | ||
| } | ||
| snapshotSources = null; | ||
| } | ||
| snapshotCaptureActive = false; | ||
| } | ||
| function recompute(e, t = false) { | ||
| const n = e.De; | ||
| if (!t) { | ||
| if (e.ve && (!n || activeTransition) && activeTransition !== e.ve) globalQueue.initTransition(e.ve); | ||
| deleteFromHeap(e, queueFor(e)); | ||
| e.Ae = null; | ||
| // Tracked effects run after finalizePureQueue, so dispose immediately instead of deferring | ||
| if (e.ve || n === EFFECT_TRACKED) disposeChildren(e); else if (e.Fe !== null || e.Me !== null) { | ||
| markDisposal(e); | ||
| e.We = e.Me; | ||
| e.Ze = e.Fe; | ||
| e.Me = null; | ||
| e.Fe = null; | ||
| e.je = 0; | ||
| } else ; | ||
| } | ||
| let i = !!(e.u & REACTIVE_OPTIMISTIC_DIRTY); | ||
| const l = e.be !== undefined && e.be !== NOT_PENDING; | ||
| const u = !!(e.i & STATUS_UNINITIALIZED); | ||
| // Re-ask classification lives in the verdict module; capture the flag before | ||
| // the recompute wipes _flags below. | ||
| const s = (e.u & REACTIVE_REASK) !== 0; | ||
| const o = context; | ||
| context = e; | ||
| e.Ke = null; | ||
| e.ze++; | ||
| e.u = REACTIVE_RECOMPUTING_DEPS; | ||
| e.Ie = clock; | ||
| let a = e.ge === NOT_PENDING ? e.Ue : e.ge; | ||
| let r = e.qe; | ||
| let c = tracking; | ||
| const _ = affectsReads; | ||
| affectsReads = null; | ||
| let f = null; | ||
| let E = currentOptimisticLane; | ||
| tracking = true; | ||
| // Lane posture lives with the engine: OPTIMISTIC_DIRTY is only ever set by | ||
| // engine-driven paths, and _optimisticNodes is only pushed by | ||
| // _optimisticWrite, so the hook is installed whenever either gate holds. | ||
| if (i) { | ||
| const t = GlobalQueue.$e(e, true); | ||
| if (t) currentOptimisticLane = t; | ||
| } else if (activeTransition && !t && activeTransition.Be.length) { | ||
| // Lane adoption: parent-deeper-than-owned-child can run before its OPT-dirty | ||
| // child propagates. Walk deps once and inherit the OPT lane so this node | ||
| // recomputes under the right posture and propagates correctly. | ||
| const t = GlobalQueue.$e(e, false); | ||
| if (t) { | ||
| i = true; | ||
| currentOptimisticLane = t; | ||
| } | ||
| } | ||
| const T = n && n !== EFFECT_USER; | ||
| const N = stale; | ||
| if (T) stale = true; | ||
| try { | ||
| if (!false && e.U & CONFIG_SYNC) { | ||
| a = e.xe(a); | ||
| e.Ae = null; | ||
| } else { | ||
| // Snapshot `_inFlight` so we can detect whether `_fn` self-registered an async | ||
| // subscription (e.g. `createProjection` calls `handleAsync` from inside its body | ||
| // with a setter callback). In that case, the outer `handleAsync` call below would | ||
| // clobber the fresh subscription, so we skip it and let the internally-registered | ||
| // iteration drive updates. | ||
| const t = e.Ae; | ||
| const n = e.xe(a); | ||
| const i = typeof n === "object" && n !== null; | ||
| const l = e.Ae !== t; | ||
| a = l || !i ? n : handleAsync(e, n); | ||
| if (!l && !i) e.Ae = null; | ||
| } | ||
| clearStatus(e, t); | ||
| // _optimisticLane is only ever assigned by engine paths. | ||
| if (e.Je) GlobalQueue.Xe(e); | ||
| } catch (t) { | ||
| // Track pending async in the lane (not the lane's source — it creates the lane | ||
| // but doesn't belong to it). Set lane BEFORE notifyStatus for downstream propagation. | ||
| if (t instanceof NotReadyError && currentOptimisticLane) GlobalQueue.et(e); | ||
| let n = false; | ||
| if (t instanceof NotReadyError) { | ||
| e.Re = true; | ||
| if (GlobalQueue.tt !== null) n = GlobalQueue.tt(e, s); | ||
| } | ||
| notifyStatus(e, t instanceof NotReadyError ? STATUS_PENDING : STATUS_ERROR, t, undefined, t instanceof NotReadyError ? e.Je : undefined); | ||
| if (n) GlobalQueue.nt(e); | ||
| } finally { | ||
| tracking = c; | ||
| if (T) stale = N; | ||
| e.u = REACTIVE_NONE | (t ? e.u & REACTIVE_SNAPSHOT_STALE : 0); | ||
| context = o; | ||
| f = affectsReads; | ||
| affectsReads = _; | ||
| } | ||
| if (!e.k) { | ||
| trimStaleDeps(e); | ||
| const s = l ? e.be : e.ge === NOT_PENDING ? e.Ue : e.ge; | ||
| let o = false; | ||
| try { | ||
| o = !n && u || !e.Ge || !e.Ge(s, a); | ||
| } catch (t) { | ||
| // A throwing user comparator is an error of this node's computation. | ||
| // Route it through the same status path as a compute-phase throw so | ||
| // error boundaries contain it; otherwise it unwinds the scheduler | ||
| // flush, bypassing every boundary and wedging the queue (#2837). | ||
| notifyStatus(e, STATUS_ERROR, t); | ||
| } | ||
| // Effects use `_equals: false` (no per-effect closure). The side effects that | ||
| // the equals closure used to perform — flagging the effect dirty and enqueueing | ||
| // its runner — happen here instead. `!create` matches the previous `initialized` | ||
| // gate: the explicit recompute(node, true) inside effect() does not enqueue, so | ||
| // effect() can call its runner synchronously for the first run. | ||
| if (n && o) { | ||
| e.it = !e.k; | ||
| // Reuse one bound runner per effect — runEffect no-ops on a stale | ||
| // `_modified`, so re-enqueueing the same function is harmless. | ||
| if (!t) e.v.enqueue(n, e.lt ??= GlobalQueue.ut.bind(null, e)); | ||
| } | ||
| if (e.k) ; else if (o) { | ||
| const u = l ? e.be : undefined; | ||
| if (t || n && activeTransition !== e.ve || i) { | ||
| e.Ue = a; | ||
| // Lane-propagated correction: upstream data is fresh, correct the | ||
| // override unconditionally. The direct _value commit is the lane's | ||
| // own reveal schedule; drop any superseded older hold so its queued | ||
| // commit can't clobber the fresh value. | ||
| if (l && i) { | ||
| e.be = a; | ||
| e.ge = NOT_PENDING; | ||
| } | ||
| } else { | ||
| e.ge = a; | ||
| // Transition-held sync recompute is a write path like setSignal/asyncWrite, | ||
| // so sync derivations of held sources stay visible to isPending()/latest() | ||
| // (#2831). Both companion writes are transition-scoped (optimistic) and | ||
| // auto-revert/re-derive at commit. Skipped for plain flushes where the | ||
| // pending value commits before effects run. | ||
| if ((activeTransition || e.ve) && GlobalQueue._e !== null) GlobalQueue._e(e, a); | ||
| } | ||
| if (!l || i || e.be !== u) insertSubs(e, i || l); | ||
| } else if (l) { | ||
| // Unchanged value (equals the override) recomputed while the override | ||
| // is active: _value may still be stale, so hold the authoritative value | ||
| // for commit on its own transition's schedule — invisibly (A17/A18). | ||
| if (e.ge === NOT_PENDING) queuePendingNode(e); | ||
| e.ge = a; | ||
| } else if (e.qe != r) { | ||
| for (let t = e.p; t !== null; t = t.de) { | ||
| insertIntoHeapHeight(t.Ee, queueFor(t.Ee)); | ||
| } | ||
| } | ||
| } | ||
| currentOptimisticLane = E; | ||
| // Marks read during this recompute re-attach after the commit above: | ||
| // earlier, the sentinel's NotReadyError in `_error` would have routed the | ||
| // fresh value into the error-skip branch instead of committing it. A real | ||
| // (non-NotReady) error wins over marks (#2893): applying the sentinel here | ||
| // would clobber the user's error with a NotReadyError from a node whose | ||
| // reads value-transparency promises can never throw NotReady. | ||
| // `markedReads` only collects under a live mark, so the hook is installed. | ||
| if (f && !(e.i & STATUS_ERROR)) GlobalQueue.j(e, f); | ||
| // Mark-only pending doesn't queue (#2893): the node holds no value needing | ||
| // a transition-scheduled commit, and queueing would stamp the marking | ||
| // action's transaction onto it — freezing unrelated writes that share it. | ||
| // (Single mask test up front keeps the mark-free hot path at original cost.) | ||
| const I = e.i & (STATUS_PENDING | STATUS_UNINITIALIZED); | ||
| const S = e.ge !== NOT_PENDING || e.Ze !== null || e.We !== null || I !== 0 && (I !== STATUS_PENDING || activeAffectsMarks === 0 || !GlobalQueue.$(e)); | ||
| // Override-covered holds (hasOverride) always queue: their commit belongs | ||
| // to their own transition's schedule (A18 re-rule) and is unobservable | ||
| // under the override (A17). Revert no longer commits anything, so an | ||
| // unqueued covered hold would leak (INV-7) once the revert clears | ||
| // _transition. | ||
| S && (!t || e.i & STATUS_PENDING) && (!e.ve || l) && queuePendingNode(e); | ||
| e.ve && n && activeTransition !== e.ve && runInTransition(e.ve, () => recompute(e)); | ||
| } | ||
| function updateIfNecessary(e) { | ||
| if (e.u & REACTIVE_CHECK) { | ||
| for (let t = e.S; t; t = t.st) { | ||
| const n = t.ot; | ||
| const i = n.rt || n; | ||
| if (i.xe) { | ||
| updateIfNecessary(i); | ||
| } | ||
| if (e.u & REACTIVE_DIRTY) { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (e.u & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY) || e.k && e.Ie < clock && !e.Ae) { | ||
| recompute(e); | ||
| } | ||
| e.u = e.u & (REACTIVE_SNAPSHOT_STALE | REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT); | ||
| } | ||
| function computed(e, t) { | ||
| const n = t?.transparent ?? false; | ||
| const i = { | ||
| id: inheritId(t, n, context), | ||
| U: (n ? CONFIG_TRANSPARENT : 0) | (t?.ownedWrite ? CONFIG_OWNED_WRITE : 0) | (!context || t?.lazy ? CONFIG_AUTO_DISPOSE : 0) | (t?.sync ? CONFIG_SYNC : 0) | (t?.re ? CONFIG_NO_SNAPSHOT : 0) | (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0), | ||
| Ge: t?.equals != null ? t.equals : isEqual, | ||
| ct: t?.unobserved, | ||
| Me: null, | ||
| v: context?.v ?? globalQueue, | ||
| we: context?.we ?? defaultContext, | ||
| je: 0, | ||
| xe: e, | ||
| Ue: undefined, | ||
| qe: 0, | ||
| G: null, | ||
| _t: undefined, | ||
| ft: null, | ||
| S: null, | ||
| Ke: null, | ||
| ze: 0, | ||
| p: null, | ||
| Et: null, | ||
| He: context, | ||
| Ve: null, | ||
| Tt: null, | ||
| Fe: null, | ||
| u: t?.lazy ? REACTIVE_LAZY : REACTIVE_NONE, | ||
| i: STATUS_UNINITIALIZED, | ||
| Ie: clock, | ||
| ge: NOT_PENDING, | ||
| We: null, | ||
| Ze: null, | ||
| Ae: null, | ||
| ve: null, | ||
| A: false | ||
| }; | ||
| setupComputedNode(i, t); | ||
| return i; | ||
| } | ||
| /** | ||
| * Build an Effect node with all effect-specific fields baked into a single object literal, | ||
| * so V8 sees the full hidden class shape at construction time. Effects always run in lazy | ||
| * mode (recompute is called explicitly by `effect()`), so we hardcode the lazy bits and skip | ||
| * the auto-dispose CONFIG bit (effect() previously cleared it post-construction). | ||
| */ function createEffectNode(e, t, n, i, l, u) { | ||
| const s = u?.transparent ?? false; | ||
| const o = { | ||
| id: inheritId(u, s, context), | ||
| U: (s ? CONFIG_TRANSPARENT : 0) | (u?.ownedWrite ? CONFIG_OWNED_WRITE : 0) | (u?.sync ? CONFIG_SYNC : 0) | (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0), | ||
| Ge: false, | ||
| ct: u?.unobserved, | ||
| Me: null, | ||
| v: context?.v ?? globalQueue, | ||
| we: context?.we ?? defaultContext, | ||
| je: 0, | ||
| xe: e, | ||
| Ue: undefined, | ||
| qe: 0, | ||
| G: null, | ||
| _t: undefined, | ||
| ft: null, | ||
| S: null, | ||
| Ke: null, | ||
| ze: 0, | ||
| p: null, | ||
| Et: null, | ||
| He: context, | ||
| Ve: null, | ||
| Tt: null, | ||
| Fe: null, | ||
| u: REACTIVE_LAZY, | ||
| i: STATUS_UNINITIALIZED, | ||
| Ie: clock, | ||
| ge: NOT_PENDING, | ||
| We: null, | ||
| Ze: null, | ||
| Ae: null, | ||
| ve: null, | ||
| A: false, | ||
| it: false, | ||
| Nt: undefined, | ||
| It: t, | ||
| St: n, | ||
| At: undefined, | ||
| De: i, | ||
| C: l | ||
| }; | ||
| setupComputedNode(o, lazyOptions); | ||
| return o; | ||
| } | ||
| const lazyOptions = { | ||
| lazy: true | ||
| }; | ||
| function setupComputedNode(e, t) { | ||
| e.ft = e; | ||
| const n = context?.dt ? context.Ct : context; | ||
| if (context) { | ||
| const t = context.Fe; | ||
| if (t === null) { | ||
| context.Fe = e; | ||
| } else { | ||
| e.Ve = t; | ||
| t.Tt = e; | ||
| context.Fe = e; | ||
| } | ||
| } | ||
| if (n) e.qe = n.qe + 1; | ||
| if (GlobalQueue.Ot !== null) GlobalQueue.Ot(e); | ||
| !t?.lazy && recompute(e, true); | ||
| if (snapshotCaptureActive && !t?.lazy) { | ||
| if (!(e.i & STATUS_PENDING) && !(e.U & CONFIG_NO_SNAPSHOT)) { | ||
| e.Ye = e.Ue === undefined ? NO_SNAPSHOT : e.Ue; | ||
| snapshotSources.add(e); | ||
| } | ||
| } | ||
| } | ||
| function signal(e, t, n = null) { | ||
| const i = { | ||
| Ge: t?.equals != null ? t.equals : isEqual, | ||
| U: (t?.ownedWrite ? CONFIG_OWNED_WRITE : 0) | (t?.re ? CONFIG_NO_SNAPSHOT : 0), | ||
| ct: t?.unobserved, | ||
| Ue: e, | ||
| p: null, | ||
| Et: null, | ||
| Ie: clock, | ||
| rt: n, | ||
| Ne: n?.G || null, | ||
| ge: NOT_PENDING | ||
| }; | ||
| n && (n.G = i); | ||
| if (snapshotCaptureActive && !(i.U & CONFIG_NO_SNAPSHOT) && !((n?.i ?? 0) & STATUS_PENDING)) { | ||
| i.Ye = e === undefined ? NO_SNAPSHOT : e; | ||
| snapshotSources.add(i); | ||
| } | ||
| return i; | ||
| } | ||
| function optimisticSignal(e, t) { | ||
| const n = signal(e, t); | ||
| n.be = NOT_PENDING; | ||
| return n; | ||
| } | ||
| function optimisticComputed(e, t) { | ||
| const n = computed(e, t); | ||
| n.be = NOT_PENDING; | ||
| return n; | ||
| } | ||
| function isEqual(e, t) { | ||
| return e === t; | ||
| } | ||
| /** | ||
| * Runs `fn` outside of any reactive tracking — reads inside `fn` will not | ||
| * subscribe the current scope. Returns whatever `fn` returns. | ||
| * | ||
| * Use `untrack` inside a memo or effect when you need to read a signal once | ||
| * without making the surrounding computation depend on its future changes. | ||
| * | ||
| * Pass a `strictReadLabel` string to enable a dev-mode warning: any reactive | ||
| * read inside `fn` that isn't inside a nested tracking scope will log a | ||
| * warning naming the label. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * createEffect( | ||
| * () => trigger(), // tracks `trigger` only | ||
| * () => { | ||
| * const snapshot = untrack(() => state); // read once, untracked | ||
| * log(snapshot); | ||
| * } | ||
| * ); | ||
| * ``` | ||
| */ function untrack(e, t) { | ||
| if (GlobalQueue.Rt === null && !tracking && true) return e(); | ||
| const n = tracking; | ||
| tracking = false; | ||
| try { | ||
| if (GlobalQueue.Rt !== null) return GlobalQueue.Rt(e); | ||
| return e(); | ||
| } finally { | ||
| tracking = n; | ||
| } | ||
| } | ||
| /** | ||
| * Bring a computed to a readable state: lazy/disposed nodes are (re)computed; | ||
| * an isPending() probe (`refresh`) additionally pulls the node fully up to | ||
| * date so its status flags reflect the current graph. | ||
| */ function prepareComputed(e, t) { | ||
| if (e.u & REACTIVE_LAZY) { | ||
| e.u &= ~REACTIVE_LAZY; | ||
| recompute(e, true); | ||
| } else if (e.u & REACTIVE_DISPOSED) { | ||
| recompute(e, true); | ||
| } else if (t) { | ||
| updateIfNecessary(e); | ||
| } | ||
| } | ||
| function read(e) { | ||
| // Handle latest() mode: read from _latestValueComputed | ||
| // Checked before isPending so that isPending(() => latest(x)) checks | ||
| // the _pendingSignal of _latestValueComputed (async in flight) rather | ||
| // than the original node (which stays "pending" while held in a transition). | ||
| if (latestReadActive) return GlobalQueue.Gt(e); | ||
| let t = context; | ||
| if (t?.dt) t = t.Ct; | ||
| const n = e; | ||
| const i = e.rt; | ||
| const l = i || e; | ||
| // Handle isPending() mode: collect pending state while preserving normal read semantics. | ||
| // Probe mode is suspended while preparing the node so nested reads during a | ||
| // recompute don't collect into the probe. | ||
| if (pendingCheckActive) { | ||
| GlobalQueue.Pt(e, t, l, i); | ||
| } else if (typeof n.xe === "function") { | ||
| prepareComputed(e, false); | ||
| } | ||
| if (!n.xe && l === e && e.be === undefined && e.Ye === undefined && activeTransition === null && currentOptimisticLane === null && !snapshotCaptureActive && true) { | ||
| if (t && tracking) { | ||
| link(e, t); | ||
| // A live mark on a read source flows to the reader (probe reads are | ||
| // excluded: the probe's own collection owns that verdict, and marking | ||
| // the enclosing memo would make an `isPending` wrapper itself pending). | ||
| if (activeAffectsMarks !== 0 && e.P && !pendingCheckActive) (affectsReads ??= []).push(e); | ||
| } | ||
| return !t || e.ge === NOT_PENDING ? e.Ue : e.ge; | ||
| } | ||
| if (t && tracking) { | ||
| link(e, t, pendingCheckActive); | ||
| // Mark inheritance through derivation (see the fast path above), and its | ||
| // transitive half (#2893): a mark-pended owner's sentinel sources flow to | ||
| // the reader like a direct mark — value-transparent reads have no throw | ||
| // to re-establish through, so without this a mid-window recompute sheds | ||
| // pendingness permanently past one derivation level. | ||
| if (activeAffectsMarks !== 0 && !pendingCheckActive) { | ||
| if (e.P) (affectsReads ??= []).push(e); | ||
| if (l.i & STATUS_PENDING) GlobalQueue.I(l, affectsReads ??= []); | ||
| } | ||
| if (l.xe) { | ||
| const n = queueFor(e); | ||
| if (l.qe >= n.Le) { | ||
| markNode(t); | ||
| markHeap(n); | ||
| updateIfNecessary(l); | ||
| } | ||
| const i = l.qe; | ||
| // parent check is shallow, might need to be recursive | ||
| if (i >= t.qe && e.He !== t) { | ||
| t.qe = i + 1; | ||
| } | ||
| } | ||
| } | ||
| // Mark-only pending never suspends the reader (#2886): an affects() mark is | ||
| // a promise of change, not an absence of value, so a derived node whose only | ||
| // pending sources are mark sentinels keeps its real value readable — | ||
| // pendingness reaches readers through verdicts (isPending), not throws. | ||
| // (`activeAffectsMarks` gates the source scan off the mark-free hot path; | ||
| // sentinels can't survive in pending sources past their last release.) | ||
| if (l.i & STATUS_PENDING && !(activeAffectsMarks !== 0 && GlobalQueue.$(l))) { | ||
| if (t && !(stale && l.ve && activeTransition !== l.ve)) { | ||
| // Per-lane suspension lives with the engine (a non-null lane implies it | ||
| // is installed): under a lane, only same-lane pending async without an | ||
| // active override throws. | ||
| if (currentOptimisticLane === null || GlobalQueue.ht(l)) { | ||
| if (!tracking && e !== t) link(e, t); | ||
| throw l.k; | ||
| } | ||
| } else if (t && l !== e && l.i & STATUS_UNINITIALIZED) { | ||
| if (!tracking && e !== t) link(e, t); | ||
| throw l.k; | ||
| } else if (!t && l.i & STATUS_UNINITIALIZED) { | ||
| throw l.k; | ||
| } | ||
| } | ||
| if (e.xe && e.i & STATUS_ERROR) { | ||
| // Only a genuine reactive re-read may retry an errored async source: | ||
| // - tracking: owned/tracked scope only (never events / `untrack` / effect side-effect phase) | ||
| // - !pendingCheckActive: an `isPending` probe observes the error, never refetches | ||
| // - el._time < clock: only on a later cycle than the one the error was found | ||
| if (tracking && !pendingCheckActive && e.Ie < clock) { | ||
| recompute(e); | ||
| return read(e); | ||
| } else throw e.k; | ||
| } | ||
| if (snapshotCaptureActive && t && t.U & CONFIG_IN_SNAPSHOT_SCOPE) { | ||
| const n = e.Ye; | ||
| if (n !== undefined) { | ||
| const i = n === NO_SNAPSHOT ? undefined : n; | ||
| const l = e.ge !== NOT_PENDING ? e.ge : e.Ue; | ||
| if (l !== i) t.u |= REACTIVE_SNAPSHOT_STALE; | ||
| return i; | ||
| } | ||
| } | ||
| if (e.be !== undefined && e.be !== 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 (t && stale && GlobalQueue.gt(e)) return e.Ue; | ||
| return e.be; | ||
| } | ||
| // Entanglement gate: a reader recomputing under an optimistic lane that reads | ||
| // a pending mid-transition write sees the committed value. Projection-store | ||
| // manual writes use the firewall's manual-write flag to opt into this path. | ||
| // Async drivers are not under an optimistic lane and so bypass this, reading | ||
| // _pendingValue for correct fetching. The sub is recorded for replay at commit | ||
| // so it re-runs with the new committed view. (Gate details live with the | ||
| // engine — a non-null lane implies it is installed.) | ||
| if (currentOptimisticLane !== null && activeTransition !== null && t !== null && GlobalQueue.Dt(e, l, t)) { | ||
| return e.Ue; | ||
| } | ||
| // In optimistic lane context, return _value for optimistic/lane-assigned signals | ||
| // and for regular signals in stale mode (render effects). Non-stale readers (user | ||
| // effects) see _pendingValue so that latest() and direct reads stay consistent. | ||
| // (The lane-context clause lives with the engine.) | ||
| const u = !t || currentOptimisticLane !== null && GlobalQueue.kt(e, l, t) || e.ge === NOT_PENDING || stale && e.ve && activeTransition !== e.ve ? e.Ue : e.ge; | ||
| // 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.vt(e, u); | ||
| if (!t && l === e && typeof n.xe === "function" && e.U & CONFIG_AUTO_DISPOSE && !(l.i & STATUS_PENDING) && !e.p) { | ||
| unobserved(e); | ||
| } | ||
| return u; | ||
| } | ||
| function setSignal(e, t) { | ||
| if (e.ve && activeTransition !== e.ve) globalQueue.initTransition(e.ve); | ||
| // 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 | ||
| // engine first. | ||
| if (e.be !== undefined && !projectionWriteActive) return GlobalQueue.bt(e, t); | ||
| const n = e.ge === NOT_PENDING ? e.Ue : e.ge; | ||
| if (typeof t === "function") t = t(n); | ||
| // Uninitialized check first: the first commit has no previous value, so the | ||
| // user comparator must not run against `undefined` (matches recompute). | ||
| const i = !!(e.i & STATUS_UNINITIALIZED) || !e.Ge || !e.Ge(n, t); | ||
| if (!i) return t; | ||
| if (e.ge === NOT_PENDING) queuePendingNode(e); | ||
| e.ge = t; | ||
| GlobalQueue._e !== null && GlobalQueue._e(e, t); | ||
| e.Ie = clock; | ||
| insertSubs(e); | ||
| schedule(); | ||
| return t; | ||
| } | ||
| /** | ||
| * Suppresses automatic recomputation of `el` until the scheduler drains. Used | ||
| * when a manual write should win over dependency changes queued in the same | ||
| * tick. The MANUAL_WRITE flag is cleared by the pending-node drain; projection | ||
| * computeds don't commit values, but they still need the same end-of-tick | ||
| * cleanup point. | ||
| */ function suppressComputedRecompute(e) { | ||
| deleteFromHeap(e, queueFor(e)); | ||
| if (!(e.u & REACTIVE_MANUAL_WRITE) && e.ge === NOT_PENDING) { | ||
| queuePendingNode(e); | ||
| schedule(); | ||
| } | ||
| e.u = e.u & -4 | REACTIVE_MANUAL_WRITE; | ||
| } | ||
| /** | ||
| * User-facing setter for the memo form of `createSignal(fn)`. Behaves like | ||
| * `setSignal`, but also cancels any pending recompute of the memo so the | ||
| * manual value wins over a value that would otherwise be produced by an | ||
| * upstream change in the same tick. | ||
| */ function setMemo(e, t) { | ||
| const n = setSignal(e, t); | ||
| suppressComputedRecompute(e); | ||
| return n; | ||
| } | ||
| /** | ||
| * Executes `fn` with the given `owner` set as the current owner. Any reactive | ||
| * primitives (`createSignal`, `createMemo`, `createEffect`, `onCleanup`, | ||
| * `cleanup`, etc.) created inside `fn` are attached to that owner, so they | ||
| * are disposed when the owner is disposed. | ||
| * | ||
| * The classic pattern: capture the current owner with `getOwner()` inside a | ||
| * component, then re-enter it from a callback (event handler, async resolve, | ||
| * setTimeout) so disposables created in the callback get cleaned up with the | ||
| * component. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * function delayed<T>(ms: number, fn: () => T) { | ||
| * const owner = getOwner(); | ||
| * setTimeout(() => runWithOwner(owner, fn), ms); | ||
| * } | ||
| * ``` | ||
| */ function runWithOwner(e, t) { | ||
| const n = context; | ||
| const i = tracking; | ||
| context = e; | ||
| tracking = false; | ||
| try { | ||
| return t(); | ||
| } finally { | ||
| context = n; | ||
| tracking = i; | ||
| } | ||
| } | ||
| function staleValues(e, t = true) { | ||
| const n = stale; | ||
| stale = t; | ||
| try { | ||
| return e(); | ||
| } finally { | ||
| stale = n; | ||
| } | ||
| } | ||
| /** | ||
| * Invalidates one reactive source, forcing it to re-execute even if its inputs | ||
| * haven't changed. | ||
| * | ||
| * Pass either a Solid-created accessor or a projected store created from | ||
| * `createStore(fn, ...)` / `createProjection(...)`. `refresh()` is a | ||
| * write-like invalidation operation: it does not read the target's value, and | ||
| * refreshing a plain signal accessor is a no-op. | ||
| * | ||
| * Use it to invalidate cached async values (e.g. force a re-fetch) without | ||
| * tearing the consumer down. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json())); | ||
| * | ||
| * // Re-fetch on demand | ||
| * <button onClick={() => refresh(user)}>Reload</button> | ||
| * ``` | ||
| */ function refresh(e) { | ||
| const t = e?.[$REFRESH]; | ||
| if (!t) { | ||
| return; | ||
| } | ||
| if (typeof t.xe === "function" && !(t.u & (REACTIVE_DISPOSED | REACTIVE_MANUAL_WRITE))) { | ||
| // A refresh with no value-change dirt already queued is a re-ask of the | ||
| // same question: mark it so the recompute classifies any resulting | ||
| // pending window as quiet (not pending). If the node is already dirty | ||
| // from a real input change, the question changed — don't mark. | ||
| // REACTIVE_IN_HEAP counts as dirt: insertSubs schedules subscribers by | ||
| // heap insertion alone (no DIRTY/CHECK flag), so a same-batch value | ||
| // change followed by refresh() must not be laundered into a quiet re-ask. | ||
| if (!(t.u & (REACTIVE_DIRTY | REACTIVE_CHECK | REACTIVE_IN_HEAP))) { | ||
| t.u |= REACTIVE_REASK; | ||
| armReaskClear(); | ||
| } | ||
| t.u = t.u & ~REACTIVE_CHECK | REACTIVE_DIRTY; | ||
| insertIntoHeap(t, queueFor(t)); | ||
| schedule(); | ||
| } | ||
| } | ||
| export { clearSnapshots, computed, context, createEffectNode, currentOptimisticLane, isEqual, latestReadActive, markSnapshotScope, optimisticComputed, optimisticSignal, pendingCheckActive, prepareComputed, read, recompute, refresh, releaseSnapshotScope, runWithOwner, setContextInternal, setLatestReadActive, setMemo, setPendingCheckActive, setSignal, setSnapshotCapture, signal, snapshotCaptureActive, snapshotSources, stale, staleValues, suppressComputedRecompute, tracking, untrack }; |
| const DEV = undefined; | ||
| export { DEV }; |
| import { EFFECT_USER, CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, EFFECT_TRACKED, STATUS_ERROR, STATUS_PENDING, EFFECT_RENDER, REACTIVE_DISPOSED } from "./constants.js"; | ||
| import { createEffectNode, recompute, computed, staleValues } from "./core.js"; | ||
| import { unwrapStatusError, StatusError } from "./error.js"; | ||
| import { haltReactivity, GlobalQueue } from "./scheduler.js"; | ||
| /** | ||
| * Effects are the leaf nodes of our reactive graph. When their sources change, they are | ||
| * automatically added to the queue of effects to re-execute, which will cause them to fetch their | ||
| * sources and recompute | ||
| */ function effect(t, E, e, R) { | ||
| const r = !!R?.user; | ||
| const f = createEffectNode(t, E, e, r ? EFFECT_USER : EFFECT_RENDER, notifyEffectStatus, R); | ||
| recompute(f, true); | ||
| !R?.defer && (f.De === EFFECT_USER || R?.schedule ? f.v.enqueue(f.De, runEffect.bind(null, f)) : runEffect(f)); | ||
| } | ||
| function notifyEffectStatus(t, E) { | ||
| // Use passed values if provided, otherwise read from node | ||
| const e = t !== undefined ? t : this.i; | ||
| const R = E !== undefined ? E : this.k; | ||
| if (e & STATUS_ERROR) { | ||
| this.v.notify(this, STATUS_PENDING, 0); | ||
| if (this.De === EFFECT_USER) { | ||
| // The error handler is the error arm of the effect phase (#2840 ruling): | ||
| // queue it like the effect function. It runs in the same imperative, | ||
| // writable scope, throws escalate the same way, and a held transition | ||
| // (or optimistic lane) defers it exactly as it defers the success arm. | ||
| // No payload is queued — the node already carries `_statusFlags`/`_error`, | ||
| // and the runner dispatches on them, so a recovery before the effect | ||
| // phase takes the success arm instead. Blocked forwards (explicit | ||
| // `status` arg without node-state writes) don't queue: the status | ||
| // re-propagates unblocked at commit. | ||
| if (this.i & STATUS_ERROR) { | ||
| this.it = true; | ||
| this.v.enqueue(this.De, this.lt ??= runEffect.bind(null, this)); | ||
| } | ||
| return; | ||
| } | ||
| if (!this.v.notify(this, STATUS_ERROR, STATUS_ERROR)) { | ||
| haltReactivity(unwrapStatusError(R)); | ||
| throw R; | ||
| } | ||
| } else if (this.De === EFFECT_RENDER) { | ||
| this.v.notify(this, STATUS_PENDING | STATUS_ERROR, e, R); | ||
| } | ||
| } | ||
| function runEffect(t) { | ||
| if (!t.it || t.u & REACTIVE_DISPOSED) return; | ||
| // Error arm (#2840), user effects only: a compute-phase error that is still | ||
| // the node's settled state at effect time runs the bundle's error handler in | ||
| // this same imperative, writable scope. Unwrap the StatusError used for | ||
| // source tracking — user code gets the error it threw, as boundaries do. No | ||
| // handler: log and keep the system alive (the run was skipped). A handler | ||
| // (or logging) consumes the error; a handler throw falls to the shared | ||
| // catch below and escalates boundary-or-halt like any effect-phase throw. | ||
| // Render effects bypass: their errors route to boundaries synchronously in | ||
| // notifyEffectStatus, and a runner queued by an earlier valueChanged in the | ||
| // same flush must not be hijacked by a later-arriving error status. | ||
| if (t.i & STATUS_ERROR && t.De === EFFECT_USER) { | ||
| const E = unwrapStatusError(t.k); | ||
| t.Nt = t.Ue; | ||
| t.it = false; | ||
| try { | ||
| t.St ? t.St(E, () => { | ||
| const E = t.At; | ||
| t.At = undefined; | ||
| E?.(); | ||
| }) : console.error(E); | ||
| } catch (E) { | ||
| if (!t.v.notify(t, STATUS_ERROR, STATUS_ERROR)) { | ||
| haltReactivity(E); | ||
| throw E; | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| const E = t.At; | ||
| t.At = undefined; | ||
| try { | ||
| E?.(); | ||
| const e = t.It(t.Ue, t.Nt); | ||
| if (false && e !== undefined && typeof e !== "function") ; | ||
| // The final cleanup is invoked by disposeChildren at true disposal. | ||
| t.At = e; | ||
| } catch (E) { | ||
| t.k = new StatusError(t, E); | ||
| t.i |= STATUS_ERROR; | ||
| if (!t.v.notify(t, STATUS_ERROR, STATUS_ERROR)) { | ||
| haltReactivity(E); | ||
| throw E; | ||
| } | ||
| } finally { | ||
| t.Nt = t.Ue; | ||
| t.it = false; | ||
| } | ||
| } | ||
| GlobalQueue.ut = runEffect; | ||
| /** | ||
| * Internal tracked effect - bypasses heap, goes directly to effect queue. | ||
| * Runs as a leaf owner: child primitives and onCleanup are forbidden (false throws). | ||
| * Uses stale reads. | ||
| */ function trackedEffect(t, E) { | ||
| const run = () => { | ||
| if (!e.it || e.u & REACTIVE_DISPOSED) return; | ||
| try { | ||
| e.it = false; | ||
| recompute(e); | ||
| } finally {} | ||
| }; | ||
| const e = computed(() => { | ||
| const E = e.At; | ||
| e.At = undefined; | ||
| E?.(); | ||
| const R = staleValues(t); | ||
| e.At = R; | ||
| }, { | ||
| ...E, | ||
| lazy: true | ||
| }); | ||
| e.At = undefined; | ||
| e.U = e.U & ~CONFIG_AUTO_DISPOSE | CONFIG_CHILDREN_FORBIDDEN; | ||
| e.it = true; | ||
| e.De = EFFECT_TRACKED; | ||
| e.C = (t, E) => { | ||
| const R = t !== undefined ? t : e.i; | ||
| if (R & STATUS_ERROR) { | ||
| e.v.notify(e, STATUS_PENDING, 0); | ||
| const t = E !== undefined ? E : e.k; | ||
| if (!e.v.notify(e, STATUS_ERROR, STATUS_ERROR)) { | ||
| haltReactivity(unwrapStatusError(t)); | ||
| throw t; | ||
| } | ||
| } | ||
| }; | ||
| e.Ft = run; | ||
| e.v.enqueue(EFFECT_USER, run); | ||
| } | ||
| export { effect, trackedEffect }; |
| /** | ||
| * Thrown by a tracked read whose value is currently pending (an async memo / | ||
| * `createSignal(asyncFn)` / projection / store derivation that hasn't settled | ||
| * yet). Surfacing through the reactive graph is what suspends the consumer | ||
| * scope — the nearest enclosing `<Loading>` boundary catches the throw and | ||
| * renders its fallback until the source resolves. | ||
| * | ||
| * App code rarely catches this directly; `<Loading>` is the canonical | ||
| * handler. The error type is exposed for advanced cases — e.g. interop layers | ||
| * that bridge Solid's pending-throw protocol to a different async strategy, | ||
| * or tests that want to assert on the suspension shape. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Advanced: distinguish "not ready yet" from a real error in custom | ||
| * // boundary plumbing. App code should rely on `<Loading>` / `<Errored>`. | ||
| * try { | ||
| * const value = readReactiveSource(); | ||
| * } catch (err) { | ||
| * if (err instanceof NotReadyError) throw err; // re-throw to suspend | ||
| * reportError(err); | ||
| * } | ||
| * ``` | ||
| */ | ||
| class NotReadyError extends Error { | ||
| source; | ||
| constructor(r) { | ||
| super(); | ||
| this.source = r; | ||
| } | ||
| } | ||
| class StatusError extends Error { | ||
| source; | ||
| constructor(r, o) { | ||
| super(o instanceof Error ? o.message : String(o), { | ||
| cause: o | ||
| }); | ||
| this.source = r; | ||
| } | ||
| } | ||
| /** Return the user's error from an internal status wrapper. */ function unwrapStatusError(r) { | ||
| return r instanceof StatusError ? r.cause : r; | ||
| } | ||
| class NoOwnerError extends Error { | ||
| constructor() { | ||
| super(""); | ||
| } | ||
| } | ||
| class ContextNotFoundError extends Error { | ||
| constructor() { | ||
| super(""); | ||
| } | ||
| } | ||
| export { ContextNotFoundError, NoOwnerError, NotReadyError, StatusError, unwrapStatusError }; |
| import { signal, setSignal, read } from "./core.js"; | ||
| import { cleanup } from "./owner.js"; | ||
| import { GlobalQueue } from "./scheduler.js"; | ||
| let externalSourceConfig = null; | ||
| /** | ||
| * Registers a factory that bridges external reactive systems (e.g. MobX, Vue refs) | ||
| * into Solid's tracking graph. Every computation will be wrapped so that the | ||
| * external library can track its own dependencies alongside Solid's. | ||
| * | ||
| * Multiple calls pipe together: each new factory wraps the previous one. | ||
| * | ||
| * @param config.factory receives `(fn, trigger)` — wrap fn execution in external tracking, | ||
| * call trigger when external deps change. Return `{ track, dispose }`. | ||
| * @param config.untrack optional wrapper for `untrack` — disables external tracking too. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Bridge an external "subscribe / notify" library into Solid's graph. | ||
| * // `factory` wraps every Solid compute so the external library can attach | ||
| * // its own dependency tracker; `trigger` re-runs the compute on external | ||
| * // change. `untrack` mirrors Solid's `untrack()` into the external library | ||
| * // so that reads inside `untrack(...)` don't get tracked twice. | ||
| * enableExternalSource({ | ||
| * factory: (compute, trigger) => { | ||
| * const sub = externalLib.subscribe(trigger); | ||
| * return { | ||
| * track: prev => externalLib.run(() => compute(prev)), | ||
| * dispose: () => sub.unsubscribe() | ||
| * }; | ||
| * }, | ||
| * untrack: fn => externalLib.untracked(fn) | ||
| * }); | ||
| * ``` | ||
| */ | ||
| // Wires a freshly created computed through the active external-source bridge. | ||
| // Lives here (installed on GlobalQueue while a config is active) rather than | ||
| // inline in core: esbuild cannot literal-track the mutable config binding the | ||
| // way rollup does, so an inline `if (externalSourceConfig)` block ships in | ||
| // every bundle even though only enableExternalSource() can make it reachable. | ||
| function wireExternalSource(e) { | ||
| const n = signal(undefined, { | ||
| equals: false, | ||
| ownedWrite: true | ||
| }); | ||
| const r = externalSourceConfig.factory(e.xe, () => { | ||
| setSignal(n, undefined); | ||
| }); | ||
| cleanup(() => r.dispose()); | ||
| e.xe = e => { | ||
| read(n); | ||
| return r.track(e); | ||
| }; | ||
| } | ||
| function externalUntrack(e) { | ||
| return externalSourceConfig.untrack(e); | ||
| } | ||
| // The hooks mirror the config's liveness exactly (installed on enable, | ||
| // removed on reset) so core's null checks stay equivalent to the old | ||
| // `externalSourceConfig` truthiness checks. | ||
| function syncExternalHooks() { | ||
| GlobalQueue.Ot = externalSourceConfig ? wireExternalSource : null; | ||
| GlobalQueue.Rt = externalSourceConfig ? externalUntrack : null; | ||
| } | ||
| function enableExternalSource(e) { | ||
| const {factory: n, untrack: r = e => e()} = e; | ||
| if (externalSourceConfig) { | ||
| const {factory: e, untrack: o} = externalSourceConfig; | ||
| externalSourceConfig = { | ||
| factory: (r, o) => { | ||
| const t = e(r, o); | ||
| const a = n(e => t.track(e), o); | ||
| return { | ||
| track: e => a.track(e), | ||
| dispose() { | ||
| a.dispose(); | ||
| t.dispose(); | ||
| } | ||
| }; | ||
| }, | ||
| untrack: e => o(() => r(e)) | ||
| }; | ||
| } else { | ||
| externalSourceConfig = { | ||
| factory: n, | ||
| untrack: r | ||
| }; | ||
| } | ||
| syncExternalHooks(); | ||
| } | ||
| export { enableExternalSource, externalSourceConfig }; |
| import { REACTIVE_RECOMPUTING_DEPS, CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE } from "./constants.js"; | ||
| import { deleteFromHeap, queueFor } from "./heap.js"; | ||
| import { disposeChildren } from "./owner.js"; | ||
| import "./scheduler.js"; | ||
| // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L100 | ||
| function unlinkSubs(n) { | ||
| const l = n.ot; | ||
| const e = n.st; | ||
| const u = n.de; | ||
| const s = n.nn; | ||
| if (u !== null) u.nn = s; else l.Et = s; | ||
| if (s !== null) s.de = u; else { | ||
| l.p = u; | ||
| if (u === null) { | ||
| l.ct?.(); | ||
| // No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set. | ||
| const n = l; | ||
| n.xe && n.U & CONFIG_AUTO_DISPOSE && !(n.u & REACTIVE_ZOMBIE) && unobserved(n); | ||
| } | ||
| } | ||
| return e; | ||
| } | ||
| function trimStaleDeps(n) { | ||
| const l = n.Ke; | ||
| let e = l !== null ? l.st : n.S; | ||
| if (e !== null) { | ||
| do { | ||
| e = unlinkSubs(e); | ||
| } while (e !== null); | ||
| if (l !== null) l.st = null; else n.S = null; | ||
| } | ||
| } | ||
| function unobserved(n) { | ||
| deleteFromHeap(n, queueFor(n)); | ||
| let l = n.S; | ||
| while (l !== null) { | ||
| l = unlinkSubs(l); | ||
| } | ||
| n.S = null; | ||
| n.Ke = null; | ||
| disposeChildren(n, true); | ||
| } | ||
| // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52 | ||
| function link(n, l, e = false) { | ||
| const u = l.Ke; | ||
| if (u !== null && u.ot === n) { | ||
| u.Qe = e; | ||
| return; | ||
| } | ||
| let s = null; | ||
| const t = l.u & REACTIVE_RECOMPUTING_DEPS; | ||
| if (t) { | ||
| s = u !== null ? u.st : l.S; | ||
| if (s !== null && s.ot === n) { | ||
| s.ln = l.ze; | ||
| l.Ke = s; | ||
| s.Qe = e; | ||
| return; | ||
| } | ||
| } | ||
| // A link stamped with the current pass generation was created or reused | ||
| // in-order during this recompute, i.e. it already sits in the validated | ||
| // [deps.._depsTail] prefix — the O(1) equivalent of scanning the dep list | ||
| // (the old alien-signals `isValidLink` walk, O(n²) when a computation | ||
| // re-reads earlier deps non-consecutively, e.g. store leaf reads). | ||
| const i = n.Et; | ||
| if (i !== null && i.Ee === l && (!t || i.ln === l.ze)) { | ||
| i.Qe = e; | ||
| return; | ||
| } | ||
| const o = l.Ke = n.Et = { | ||
| ot: n, | ||
| Ee: l, | ||
| st: s, | ||
| nn: i, | ||
| de: null, | ||
| ln: l.ze, | ||
| Qe: e | ||
| }; | ||
| if (u !== null) u.st = o; else l.S = o; | ||
| if (i !== null) i.de = o; else n.p = o; | ||
| } | ||
| export { link, trimStaleDeps, unlinkSubs, unobserved }; |
| import { REACTIVE_IN_HEAP, REACTIVE_RECOMPUTING_DEPS, REACTIVE_MANUAL_WRITE, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_ZOMBIE, EFFECT_TRACKED, EFFECT_USER } from "./constants.js"; | ||
| import { zombieQueue, dirtyQueue } from "./scheduler.js"; | ||
| /** The queue a node belongs to, picked from its own zombie flag. */ function queueFor(e) { | ||
| return e.u & REACTIVE_ZOMBIE ? zombieQueue : dirtyQueue; | ||
| } | ||
| /** | ||
| * Schedule one subscriber to re-run on the next flush: tracked effects bypass | ||
| * the heap and go directly to their effect queue; everything else is inserted | ||
| * into its own (zombie-flag-routed) heap with the `_min` cursor pulled down. | ||
| */ function enqueueSub(e) { | ||
| if (e.De === EFFECT_TRACKED) { | ||
| const E = e; | ||
| if (!E.it) { | ||
| E.it = true; | ||
| E.v.enqueue(EFFECT_USER, E.Ft); | ||
| } | ||
| return; | ||
| } | ||
| const E = queueFor(e); | ||
| if (E.Le > e.qe) E.Le = e.qe; | ||
| insertIntoHeap(e, E); | ||
| } | ||
| function actualInsertIntoHeap(e, E) { | ||
| const t = (e.He?.dt ? e.He.Ct?.qe : e.He?.qe) ?? -1; | ||
| if (t >= e.qe) e.qe = t + 1; | ||
| const n = e.qe; | ||
| const I = E.eE[n]; | ||
| if (I === undefined) E.eE[n] = e; else { | ||
| const E = I.ft; | ||
| E._t = e; | ||
| e.ft = E; | ||
| I.ft = e; | ||
| } | ||
| if (n > E.EE) E.EE = n; | ||
| } | ||
| function insertIntoHeap(e, E) { | ||
| let t = e.u; | ||
| if (t & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return; | ||
| if (t & REACTIVE_CHECK) { | ||
| e.u = t & -4 | REACTIVE_DIRTY | REACTIVE_IN_HEAP; | ||
| } else e.u = t | REACTIVE_IN_HEAP; | ||
| if (!(t & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(e, E); | ||
| } | ||
| function insertIntoHeapHeight(e, E) { | ||
| let t = e.u; | ||
| if (t & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_IN_HEAP_HEIGHT | REACTIVE_MANUAL_WRITE)) return; | ||
| e.u = t | REACTIVE_IN_HEAP_HEIGHT; | ||
| actualInsertIntoHeap(e, E); | ||
| } | ||
| function deleteFromHeap(e, E) { | ||
| const t = e.u; | ||
| if (!(t & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT))) return; | ||
| e.u = t & -25; | ||
| const n = e.qe; | ||
| if (e.ft === e) E.eE[n] = undefined; else { | ||
| const t = e._t; | ||
| const I = E.eE[n]; | ||
| const o = t ?? I; | ||
| if (e === I) E.eE[n] = t; else e.ft._t = t; | ||
| o.ft = e.ft; | ||
| } | ||
| e.ft = e; | ||
| e._t = undefined; | ||
| } | ||
| function markHeap(e) { | ||
| if (e.tE) return; | ||
| e.tE = true; | ||
| for (let E = 0; E <= e.EE; E++) { | ||
| for (let t = e.eE[E]; t !== undefined; t = t._t) { | ||
| if (t.u & REACTIVE_IN_HEAP) markNode(t); | ||
| } | ||
| } | ||
| } | ||
| function markNode(e, E = REACTIVE_DIRTY) { | ||
| const t = e.u; | ||
| if ((t & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= E) return; | ||
| e.u = t & -4 | E; | ||
| for (let E = e.p; E !== null; E = E.de) { | ||
| markNode(E.Ee, REACTIVE_CHECK); | ||
| } | ||
| if (e.G !== null) { | ||
| for (let E = e.G; E !== null; E = E.Ne) { | ||
| for (let e = E.p; e !== null; e = e.de) { | ||
| markNode(e.Ee, REACTIVE_CHECK); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| function runHeap(e, E) { | ||
| e.tE = false; | ||
| for (e.Le = 0; e.Le <= e.EE; e.Le++) { | ||
| let t = e.eE[e.Le]; | ||
| while (t !== undefined) { | ||
| if (t.u & REACTIVE_IN_HEAP) E(t); else adjustHeight(t, e); | ||
| t = e.eE[e.Le]; | ||
| } | ||
| } | ||
| e.EE = 0; | ||
| } | ||
| function adjustHeight(e, E) { | ||
| deleteFromHeap(e, E); | ||
| let t = e.qe; | ||
| for (let E = e.S; E; E = E.st) { | ||
| const e = E.ot; | ||
| const n = e.rt || e; | ||
| if (n.xe && n.qe >= t) t = n.qe + 1; | ||
| } | ||
| if (e.qe !== t) { | ||
| e.qe = t; | ||
| for (let E = e.p; E !== null; E = E.de) { | ||
| // Route each subscriber by its own zombie flag, mirroring the | ||
| // post-recompute height-adjust path. Inserting into the running `heap` | ||
| // unconditionally can park a zombie in `dirtyQueue` (or a live node in | ||
| // `zombieQueue`), breaking the flag/queue invariant `deleteFromHeap` | ||
| // relies on — the same corruption class as #2759. | ||
| insertIntoHeapHeight(E.Ee, queueFor(E.Ee)); | ||
| } | ||
| } | ||
| } | ||
| export { deleteFromHeap, enqueueSub, insertIntoHeap, insertIntoHeapHeight, markHeap, markNode, queueFor, runHeap }; |
| /** | ||
| * INV-2: a node with an *active* override must be registered for reversion in | ||
| * the queue's or a transition's `_optimisticNodes`. An unregistered active | ||
| * override would survive transition completion forever. Runs at the end of | ||
| * every flush (not just quiescence — the invariant holds mid-transition). | ||
| * (There is no revert-target requirement: authoritative values commit | ||
| * silently into `_value` under the override mask — A17 — so reverting is | ||
| * just dropping the override.) | ||
| */ | ||
| function devCheckActiveOverrides(e) { | ||
| return; | ||
| } | ||
| /** INV-1: an isPending() probe must never leak past its own call. */ function devCheckFlushStart() { | ||
| return; | ||
| } | ||
| /** | ||
| * Companion-vs-oracle census (#2838 pre-work). A NON-ASSERTING diff logger: | ||
| * at the end of every flush it compares each live companion's cached state | ||
| * against a fresh oracle and logs every distinct divergence fingerprint once | ||
| * (console, `[census]` prefix). Legit lane-scoped windows will show up too — | ||
| * the census exists to enumerate the full taxonomy of mid-flight divergence | ||
| * so the write-driven redesign knows every case it must produce, not to | ||
| * judge them. Enabled only when the COMPANION_CENSUS env var is set (in | ||
| * addition to `false`), so normal test runs pay one boolean check. | ||
| */ typeof globalThis !== "undefined" && !!globalThis.process?.env?.COMPANION_CENSUS; | ||
| function devCensusCompanions(e) { | ||
| return; | ||
| } | ||
| /** | ||
| * Quiescence checks. Run only when the system is fully drained: nothing | ||
| * scheduled, no active/stashed transitions, no live lanes. At that point no | ||
| * transition-scoped state may survive, and the lazily-created companions must | ||
| * agree with a fresh computation of their owner's state. | ||
| */ function devCheckQuiescent(e) { | ||
| return; | ||
| } | ||
| export { devCensusCompanions, devCheckActiveOverrides, devCheckFlushStart, devCheckQuiescent }; |
| import { NOT_PENDING } from "./constants.js"; | ||
| import { activeTransition } from "./scheduler.js"; | ||
| // Map from optimistic signal to its lane (reused for multiple writes to same signal) | ||
| const signalLanes = new WeakMap; | ||
| // All active lanes (for cleanup on transition completion) | ||
| const activeLanes = new Set; | ||
| /** | ||
| * Get an existing lane for a signal or create a new one. | ||
| * Reuses lane for multiple writes to the same signal. | ||
| */ function getOrCreateLane(n) { | ||
| let e = signalLanes.get(n); | ||
| if (e) { | ||
| return findLane(e); | ||
| } | ||
| // Detect parent lane: _parentSource chains from pendingSignal → pendingValueComputed → original. | ||
| // The child lane should not merge with the parent lane. | ||
| const i = n.en; | ||
| const a = i?.Je ? findLane(i.Je) : null; | ||
| e = { | ||
| an: n, | ||
| Pe: new Set, | ||
| tn: [ [], [] ], | ||
| rn: null, | ||
| ve: activeTransition, | ||
| sn: a | ||
| }; | ||
| signalLanes.set(n, e); | ||
| activeLanes.add(e); | ||
| // A companion may have written before the owner's first optimistic write | ||
| // (affects() as an action's first statement pokes the verdict companion of a | ||
| // still lane-less node, #2887), leaving its lane parentless. Adopt it now: | ||
| // parent-child is a property of the nodes, not of write order — otherwise | ||
| // the owner's write merges the companion's subscribers into this lane and | ||
| // their effects wait on its async instead of flushing immediately. | ||
| adoptCompanionLane(n.ye, e); | ||
| adoptCompanionLane(n.pe, e); | ||
| return e; | ||
| } | ||
| function adoptCompanionLane(n, e) { | ||
| if (!n) return; | ||
| const i = signalLanes.get(n); | ||
| if (!i) return; | ||
| const a = findLane(i); | ||
| // Only the companion's own unmerged root is safely re-parentable: a root | ||
| // that absorbed other lanes carries work that is not a child of this owner. | ||
| if (a !== e && a.an === n && !a.sn) a.sn = e; | ||
| } | ||
| /** | ||
| * Union-find: find the root lane. | ||
| */ function findLane(n) { | ||
| while (n.rn) n = n.rn; | ||
| return n; | ||
| } | ||
| /** | ||
| * Merge two lanes when their dependency graphs overlap. | ||
| */ function mergeLanes(n, e) { | ||
| n = findLane(n); | ||
| e = findLane(e); | ||
| if (n === e) return n; | ||
| e.rn = n; | ||
| // Move (not copy) the merged lane's work: after the merge all routing goes | ||
| // through findLane() to the root, so anything left behind here is dead — | ||
| // and anything *added* here later is a routing bug (INV-5). | ||
| for (const i of e.Pe) n.Pe.add(i); | ||
| e.Pe.clear(); | ||
| n.tn[0].push(...e.tn[0]); | ||
| n.tn[1].push(...e.tn[1]); | ||
| e.tn[0].length = 0; | ||
| e.tn[1].length = 0; | ||
| return n; | ||
| } | ||
| /** | ||
| * Resolve a node's lane: follow union-find chain, verify active, clear if stale. | ||
| */ function resolveLane(n) { | ||
| const e = n.Je; | ||
| if (!e) return undefined; | ||
| const i = findLane(e); | ||
| if (activeLanes.has(i)) return i; | ||
| n.Je = undefined; | ||
| return undefined; | ||
| } | ||
| function resolveTransition(n) { | ||
| return resolveLane(n)?.ve ?? n.ve; | ||
| } | ||
| /** | ||
| * Check if a node has an active optimistic override. | ||
| */ function hasActiveOverride(n) { | ||
| return !!(n.be !== undefined && n.be !== 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. | ||
| */ function assignOrMergeLane(n, e) { | ||
| const i = findLane(e); | ||
| const a = n.Je; | ||
| if (a) { | ||
| // If the subscriber's lane was merged into another lane, it's stale — | ||
| // replace it with the new source lane instead of following the merge chain | ||
| // (which would incorrectly merge the new lane into the old group) | ||
| if (a.rn) { | ||
| n.Je = e; | ||
| return; | ||
| } | ||
| const t = findLane(a); | ||
| if (activeLanes.has(t)) { | ||
| if (t !== i && !hasActiveOverride(n)) { | ||
| // Parent-child lanes stay independent so isPending resolves without | ||
| // waiting for the parent's async. The child keeps ownership. | ||
| if (i.sn && findLane(i.sn) === t) { | ||
| n.Je = e; | ||
| } else if (t.sn && findLane(t.sn) === i) ; else mergeLanes(i, t); | ||
| } | ||
| return; | ||
| } | ||
| } | ||
| n.Je = e; | ||
| } | ||
| export { activeLanes, assignOrMergeLane, findLane, getOrCreateLane, hasActiveOverride, mergeLanes, resolveLane, resolveTransition, signalLanes }; |
| import { NOT_PENDING, STATUS_UNINITIALIZED, STATUS_PENDING, CONFIG_OWNED_WRITE, REACTIVE_MANUAL_WRITE, REACTIVE_OPTIMISTIC_DIRTY, EFFECT_RENDER, EFFECT_USER } from "./constants.js"; | ||
| import { latestReadActive, stale, currentOptimisticLane } from "./core.js"; | ||
| import { NotReadyError } from "./error.js"; | ||
| import { enqueueSub } from "./heap.js"; | ||
| import "./invariants.js"; | ||
| import { resolveTransition, getOrCreateLane, hasActiveOverride, activeLanes, signalLanes, findLane, resolveLane, assignOrMergeLane } from "./lanes.js"; | ||
| import { GlobalQueue, activeTransition, globalQueue, clock, insertSubs, schedule, finalizePureQueue } from "./scheduler.js"; | ||
| /** | ||
| * The optimistic write engine, moved out of core.ts/scheduler.ts. Everything | ||
| * here serves only optimistic overrides — createOptimistic, | ||
| * createOptimisticStore (and its store-node writes), and the verdict layer's | ||
| * companions (which are optimistic nodes). Modules that can create optimistic | ||
| * state call `installOptimisticEngine()` before creating it; apps that never | ||
| * 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`, | ||
| * `_optimisticNodes.length`, `activeLanes.size`), so `!` invocations are safe | ||
| * once the gate holds — the same late-binding contract as verdict.ts. | ||
| */ | ||
| // When a background transition is stashed, plain optimistic signals need one | ||
| // committed-view rerun. Keep that override local to the stash flush. | ||
| let stashedOptimisticReads = null; | ||
| /** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */ function optimisticWrite(e, t) { | ||
| const n = e.be !== NOT_PENDING; | ||
| const i = n ? e.be : e.Ue; | ||
| if (typeof t === "function") t = t(i); | ||
| const u = !!(e.i & STATUS_UNINITIALIZED) || !e.Ge || !e.Ge(i, t); | ||
| if (!u) { | ||
| // Same-value write with an active override still entangles the current | ||
| // action's transition — the hold must outlast all overlapping actions. | ||
| if (n) { | ||
| const t = resolveTransition(e); | ||
| if (t && activeTransition !== t) globalQueue.initTransition(t); | ||
| } | ||
| return t; | ||
| } | ||
| if (n) globalQueue.initTransition(resolveTransition(e)); | ||
| // No revert target is stashed: while the override is active every reader | ||
| // sees it (A17), so authoritative arrivals commit silently into _value and | ||
| // reverting is just dropping the override — _value is already correct. | ||
| else globalQueue.Be.push(e); | ||
| const s = getOrCreateLane(e); | ||
| e.Je = s; | ||
| e.be = t; | ||
| GlobalQueue._e !== null && GlobalQueue._e(e, t); | ||
| e.Ie = clock; | ||
| insertSubs(e, true); | ||
| schedule(); | ||
| return t; | ||
| } | ||
| function readStashed(e) { | ||
| return !!stashedOptimisticReads?.has(e); | ||
| } | ||
| function queueStashedOptimisticEffects(e) { | ||
| for (let t = e.p; t !== null; t = t.de) { | ||
| const e = t.Ee; | ||
| if (!e.De) continue; | ||
| enqueueSub(e); | ||
| } | ||
| } | ||
| /** | ||
| * Incomplete-transition finalization when the stashed transition holds | ||
| * optimistic nodes: give plain optimistic signals one committed-view rerun. | ||
| */ function stashOptimistic(e) { | ||
| stashedOptimisticReads = new Set; | ||
| for (let t = 0; t < e.Be.length; t++) { | ||
| const n = e.Be[t]; | ||
| if (n.xe || n.U & CONFIG_OWNED_WRITE) continue; | ||
| stashedOptimisticReads.add(n); | ||
| queueStashedOptimisticEffects(n); | ||
| } | ||
| try { | ||
| finalizePureQueue(null, true); | ||
| } finally { | ||
| stashedOptimisticReads = null; | ||
| } | ||
| } | ||
| /** | ||
| * transitionComplete's override blockage: a settling transition stays open | ||
| * while one of its optimistic nodes holds an active override that is still | ||
| * pending on real (non-affects-sentinel) async. | ||
| */ function transitionBlocked(e) { | ||
| for (let t = 0; t < e.Be.length; t++) { | ||
| const n = e.Be[t]; | ||
| if (hasActiveOverride(n) && "i" in n && n.i & STATUS_PENDING && n.k instanceof NotReadyError && | ||
| // Mark-sourced pending never blocks settlement: affects() releases AT | ||
| // settle, so counting its sentinel here would deadlock the window it | ||
| // is scoped to. | ||
| !n.k.source?.l) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| function resolveOptimisticNodes(e) { | ||
| // Settlement writes below (snapCompanionsToState → updatePendingSignal-style | ||
| // notifications) may push fresh optimistic nodes; only this batch settles | ||
| // now, so iterate a fixed window and splice it out at the end. | ||
| const t = e.length; | ||
| for (let n = 0; n < t; n++) { | ||
| const t = e[n]; | ||
| t.Je = undefined; | ||
| // Revert is a pure drop: there is no revert target to commit — | ||
| // override-covered authoritative values hold in _pendingValue and | ||
| // elevate on their OWN transition's schedule (A18 as re-ruled 2026-07-07). | ||
| if (!(t.i & STATUS_PENDING)) t.i &= ~STATUS_UNINITIALIZED; | ||
| const i = t.be; | ||
| t.be = NOT_PENDING; | ||
| if (i !== NOT_PENDING && t.Ue !== i) insertSubs(t, true); | ||
| t.ve = null; | ||
| } | ||
| // Settlement checkpoint (#2838): companions caught in this batch (or owned | ||
| // by a node in it) re-derive from committed state, so verdicts survive the | ||
| // transition that produced them (A19 — pending is a property of the data). | ||
| for (let n = 0; n < t; n++) { | ||
| const t = e[n]; | ||
| if (t.ye || t.pe) GlobalQueue.R(t); | ||
| const i = t.en; | ||
| if (i && (i.ye === t || i.pe === t)) GlobalQueue.R(i); | ||
| } | ||
| e.splice(0, t); | ||
| } | ||
| function runQueue(e, t) { | ||
| for (let n = 0; n < e.length; n++) e[n](t); | ||
| } | ||
| /** | ||
| * Run effects from all lanes that are ready (no pending async). | ||
| */ function runLaneEffects(e) { | ||
| for (const t of activeLanes) { | ||
| if (t.rn || t.Pe.size > 0) continue; | ||
| const n = t.tn[e - 1]; | ||
| if (n.length) { | ||
| t.tn[e - 1] = []; | ||
| runQueue(n, e); | ||
| } | ||
| } | ||
| } | ||
| function cleanupCompletedLanes(e) { | ||
| for (const t of activeLanes) { | ||
| const n = e ? t.ve === e : !t.ve; | ||
| if (!n) continue; | ||
| if (!t.rn) { | ||
| if (t.tn[0].length) runQueue(t.tn[0], EFFECT_RENDER); | ||
| if (t.tn[1].length) runQueue(t.tn[1], EFFECT_USER); | ||
| } | ||
| if (t.an.Je === t) t.an.Je = undefined; | ||
| t.Pe.clear(); | ||
| t.tn[0].length = 0; | ||
| t.tn[1].length = 0; | ||
| activeLanes.delete(t); | ||
| signalLanes.delete(t.an); | ||
| } | ||
| } | ||
| /** read()'s per-lane suspension test (pending-throw path, lane context). */ function laneSuspends(e) { | ||
| // Per-lane suspension: only throw if in same lane as pending async | ||
| // AND the node doesn't have an active override (overrides are the visible value, | ||
| // downstream in the lane should read the override, not throw) | ||
| const t = e.Je; | ||
| if (!t) return false; | ||
| return findLane(t) === findLane(currentOptimisticLane) && !hasActiveOverride(e); | ||
| } | ||
| /** | ||
| * read()'s entanglement gate: a reader recomputing under an optimistic lane | ||
| * that reads a pending mid-transition write sees the committed value; the sub | ||
| * is recorded for replay at commit. | ||
| */ function gatedRead(e, t, n) { | ||
| if (latestReadActive || e.ge === NOT_PENDING || e.xe || t !== e && !(t.u & REACTIVE_MANUAL_WRITE)) { | ||
| return false; | ||
| } | ||
| activeTransition.Qt.add(n); | ||
| return true; | ||
| } | ||
| /** | ||
| * read()'s value selection under a lane: return the committed `_value` for | ||
| * optimistic/lane-assigned signals, stale-mode reads, and pending owners. | ||
| */ function laneReadsCommitted(e, t, n) { | ||
| return e.be !== undefined || !!e.Je || t === e && stale && n.en !== e || !!(t.i & STATUS_PENDING); | ||
| } | ||
| /** | ||
| * recompute()'s lane posture: resolve the node's own lane (own=true), or adopt | ||
| * a dependency's optimistic lane (own=false — parent-deeper-than-owned-child | ||
| * can run before its OPT-dirty child propagates). | ||
| */ function recomputeLane(e, t) { | ||
| if (t) return resolveLane(e) ?? null; | ||
| for (let t = e.S; t; t = t.st) { | ||
| const n = t.ot; | ||
| if (n.u & REACTIVE_OPTIMISTIC_DIRTY) { | ||
| const t = resolveLane(n); | ||
| if (t) { | ||
| e.u |= REACTIVE_OPTIMISTIC_DIRTY; | ||
| assignOrMergeLane(e, t); | ||
| return t; | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| /** recompute()'s catch path: track pending async in the current lane. */ function laneAsyncPending(e) { | ||
| const t = findLane(currentOptimisticLane); | ||
| if (t.an !== e) { | ||
| t.Pe.add(e); | ||
| e.Je = t; | ||
| GlobalQueue._ !== null && GlobalQueue._(t.an); | ||
| } | ||
| } | ||
| /** recompute()'s success path: the node's async settled, clear it from its lane. */ function laneAsyncSettled(e) { | ||
| const t = resolveLane(e); | ||
| if (t) { | ||
| t.Pe.delete(e); | ||
| GlobalQueue._ !== null && GlobalQueue._(t.an); | ||
| } | ||
| } | ||
| function trackOptimisticStore(e) { | ||
| // After initTransition, globalQueue._optimisticStores IS activeTransition._optimisticStores (same reference) | ||
| globalQueue.Lt.add(e); | ||
| schedule(); | ||
| } | ||
| /** | ||
| * Installs the engine's hooks. Idempotent; called by every module that can | ||
| * create optimistic state (verdict.ts at module top level, createOptimistic | ||
| * and createOptimisticStore at first call) BEFORE any optimistic node exists. | ||
| */ function installOptimisticEngine() { | ||
| if (GlobalQueue.bt !== null) return; | ||
| GlobalQueue.bt = optimisticWrite; | ||
| GlobalQueue.Ut = resolveOptimisticNodes; | ||
| GlobalQueue.yt = stashOptimistic; | ||
| GlobalQueue.Wt = transitionBlocked; | ||
| GlobalQueue.jt = cleanupCompletedLanes; | ||
| GlobalQueue.Mt = runLaneEffects; | ||
| GlobalQueue.gt = readStashed; | ||
| GlobalQueue.Dt = gatedRead; | ||
| GlobalQueue.ht = laneSuspends; | ||
| GlobalQueue.kt = laneReadsCommitted; | ||
| GlobalQueue.$e = recomputeLane; | ||
| GlobalQueue.et = laneAsyncPending; | ||
| GlobalQueue.Xe = laneAsyncSettled; | ||
| GlobalQueue.Vt = trackOptimisticStore; | ||
| } | ||
| export { installOptimisticEngine }; |
| import { defaultContext, CONFIG_TRANSPARENT, REACTIVE_DISPOSED, REACTIVE_ZOMBIE, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT } from "./constants.js"; | ||
| import { context, runWithOwner, pendingCheckActive, latestReadActive, tracking } from "./core.js"; | ||
| import { unlinkSubs } from "./graph.js"; | ||
| import { deleteFromHeap, queueFor, insertIntoHeap, insertIntoHeapHeight } from "./heap.js"; | ||
| import { globalQueue, GlobalQueue, zombieQueue, dirtyQueue } from "./scheduler.js"; | ||
| const PENDING_OWNER = {}; | ||
| // Dummy owner to trigger store's read() path | ||
| function markDisposal(e) { | ||
| let n = e.Fe; | ||
| while (n) { | ||
| const e = n.u; | ||
| n.u = e | REACTIVE_ZOMBIE; | ||
| // migrate height-adjust entries too, not just recompute entries: every | ||
| // `deleteFromHeap` call site picks the queue from the zombie flag, so a | ||
| // node left physically linked in `dirtyQueue` after being zombified gets | ||
| // unlinked from the wrong queue on dispose, corrupting the bucket and | ||
| // livelocking the next `runHeap` that reaches it (#2759) | ||
| if (e & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT)) { | ||
| deleteFromHeap(n, e & REACTIVE_ZOMBIE ? zombieQueue : dirtyQueue); | ||
| if (e & REACTIVE_IN_HEAP) insertIntoHeap(n, zombieQueue); else insertIntoHeapHeight(n, zombieQueue); | ||
| } | ||
| markDisposal(n); | ||
| n = n.Ve; | ||
| } | ||
| } | ||
| function dispose(e) { | ||
| let n = e.S; | ||
| while (n !== null) { | ||
| n = unlinkSubs(n); | ||
| } | ||
| e.S = null; | ||
| e.Ke = null; | ||
| disposeChildren(e, true); | ||
| } | ||
| function disposeChildren(e, n = false, t) { | ||
| const i = e.u; | ||
| if (i & REACTIVE_DISPOSED) return; | ||
| if (n) { | ||
| e.u = i | REACTIVE_DISPOSED; | ||
| // Companions are created detached and outlive their owner, but a verdict | ||
| // must not: a disposed source can never settle, so an isPending companion | ||
| // latched `true` here would hold a spinner forever (INV-9, the PR #2845 | ||
| // edge). Snap runs after the DISPOSED flag is set so the oracle reads | ||
| // false, and notifies subscribers still watching the companion. | ||
| const n = e; | ||
| if (n.ye || n.pe) GlobalQueue.R(n); | ||
| } | ||
| if (n && e.xe) e.Ae = null; | ||
| let l = t ? e.Ze : e.Fe; | ||
| while (l) { | ||
| const e = l.Ve; | ||
| if (l.S) { | ||
| const e = l; | ||
| deleteFromHeap(e, queueFor(e)); | ||
| let n = e.S; | ||
| do { | ||
| n = unlinkSubs(n); | ||
| } while (n !== null); | ||
| e.S = null; | ||
| e.Ke = null; | ||
| } | ||
| disposeChildren(l, true); | ||
| l = e; | ||
| } | ||
| if (t) { | ||
| e.Ze = null; | ||
| } else { | ||
| e.Fe = null; | ||
| e.je = 0; | ||
| } | ||
| // O(1) splice out of parent's chain on individual dispose. Skipped during | ||
| // batch dispose (parent already disposed) and zombie disposal (node sits on | ||
| // parent's _pendingFirstChild). We leave node._nextSibling intact so outer | ||
| // walks that already advanced past us still reach later siblings. | ||
| if (n && !t && !(i & REACTIVE_ZOMBIE) && e.He !== null && !(e.He.u & REACTIVE_DISPOSED)) { | ||
| const n = e.Tt; | ||
| const t = e.Ve; | ||
| if (n !== null) n.Ve = t; else e.He.Fe = t; | ||
| if (t !== null) t.Tt = n; | ||
| e.Tt = null; | ||
| } | ||
| runDisposal(e, t); | ||
| // Final effect-returned cleanup fires at true disposal, after `_disposal` | ||
| // to mirror rerun ordering (compute-phase teardown first, cleanup last). | ||
| if (n && e.At) { | ||
| const n = e.At; | ||
| e.At = undefined; | ||
| n(); | ||
| } | ||
| } | ||
| function runDisposal(e, n) { | ||
| let t = n ? e.We : e.Me; | ||
| if (!t) return; | ||
| if (Array.isArray(t)) { | ||
| for (let e = 0; e < t.length; e++) { | ||
| const n = t[e]; | ||
| n.call(n); | ||
| } | ||
| } else { | ||
| t.call(t); | ||
| } | ||
| n ? e.We = null : e.Me = null; | ||
| } | ||
| function childId(e, n) { | ||
| let t = e; | ||
| while (t.U & CONFIG_TRANSPARENT && t.He) t = t.He; | ||
| if (t.id != null) return formatId(t.id, n ? t.je++ : t.je); | ||
| throw new Error(""); | ||
| } | ||
| /** | ||
| * Allocates and returns the next stable child id for `owner`. Used by | ||
| * hydration plumbing and `createUniqueId`. Not part of the user-facing API. | ||
| * | ||
| * @internal | ||
| */ function getNextChildId(e) { | ||
| return childId(e, true); | ||
| } | ||
| /** | ||
| * The id a freshly-created node inherits: an explicit `options.id` wins; | ||
| * transparent nodes share their parent's id; otherwise the parent's next | ||
| * child id is consumed (or `undefined` outside an id-carrying tree). | ||
| */ function inheritId(e, n, t) { | ||
| return e?.id ?? (n ? t?.id : t?.id != null ? getNextChildId(t) : undefined); | ||
| } | ||
| /** | ||
| * Returns the *next* child id for `owner` without consuming it. Used by | ||
| * hydration plumbing to peek at the id a future child will receive. | ||
| * | ||
| * @internal | ||
| */ function peekNextChildId(e) { | ||
| return childId(e, false); | ||
| } | ||
| function formatId(e, n) { | ||
| const t = n.toString(36), i = t.length - 1; | ||
| return e + (i ? String.fromCharCode(64 + i) : "") + t; | ||
| } | ||
| /** | ||
| * Returns the currently-tracking observer (the computation that subscribes to | ||
| * reactive reads at this point), or `null` if reads here would be untracked. | ||
| * Used by reactive primitives that need to know whether they're inside a | ||
| * tracking scope. App code rarely needs this — see `getOwner()` for the | ||
| * lifecycle owner instead. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Library predicate: only register a hot-path subscription when the | ||
| * // caller is inside a tracking scope (memo / effect compute / JSX). | ||
| * function trackIfTracked(source: () => unknown) { | ||
| * if (getObserver()) source(); | ||
| * } | ||
| * ``` | ||
| */ function getObserver() { | ||
| if (pendingCheckActive || latestReadActive) return PENDING_OWNER; | ||
| return tracking ? context : null; | ||
| } | ||
| /** | ||
| * Returns the current reactive **owner** — the lifecycle node that the next | ||
| * `cleanup()` / `onCleanup()` / `createSignal()` etc. will be attached to. | ||
| * | ||
| * Returns `null` if called outside any owner. Capture the owner with | ||
| * `getOwner()` and re-enter it later with `runWithOwner(owner, fn)` to attach | ||
| * disposables created from a callback (event handler, async resolution, etc.) | ||
| * back to a component's lifecycle. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * function defer<T>(fn: () => T) { | ||
| * const owner = getOwner(); | ||
| * queueMicrotask(() => runWithOwner(owner, fn)); | ||
| * } | ||
| * ``` | ||
| */ function getOwner() { | ||
| return context; | ||
| } | ||
| /** | ||
| * Low-level: registers `fn` as a disposal callback on the current owner. | ||
| * Most code should use `onCleanup()` from `solid-js`, which adds dev-mode | ||
| * checks. `cleanup()` is the unchecked primitive used by internals. | ||
| */ function cleanup(e) { | ||
| if (!context) return e; | ||
| if (!context.Me) context.Me = e; else if (Array.isArray(context.Me)) context.Me.push(e); else context.Me = [ context.Me, e ]; | ||
| return e; | ||
| } | ||
| /** | ||
| * Returns `true` if the owner has been disposed (or marked zombie pending | ||
| * disposal). Pair with a captured owner to bail out of late callbacks whose | ||
| * surrounding component already unmounted. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * function onSettleSafe(fn: () => void) { | ||
| * const owner = getOwner(); | ||
| * queueMicrotask(() => { | ||
| * if (owner && isDisposed(owner)) return; // component unmounted; skip | ||
| * runWithOwner(owner, fn); | ||
| * }); | ||
| * } | ||
| * ``` | ||
| */ function isDisposed(e) { | ||
| return !!(e.u & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE)); | ||
| } | ||
| function disposeRootSelf(e = true) { | ||
| disposeChildren(this, e); | ||
| } | ||
| /** | ||
| * Creates a fresh owner attached as a child of the current owner (or as a | ||
| * detached root if there is none). Used by framework internals to group | ||
| * cleanups; app code should use `createRoot()` (host a reactive scope outside | ||
| * a component) or `runWithOwner()` (re-enter a captured owner). | ||
| * | ||
| * @internal | ||
| */ function createOwner(e) { | ||
| const n = context; | ||
| const t = e?.transparent ?? false; | ||
| const i = { | ||
| id: inheritId(e, t, n), | ||
| U: t ? CONFIG_TRANSPARENT : 0, | ||
| dt: true, | ||
| Ct: n?.dt ? n.Ct : n, | ||
| Fe: null, | ||
| Ve: null, | ||
| Tt: null, | ||
| Me: null, | ||
| v: n?.v ?? globalQueue, | ||
| we: n?.we || defaultContext, | ||
| je: 0, | ||
| We: null, | ||
| Ze: null, | ||
| He: n, | ||
| dispose: disposeRootSelf | ||
| }; | ||
| if (n) { | ||
| const e = n.Fe; | ||
| if (e === null) { | ||
| n.Fe = i; | ||
| } else { | ||
| i.Ve = e; | ||
| e.Tt = i; | ||
| n.Fe = i; | ||
| } | ||
| } | ||
| return i; | ||
| } | ||
| /** | ||
| * Creates a detached reactive root. The callback receives a `dispose()` | ||
| * function which, when called, tears down every signal, memo, effect, and | ||
| * `onCleanup` registered inside the root. | ||
| * | ||
| * Use this to host long-lived reactive scopes outside of a component (custom | ||
| * controllers, app bootstrapping, tests). Inside a component, prefer | ||
| * letting Solid's component lifecycle own things. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const dispose = createRoot(dispose => { | ||
| * const [n, setN] = createSignal(0); | ||
| * createEffect(() => n(), value => console.log(value)); | ||
| * setInterval(() => setN(x => x + 1), 1000); | ||
| * return dispose; | ||
| * }); | ||
| * | ||
| * // Later, to tear everything down: | ||
| * dispose(); | ||
| * ``` | ||
| * | ||
| * @description https://docs.solidjs.com/reference/reactive-utilities/create-root | ||
| */ function createRoot(e, n) { | ||
| const t = createOwner(n); | ||
| return runWithOwner(t, () => e(() => t.dispose())); | ||
| } | ||
| export { cleanup, createOwner, createRoot, dispose, disposeChildren, getNextChildId, getObserver, getOwner, inheritId, isDisposed, markDisposal, peekNextChildId }; |
| import { EFFECT_RENDER, EFFECT_USER, STATUS_PENDING, REACTIVE_DISPOSED, REACTIVE_REASK, CONFIG_IN_SNAPSHOT_SCOPE, REACTIVE_SNAPSHOT_STALE, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_ZOMBIE, NOT_PENDING, EFFECT_TRACKED, REACTIVE_MANUAL_WRITE, STATUS_UNINITIALIZED } from "./constants.js"; | ||
| import { currentOptimisticLane } from "./core.js"; | ||
| import { DEV } from "./dev.js"; | ||
| import { NotReadyError } from "./error.js"; | ||
| import { runHeap, enqueueSub } from "./heap.js"; | ||
| import { activeLanes, assignOrMergeLane, findLane } from "./lanes.js"; | ||
| export { getOrCreateLane, hasActiveOverride, mergeLanes, resolveLane } from "./lanes.js"; | ||
| import { devCheckFlushStart, devCheckActiveOverrides, devCensusCompanions, devCheckQuiescent } from "./invariants.js"; | ||
| const transitions = new Set; | ||
| const dirtyQueue = { | ||
| eE: new Array(2e3).fill(undefined), | ||
| tE: false, | ||
| Le: 0, | ||
| EE: 0 | ||
| }; | ||
| const zombieQueue = { | ||
| eE: new Array(2e3).fill(undefined), | ||
| tE: false, | ||
| Le: 0, | ||
| EE: 0 | ||
| }; | ||
| let clock = 0; | ||
| let activeTransition = null; | ||
| let scheduled = false; | ||
| let halted = false; | ||
| let haltNotified = false; | ||
| let syncDepth = 0; | ||
| let projectionWriteActive = false; | ||
| // Store property nodes that were created solely to carry a pending write (no | ||
| // subscribers at write time). Swept after each flush that commits pending | ||
| // values — any still without subs get disposed via their `_unobserved` hook, | ||
| // releasing the slot in the parent store's node map. | ||
| const transientStoreNodes = new Set; | ||
| function registerTransientStoreNode(e) { | ||
| transientStoreNodes.add(e); | ||
| } | ||
| function canUseSimpleSyncFlush(e) { | ||
| return transitions.size === 0 && activeLanes.size === 0 && e.wt.length === 0 && e.Be.length === 0 && e.N.length === 0 && e.Lt.size === 0 && transientStoreNodes.size === 0; | ||
| } | ||
| function sweepTransientStoreNodes() { | ||
| if (transientStoreNodes.size === 0) return; | ||
| for (const e of transientStoreNodes) { | ||
| if (e.p !== null) { | ||
| transientStoreNodes.delete(e); | ||
| continue; | ||
| } | ||
| if (e.ge !== NOT_PENDING) continue; | ||
| if (e.be !== undefined && e.be !== 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). | ||
| if (e.P) continue; | ||
| transientStoreNodes.delete(e); | ||
| e.ct?.(); | ||
| } | ||
| } | ||
| /** | ||
| * Toggles the dev-mode "must be inside a `<Loading>` boundary" enforcement | ||
| * window. Only `render()` calls this — wrapping the initial mount so that a | ||
| * top-level uncaught async read surfaces the diagnostic. Not part of the | ||
| * user-facing API. | ||
| * | ||
| * @internal | ||
| */ function enforceLoadingBoundary(e) {} | ||
| function setProjectionWriteActive(e) { | ||
| projectionWriteActive = e; | ||
| } | ||
| function mergeTransitionState(e, t) { | ||
| t.Ht = e; | ||
| e.Te.push(...t.Te); | ||
| for (const i of activeLanes) if (i.ve === t) i.ve = e; | ||
| if (t.Be.length) { | ||
| // Move (don't copy): the global queue may still alias the outgoing | ||
| // array, and the adoption pass in initTransition would re-push its | ||
| // contents into the target — duplicating every entry. | ||
| e.Be.push(...t.Be); | ||
| t.Be.length = 0; | ||
| } | ||
| if (t.N.length) { | ||
| // Move (don't copy): the global queue may still alias the outgoing | ||
| // array, and the adoption pass in initTransition would re-push its | ||
| // contents into the target — double-releasing every mark. | ||
| e.N.push(...t.N); | ||
| t.N.length = 0; | ||
| } | ||
| for (const i of t.Lt) e.Lt.add(i); | ||
| for (const [i, n] of t.qt) { | ||
| let t = e.qt.get(i); | ||
| if (!t) e.qt.set(i, t = new Set); | ||
| for (const e of n) t.add(e); | ||
| } | ||
| for (const i of t.Qt) e.Qt.add(i); | ||
| } | ||
| function schedule() { | ||
| if (halted) { | ||
| notifyHalted(); | ||
| return; | ||
| } | ||
| if (scheduled) return; | ||
| scheduled = true; | ||
| if (!syncDepth && !globalQueue.Bt && !projectionWriteActive) queueMicrotask(flush); | ||
| } | ||
| /** | ||
| * Permanently halts the reactive system. Called when a user error escapes | ||
| * every boundary — app state is undefined at that point, so scheduling stops | ||
| * entirely rather than limping along with a half-applied update. | ||
| */ function haltReactivity(e) { | ||
| if (halted) return; | ||
| halted = true; | ||
| let t = "[REACTIVITY_HALTED]"; | ||
| // Log the cause here too: callers rethrow it, but a creation-time throw | ||
| // unwinds through ancestor recomputes that convert it to status instead of | ||
| // surfacing it (#2884), so the rethrow alone cannot guarantee visibility. | ||
| e === undefined ? console.error(t) : console.error(t, e); | ||
| } | ||
| // Logs on the first write after a halt so a frozen interaction is traceable. | ||
| function notifyHalted() { | ||
| if (haltNotified) return; | ||
| haltNotified = true; | ||
| console.error("[REACTIVITY_HALTED]"); | ||
| } | ||
| /** @internal Test/dev-reload hook. Revives scheduling after a halt. */ function resetErrorHalt() { | ||
| halted = false; | ||
| haltNotified = false; | ||
| } | ||
| class Queue { | ||
| He=null; | ||
| zt=[ [], [] ]; | ||
| wt=[]; | ||
| created=clock; | ||
| addChild(e) { | ||
| this.wt.push(e); | ||
| e.He = this; | ||
| } | ||
| removeChild(e) { | ||
| const t = this.wt.indexOf(e); | ||
| if (t >= 0) { | ||
| this.wt.splice(t, 1); | ||
| e.He = null; | ||
| } | ||
| } | ||
| notify(e, t, i, n) { | ||
| if (this.He) return this.He.notify(e, t, i, n); | ||
| return false; | ||
| } | ||
| run(e) { | ||
| if (this.zt[e - 1].length) { | ||
| const t = this.zt[e - 1]; | ||
| this.zt[e - 1] = []; | ||
| runQueue(t, e); | ||
| } | ||
| for (let t = 0; t < this.wt.length; t++) this.wt[t].run?.(e); | ||
| } | ||
| enqueue(e, t) { | ||
| if (e) { | ||
| // Route to lane's effect queue if we're in an optimistic recomputation | ||
| if (currentOptimisticLane) { | ||
| const i = findLane(currentOptimisticLane); | ||
| i.tn[e - 1].push(t); | ||
| } else { | ||
| this.zt[e - 1].push(t); | ||
| } | ||
| } | ||
| schedule(); | ||
| } | ||
| stashQueues(e) { | ||
| e.zt[0].push(...this.zt[0]); | ||
| e.zt[1].push(...this.zt[1]); | ||
| this.zt = [ [], [] ]; | ||
| for (let t = 0; t < this.wt.length; t++) { | ||
| let i = this.wt[t]; | ||
| let n = e.wt[t]; | ||
| if (!n) { | ||
| n = { | ||
| zt: [ [], [] ], | ||
| wt: [] | ||
| }; | ||
| e.wt[t] = n; | ||
| } | ||
| i.stashQueues(n); | ||
| } | ||
| } | ||
| restoreQueues(e) { | ||
| this.zt[0].push(...e.zt[0]); | ||
| this.zt[1].push(...e.zt[1]); | ||
| for (let t = 0; t < e.wt.length; t++) { | ||
| const i = e.wt[t]; | ||
| let n = this.wt[t]; | ||
| if (n) n.restoreQueues(i); | ||
| } | ||
| } | ||
| } | ||
| class GlobalQueue extends Queue { | ||
| Bt=false; | ||
| xt=null; | ||
| Yt=[]; | ||
| Be=[]; | ||
| N=[]; | ||
| Lt=new Set; | ||
| static Ce; | ||
| static Oe; | ||
| static ut; | ||
| static Kt=null; | ||
| // Store-side hook: drops a keyless affects() mark's identity scope when the | ||
| // carrier node's last registration releases (wired by store.ts, mirroring | ||
| // _clearOptimisticStore). | ||
| static T=null; | ||
| // affects()-side hooks (wired by affects.ts, mirroring _update): the mark | ||
| // engine — count/register/release plus the post-commit re-application of | ||
| // marked reads — lives with the feature. Every call site is gated by state | ||
| // only that module creates, so `!` invocations are safe once the gate holds. | ||
| static j=null; | ||
| static h=null; | ||
| static D=null; | ||
| static F=null; | ||
| static $=null; | ||
| static I=null; | ||
| // External-source bridge (wired by enableExternalSource(); null while no | ||
| // config is active — including after _resetExternalSourceConfig()). | ||
| static Ot=null; | ||
| static Rt=null; | ||
| // Verdict-layer hooks (wired by verdict.ts when isPending()/latest() are | ||
| // imported; null in apps that never use them). Call sites either guard for | ||
| // null or sit behind state only the verdict layer can create (`!` is safe | ||
| // there: `_pendingSignal`/`_latestValueComputed` are only ever assigned by | ||
| // verdict.ts, and `pendingCheckActive`/`latestReadActive` only flip inside | ||
| // isPending()/latest()). | ||
| static _e=null; | ||
| static _=null; | ||
| static me=null; | ||
| static R=null; | ||
| static Gt=null; | ||
| static Pt=null; | ||
| static vt=null; | ||
| static tt=null; | ||
| static nt=null; | ||
| static Zt=null; | ||
| // Optimistic-engine hooks (wired by core/optimistic.ts via | ||
| // installOptimisticEngine(), called from verdict.ts / createOptimistic / | ||
| // createOptimisticStore — every module that can create optimistic state). | ||
| // Call sites are gated by state only the engine can create: an | ||
| // `_overrideValue` slot, a lane in `activeLanes`, an `_optimisticNodes` | ||
| // entry, or a non-null `currentOptimisticLane`, so `!` invocations are safe | ||
| // once the gate holds. | ||
| static bt=null; | ||
| static Ut=null; | ||
| static yt=null; | ||
| static Wt=null; | ||
| static jt=null; | ||
| static Mt=null; | ||
| static gt=null; | ||
| static Dt=null; | ||
| static ht=null; | ||
| static kt=null; | ||
| static $e=null; | ||
| static et=null; | ||
| static Xe=null; | ||
| static Vt=null; | ||
| flush() { | ||
| if (this.Bt) return; | ||
| this.Bt = true; | ||
| try { | ||
| if (false) ; | ||
| runHeap(dirtyQueue, GlobalQueue.Ce); | ||
| if (activeTransition) { | ||
| const e = transitionComplete(activeTransition); | ||
| if (!e) { | ||
| const e = activeTransition; | ||
| runHeap(zombieQueue, GlobalQueue.Ce); | ||
| this.xt = null; | ||
| this.Yt = []; | ||
| this.Be = []; | ||
| this.N = []; | ||
| this.Lt = new Set; | ||
| // Run lane effects immediately (before stashing) - lanes with no pending async | ||
| if (activeLanes.size) { | ||
| GlobalQueue.Mt(EFFECT_RENDER); | ||
| GlobalQueue.Mt(EFFECT_USER); | ||
| } | ||
| this.stashQueues(e.Jt); | ||
| clock++; | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.Le; | ||
| reassignPendingTransition(e.Yt); | ||
| activeTransition = null; | ||
| // The stash pass (committed-view rerun of plain optimistic signals) | ||
| // wraps finalizePureQueue in the engine; a non-empty _optimisticNodes | ||
| // means _optimisticWrite ran, which installed the hook. | ||
| if (!e.Te.length && !e.qt.size && e.Be.length) { | ||
| GlobalQueue.yt(e); | ||
| } else { | ||
| finalizePureQueue(null, true); | ||
| } | ||
| return; | ||
| } | ||
| this.Yt !== activeTransition.Yt && this.Yt.push(...activeTransition.Yt); | ||
| this.restoreQueues(activeTransition.Jt); | ||
| transitions.delete(activeTransition); | ||
| const t = activeTransition; | ||
| activeTransition = null; | ||
| reassignPendingTransition(this.Yt); | ||
| finalizePureQueue(t); | ||
| } else { | ||
| if (canUseSimpleSyncFlush(this)) { | ||
| commitPendingNodes(); | ||
| if (dirtyQueue.EE >= dirtyQueue.Le) { | ||
| runHeap(dirtyQueue, GlobalQueue.Ce); | ||
| commitPendingNodes(); | ||
| } | ||
| } else { | ||
| if (transitions.size) runHeap(zombieQueue, GlobalQueue.Ce); | ||
| finalizePureQueue(); | ||
| } | ||
| } | ||
| clock++; | ||
| // Check if finalization added items to the heap (from optimistic reversion) | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.Le; | ||
| // Run lane effects first (for ready lanes), then regular effects | ||
| activeLanes.size && GlobalQueue.Mt(EFFECT_RENDER); | ||
| this.run(EFFECT_RENDER); | ||
| activeLanes.size && GlobalQueue.Mt(EFFECT_USER); | ||
| this.run(EFFECT_USER); | ||
| if (false) ; | ||
| if (false && !scheduled && !activeTransition && transitions.size === 0 && activeLanes.size === 0) ; | ||
| if (false) ; | ||
| } finally { | ||
| this.Bt = false; | ||
| } | ||
| } | ||
| notify(e, t, i, n) { | ||
| // Only track async if the boundary is propagating STATUS_PENDING (not caught by boundary) | ||
| if (t & STATUS_PENDING) { | ||
| if (i & STATUS_PENDING) { | ||
| const t = n !== undefined ? n : e.k; | ||
| if (activeTransition && t) { | ||
| const i = t.source; | ||
| let n = activeTransition.qt.get(i); | ||
| if (!n) activeTransition.qt.set(i, n = new Set); | ||
| const s = n.size; | ||
| n.add(e); | ||
| if (n.size !== s) schedule(); | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| initTransition(e) { | ||
| if (e) e = currentTransition(e); | ||
| if (e && e === activeTransition) return; | ||
| if (!e && activeTransition && activeTransition.Ie === clock) return; | ||
| if (!activeTransition) { | ||
| activeTransition = e ?? { | ||
| Ie: clock, | ||
| Yt: [], | ||
| qt: new Map, | ||
| Be: [], | ||
| N: [], | ||
| Lt: new Set, | ||
| Te: [], | ||
| Jt: { | ||
| zt: [ [], [] ], | ||
| wt: [] | ||
| }, | ||
| Ht: false, | ||
| Qt: new Set | ||
| }; | ||
| } else if (e) { | ||
| const t = activeTransition; | ||
| mergeTransitionState(e, t); | ||
| transitions.delete(t); | ||
| activeTransition = e; | ||
| } | ||
| transitions.add(activeTransition); | ||
| activeTransition.Ie = clock; | ||
| if (this.xt !== null) { | ||
| this.xt.ve = activeTransition; | ||
| activeTransition.Yt.push(this.xt); | ||
| this.xt = null; | ||
| } | ||
| if (this.Yt !== activeTransition.Yt) { | ||
| for (let e = 0; e < this.Yt.length; e++) { | ||
| const t = this.Yt[e]; | ||
| t.ve = activeTransition; | ||
| activeTransition.Yt.push(t); | ||
| } | ||
| this.Yt = activeTransition.Yt; | ||
| } | ||
| if (this.Be !== activeTransition.Be) { | ||
| for (let e = 0; e < this.Be.length; e++) { | ||
| const t = this.Be[e]; | ||
| t.ve = activeTransition; | ||
| activeTransition.Be.push(t); | ||
| } | ||
| this.Be = activeTransition.Be; | ||
| } | ||
| if (this.N !== activeTransition.N) { | ||
| // Adopt ambient marks into the transaction (marks don't hijack the | ||
| // node's _transition — a mark on a plain signal must not entangle | ||
| // unrelated writes to it; the same rule holds one hop downstream: | ||
| // propagation never queues pended subscribers as pending nodes, see | ||
| // propagateAffectsMark, #2893). After adoption the queue aliases the | ||
| // transition's array, so later registrations land there directly. | ||
| activeTransition.N.push(...this.N); | ||
| this.N = activeTransition.N; | ||
| } | ||
| for (const e of activeLanes) { | ||
| if (!e.ve) e.ve = activeTransition; | ||
| } | ||
| if (this.Lt !== activeTransition.Lt) { | ||
| for (const e of this.Lt) activeTransition.Lt.add(e); | ||
| this.Lt = activeTransition.Lt; | ||
| } | ||
| } | ||
| } | ||
| function queuePendingNode(e) { | ||
| if (activeTransition) { | ||
| globalQueue.Yt.push(e); | ||
| return; | ||
| } | ||
| if (globalQueue.xt === null && globalQueue.Yt.length === 0) { | ||
| globalQueue.xt = e; | ||
| return; | ||
| } | ||
| if (globalQueue.xt !== null) { | ||
| globalQueue.Yt.push(globalQueue.xt); | ||
| globalQueue.xt = null; | ||
| } | ||
| globalQueue.Yt.push(e); | ||
| } | ||
| // Sticky: flips true on the first refresh() ever (the only setter of | ||
| // REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag | ||
| // clear entirely in apps that never refresh. | ||
| let reaskArmed = false; | ||
| function armReaskClear() { | ||
| reaskArmed = true; | ||
| } | ||
| function insertSubs(e, t = false) { | ||
| // Get source lane: prefer node's own lane over current context | ||
| // This is important for isPending signals which need their own lane to flush immediately | ||
| const i = e.Je || currentOptimisticLane; | ||
| const n = e.Ye !== undefined; | ||
| const s = reaskArmed; | ||
| for (let a = e.p; a !== null; a = a.de) { | ||
| // A value-change notification is a new question for the subscriber: any | ||
| // pending re-ask mark (refresh) it carried is superseded. | ||
| if (s) a.Ee.u &= ~REACTIVE_REASK; | ||
| if (n && a.Ee.U & CONFIG_IN_SNAPSHOT_SCOPE) { | ||
| a.Ee.u |= REACTIVE_SNAPSHOT_STALE; | ||
| continue; | ||
| } | ||
| if (t && i) { | ||
| a.Ee.u |= REACTIVE_OPTIMISTIC_DIRTY; | ||
| assignOrMergeLane(a.Ee, i); | ||
| } else if (t) { | ||
| a.Ee.u |= REACTIVE_OPTIMISTIC_DIRTY; | ||
| // No source lane means reversion - clear subscriber's lane so effects go to regular queue | ||
| a.Ee.Je = undefined; | ||
| } | ||
| enqueueSub(a.Ee); | ||
| } | ||
| } | ||
| function commitPendingNode(e) { | ||
| const t = e; | ||
| if (!t.xe) { | ||
| if (e.ge !== NOT_PENDING) { | ||
| e.Ue = e.ge; | ||
| e.ge = NOT_PENDING; | ||
| } | ||
| if (e.ye || e.pe) GlobalQueue.R(e); | ||
| return; | ||
| } | ||
| if (e.ge !== NOT_PENDING) { | ||
| e.Ue = e.ge; | ||
| e.ge = NOT_PENDING; | ||
| // Set _modified for effects, but not for tracked effects (they handle their own scheduling) | ||
| if (e.De && e.De !== EFFECT_TRACKED) e.it = true; | ||
| } | ||
| t.u &= ~REACTIVE_MANUAL_WRITE; | ||
| if (!(t.i & STATUS_PENDING)) t.i &= ~STATUS_UNINITIALIZED; | ||
| if (t.Ze !== null || t.We !== null) GlobalQueue.Oe(t, false, true); | ||
| if (e.ye || e.pe) GlobalQueue.R(e); | ||
| } | ||
| function commitPendingNodes() { | ||
| if (globalQueue.xt !== null) { | ||
| commitPendingNode(globalQueue.xt); | ||
| globalQueue.xt = null; | ||
| } | ||
| const e = globalQueue.Yt; | ||
| for (let t = 0; t < e.length; t++) { | ||
| commitPendingNode(e[t]); | ||
| } | ||
| e.length = 0; | ||
| } | ||
| function finalizePureQueue(e = null, t = false) { | ||
| // For incomplete transitions, skip pending resolution and optimistic reversion | ||
| // For completing transitions or no-transition, resolve pending and revert optimistic | ||
| const i = !t; | ||
| if (i) commitPendingNodes(); | ||
| if (!t && globalQueue.wt.length) checkBoundaryChildren(globalQueue); | ||
| const n = dirtyQueue.EE >= dirtyQueue.Le; | ||
| if (n) runHeap(dirtyQueue, GlobalQueue.Ce); | ||
| if (i) { | ||
| if (n) commitPendingNodes(); | ||
| // Optimistic reversion: a non-empty batch means _optimisticWrite ran, | ||
| // which installed the engine's hooks. | ||
| const t = e ? e.Be : globalQueue.Be; | ||
| if (t.length) GlobalQueue.Ut(t); | ||
| // Replay entanglement: subs recorded by the read-time gate get rescheduled | ||
| // so they re-run with the now-committed values visible. | ||
| if (e && e.Qt.size) { | ||
| for (const t of e.Qt) { | ||
| if (t.u & REACTIVE_DISPOSED) continue; | ||
| enqueueSub(t); | ||
| } | ||
| e.Qt.clear(); | ||
| } | ||
| // Declared motion ends with the transaction: settle (or plain flush end | ||
| // for ambient marks) releases each registration's refcount. A non-empty | ||
| // batch means registerAffectsMark ran, which installed the hook. | ||
| const i = e ? e.N : globalQueue.N; | ||
| if (i.length) GlobalQueue.h(i); | ||
| const s = e ? e.Lt : globalQueue.Lt; | ||
| // A non-empty set means trackOptimisticStore ran, which installed the | ||
| // hook; the hook iterates, clears, and schedules (keeping the loop out of | ||
| // core lets esbuild shake it — rollup already folds the null guard). | ||
| if (s.size) GlobalQueue.Kt(s); | ||
| sweepTransientStoreNodes(); | ||
| // Lanes only enter activeLanes through the engine's getOrCreateLane. | ||
| if (activeLanes.size) GlobalQueue.jt(e); | ||
| } | ||
| } | ||
| function checkBoundaryChildren(e) { | ||
| for (const t of e.wt) { | ||
| t.Se?.(); | ||
| checkBoundaryChildren(t); | ||
| } | ||
| } | ||
| /** | ||
| * Count of live `affects()` registrations across the system (including | ||
| * store-scope inherited marks). Gates the read-path mark check in `read()` so | ||
| * graphs that never use the feature pay one integer compare. | ||
| */ let activeAffectsMarks = 0; | ||
| /** | ||
| * Counter mutation seam for the mark engine in affects.ts: an imported `let` | ||
| * binding is read-only, and the read-path gate above must stay a plain module | ||
| * variable so `read()` pays one integer compare, not a function call. | ||
| * | ||
| * @internal | ||
| */ function shiftAffectsMarks(e) { | ||
| activeAffectsMarks += e; | ||
| } | ||
| function reassignPendingTransition(e) { | ||
| for (let t = 0; t < e.length; t++) { | ||
| e[t].ve = activeTransition; | ||
| } | ||
| } | ||
| const globalQueue = new GlobalQueue; | ||
| function flush(e) { | ||
| if (e) { | ||
| syncDepth++; | ||
| try { | ||
| return e(); | ||
| } finally { | ||
| // Decrement even if the drain throws (a throwing effect): a leaked | ||
| // syncDepth would stop `schedule()` from ever queuing a microtask again. | ||
| try { | ||
| flush(); | ||
| } finally { | ||
| syncDepth--; | ||
| } | ||
| } | ||
| } | ||
| if (globalQueue.Bt) { | ||
| return; | ||
| } | ||
| if (halted) return; | ||
| // `flush()` is an explicit drain point, so it must also process an active | ||
| // transition even if no microtask was scheduled for it yet. | ||
| while (scheduled || activeTransition) { | ||
| globalQueue.flush(); | ||
| } | ||
| } | ||
| function runQueue(e, t) { | ||
| for (let i = 0; i < e.length; i++) e[i](t); | ||
| } | ||
| function reporterBlocksSource(e, t) { | ||
| if (e.u & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false; | ||
| if (e.m === t || e.M?.has(t)) return true; | ||
| for (let i = e.S; i; i = i.st) { | ||
| let e = i.ot; | ||
| while (e) { | ||
| if (e === t || e.rt === t) return true; | ||
| e = e.en; | ||
| } | ||
| } | ||
| return !!(e.i & STATUS_PENDING && e.k instanceof NotReadyError && e.k.source === t); | ||
| } | ||
| function transitionComplete(e) { | ||
| if (e.Ht) return true; | ||
| if (e.Te.length) return false; | ||
| let t = true; | ||
| for (const [i, n] of e.qt) { | ||
| let s = false; | ||
| for (const e of n) { | ||
| if (reporterBlocksSource(e, i)) { | ||
| s = true; | ||
| break; | ||
| } | ||
| n.delete(e); | ||
| } | ||
| if (!s) e.qt.delete(i); else if (i.i & STATUS_PENDING && i.k?.source === i) { | ||
| t = false; | ||
| break; | ||
| } | ||
| } | ||
| // Override blockage lives with the engine. Absent hook = "no optimistic | ||
| // blockage", which is exact: only _optimisticWrite (engine) pushes to | ||
| // _optimisticNodes, so without the engine the loop was vacuous anyway. | ||
| if (t && e.Be.length && GlobalQueue.Wt(e)) t = false; | ||
| t && (e.Ht = true); | ||
| return t; | ||
| } | ||
| function currentTransition(e) { | ||
| while (e.Ht && typeof e.Ht === "object") e = e.Ht; | ||
| return e; | ||
| } | ||
| function setActiveTransition(e) { | ||
| activeTransition = e; | ||
| } | ||
| function runInTransition(e, t) { | ||
| const i = activeTransition; | ||
| try { | ||
| activeTransition = currentTransition(e); | ||
| return t(); | ||
| } finally { | ||
| activeTransition = i; | ||
| } | ||
| } | ||
| export { GlobalQueue, Queue, activeAffectsMarks, activeLanes, activeTransition, armReaskClear, assignOrMergeLane, clock, currentTransition, dirtyQueue, enforceLoadingBoundary, finalizePureQueue, findLane, flush, globalQueue, haltReactivity, insertSubs, projectionWriteActive, queuePendingNode, registerTransientStoreNode, resetErrorHalt, runInTransition, schedule, setActiveTransition, setProjectionWriteActive, shiftAffectsMarks, zombieQueue }; |
| import { STATUS_UNINITIALIZED, REACTIVE_DISPOSED, NOT_PENDING, REACTIVE_MANUAL_WRITE, STATUS_PENDING, REACTIVE_DIRTY, REACTIVE_CHECK } from "./constants.js"; | ||
| import { context, setPendingCheckActive, read, optimisticSignal, setSignal, setLatestReadActive, pendingCheckActive, latestReadActive, stale, currentOptimisticLane, prepareComputed, tracking, setContextInternal, optimisticComputed } from "./core.js"; | ||
| import { NotReadyError } from "./error.js"; | ||
| import { link } from "./graph.js"; | ||
| import { insertIntoHeap, queueFor } from "./heap.js"; | ||
| import "./invariants.js"; | ||
| import { hasActiveOverride, findLane } from "./lanes.js"; | ||
| import { installOptimisticEngine } from "./optimistic.js"; | ||
| import { GlobalQueue, clock, insertSubs, schedule } from "./scheduler.js"; | ||
| /** | ||
| * The isPending()/latest() verdict layer, moved out of core.ts. Importing this | ||
| * module installs the companion-maintenance hooks on GlobalQueue; apps that | ||
| * never import isPending/latest never pay for any of it. | ||
| */ | ||
| // Companions (pending signals / latest shadows) are optimistic nodes: their | ||
| // writes go through the optimistic write path and their reversion rides the | ||
| // same lanes, so the verdict layer brings the engine with it. | ||
| installOptimisticEngine(); | ||
| let pendingProbe = null; | ||
| /** | ||
| * Get or create the pending signal for a node (lazy). | ||
| * Used by isPending() to track pending state reactively. | ||
| */ function getPendingSignal(e) { | ||
| if (!e.ye) { | ||
| // Start false, write true if pending - ensures reversion returns to false | ||
| e.ye = optimisticSignal(false, { | ||
| ownedWrite: true | ||
| }); | ||
| e.ye.en = e; | ||
| if (computePendingState(e)) setSignal(e.ye, true); | ||
| } | ||
| return e.ye; | ||
| } | ||
| function collectPendingSources(e) { | ||
| if (!pendingProbe) return; | ||
| pendingProbe.sources.add(e); | ||
| const t = e.rt || e; | ||
| if (t !== e) pendingProbe.sources.add(t); | ||
| } | ||
| /** | ||
| * Adds a node to the active isPending() probe without reading it. The store's | ||
| * untracked-probe fallback (`witnessAffectsMark`) reaches this through | ||
| * `GlobalQueue._witnessAffects` — its callers guard on `pendingCheckActive`, | ||
| * which only flips inside `isPending()`, so the hook is always installed by | ||
| * the time it can fire. | ||
| */ function witnessAffects(e) { | ||
| pendingProbe?.sources.add(e); | ||
| } | ||
| function quietPending(e) { | ||
| if (e.M) { | ||
| for (const t of e.M) if (!t.A) return false; | ||
| return true; | ||
| } | ||
| if (e.m) return e.m.A; | ||
| return e.A; | ||
| } | ||
| function newQuestionInFlight(e) { | ||
| return !!(e.i & STATUS_PENDING) && !(e.i & STATUS_UNINITIALIZED) && !quietPending(e); | ||
| } | ||
| function computePendingState(e) { | ||
| const t = e; | ||
| if (t.u & REACTIVE_DISPOSED) return false; | ||
| if (e.P) return true; | ||
| const n = e.rt; | ||
| if (e.en) { | ||
| const t = e.en; | ||
| if (t.P) return true; | ||
| const n = t.rt || t; | ||
| return newQuestionInFlight(n); | ||
| } | ||
| if (n && e.ge !== NOT_PENDING && !hasActiveOverride(e)) { | ||
| return !!(n.u & REACTIVE_MANUAL_WRITE) || !n.Ae && !(n.i & STATUS_PENDING) || !!(n.i & STATUS_PENDING) && quietPending(n); | ||
| } | ||
| if (e.ge !== NOT_PENDING && !(t.i & STATUS_UNINITIALIZED)) { | ||
| if (hasActiveOverride(e)) return !e.Ge || !e.Ge(e.ge, e.be); | ||
| return true; | ||
| } | ||
| return newQuestionInFlight(t); | ||
| } | ||
| function syncCompanions(e, t) { | ||
| if (e.ye) updatePendingSignal(e); | ||
| if (e.pe) setSignal(e.pe, t); | ||
| } | ||
| function updatePendingSignal(e) { | ||
| if (e.ye) { | ||
| setSignal(e.ye, computePendingState(e)); | ||
| } | ||
| if (e.pe) updatePendingSignal(e.pe); | ||
| } | ||
| function updateChildCompanions(e) { | ||
| for (let t = e.G; t !== null; t = t.Ne) { | ||
| if (t.ye || t.pe) updatePendingSignal(t); | ||
| } | ||
| } | ||
| function repollDownstreamVerdicts(e) { | ||
| const t = new Set; | ||
| const visit = e => { | ||
| if (t.has(e)) return; | ||
| t.add(e); | ||
| if (e.ye || e.pe) updatePendingSignal(e); | ||
| for (let t = e.p; t !== null; t = t.de) visit(t.Ee); | ||
| for (let t = e.G ?? null; t !== null; t = t.Ne) { | ||
| visit(t); | ||
| } | ||
| }; | ||
| visit(e); | ||
| } | ||
| function snapCompanionsToState(e) { | ||
| const t = e.ye; | ||
| if (t && (t.be === undefined || t.be === NOT_PENDING)) { | ||
| const n = computePendingState(e); | ||
| if (t.Ue !== n || t.ge !== NOT_PENDING) { | ||
| t.Ue = n; | ||
| t.ge = NOT_PENDING; | ||
| t.Ie = clock; | ||
| insertSubs(t); | ||
| schedule(); | ||
| } | ||
| } | ||
| const n = e.pe; | ||
| if (n && !(n.u & REACTIVE_DISPOSED)) { | ||
| if ((n.be === undefined || n.be === NOT_PENDING) && n.ge === NOT_PENDING && !Object.is(n.Ue, e.Ue) && !(n.u & (REACTIVE_DIRTY | REACTIVE_CHECK))) { | ||
| n.u |= REACTIVE_DIRTY; | ||
| insertIntoHeap(n, queueFor(n)); | ||
| insertSubs(n); | ||
| schedule(); | ||
| } | ||
| snapCompanionsToState(n); | ||
| } | ||
| } | ||
| function getLatestValueComputed(e) { | ||
| if (!e.pe) { | ||
| const t = latestReadActive; | ||
| setLatestReadActive(false); | ||
| const n = pendingCheckActive; | ||
| setPendingCheckActive(false); | ||
| const i = context; | ||
| setContextInternal(null); | ||
| // Detach from owner so it isn't disposed with effects | ||
| e.pe = optimisticComputed(() => read(e)); | ||
| e.pe.en = e; | ||
| // Parent-child lane relationship | ||
| setContextInternal(i); | ||
| setPendingCheckActive(n); | ||
| setLatestReadActive(t); | ||
| } | ||
| return e.pe; | ||
| } | ||
| /** The latest()-mode read path, installed as GlobalQueue._latestRead. */ function latestRead(e) { | ||
| const t = getLatestValueComputed(e); | ||
| const n = latestReadActive; | ||
| setLatestReadActive(false); | ||
| const i = e.be !== undefined && e.be !== NOT_PENDING ? e.be : e.Ue; | ||
| let r; | ||
| try { | ||
| r = read(t); | ||
| } catch (t) { | ||
| if (t instanceof NotReadyError && (!context || !(e.i & STATUS_UNINITIALIZED))) return i; | ||
| throw t; | ||
| } finally { | ||
| setLatestReadActive(n); | ||
| } | ||
| if (t.i & STATUS_PENDING) return i; | ||
| if (stale && currentOptimisticLane && t.Je) { | ||
| const e = findLane(t.Je); | ||
| const n = findLane(currentOptimisticLane); | ||
| if (e !== n && e.Pe.size > 0) { | ||
| return i; | ||
| } | ||
| } | ||
| return r; | ||
| } | ||
| /** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */ function pendingCheckRead(e, t, n, i) { | ||
| setPendingCheckActive(false); | ||
| if (typeof e.xe === "function") prepareComputed(e, true); | ||
| const r = n.i; | ||
| if (t && r & STATUS_PENDING && r & STATUS_UNINITIALIZED) { | ||
| if (tracking && e !== t) link(e, t); | ||
| setPendingCheckActive(true); | ||
| throw n.k; | ||
| } | ||
| collectPendingSources(e); | ||
| if (i) collectPendingSources(i); | ||
| setPendingCheckActive(true); | ||
| } | ||
| function recordFreshRead(e, t) { | ||
| if (pendingProbe !== null && e.ge !== NOT_PENDING && t === e.ge) pendingProbe.freshReads.add(e); | ||
| } | ||
| function applyReask(e, t) { | ||
| const n = !!(e.i & STATUS_PENDING); | ||
| const i = t && !(n && !e.A); | ||
| const r = n && e.A !== i; | ||
| e.A = i; | ||
| return r; | ||
| } | ||
| function latest(e) { | ||
| const t = latestReadActive; | ||
| setLatestReadActive(true); | ||
| try { | ||
| return e(); | ||
| } finally { | ||
| setLatestReadActive(t); | ||
| } | ||
| } | ||
| function isPending(e) { | ||
| const t = pendingCheckActive; | ||
| const n = pendingProbe; | ||
| setPendingCheckActive(true); | ||
| const i = pendingProbe = { | ||
| found: false, | ||
| sources: new Set, | ||
| freshReads: new Set | ||
| }; | ||
| const collectPending = () => { | ||
| setPendingCheckActive(false); | ||
| try { | ||
| i.sources.forEach(e => { | ||
| if (read(getPendingSignal(e)) && !i.freshReads.has(e)) i.found = true; | ||
| }); | ||
| } finally { | ||
| setPendingCheckActive(true); | ||
| } | ||
| }; | ||
| try { | ||
| e(); | ||
| collectPending(); | ||
| return i.found; | ||
| } catch (e) { | ||
| collectPending(); | ||
| if (e instanceof NotReadyError) { | ||
| const t = !!(e.source?.i & STATUS_UNINITIALIZED); | ||
| if (i.found && !t) return true; | ||
| if (context && t) throw e; | ||
| } | ||
| return i.found; | ||
| } finally { | ||
| setPendingCheckActive(t); | ||
| pendingProbe = n; | ||
| } | ||
| } | ||
| // Hook installation (same late-binding pattern as GlobalQueue._update / | ||
| // _propagateAffects): core call sites fire these behind the same guards the | ||
| // direct calls used, so behavior is identical once this module loads. | ||
| GlobalQueue._e = syncCompanions; | ||
| GlobalQueue._ = updatePendingSignal; | ||
| GlobalQueue.me = updateChildCompanions; | ||
| GlobalQueue.R = snapCompanionsToState; | ||
| GlobalQueue.Gt = latestRead; | ||
| GlobalQueue.Pt = pendingCheckRead; | ||
| GlobalQueue.vt = recordFreshRead; | ||
| GlobalQueue.tt = applyReask; | ||
| GlobalQueue.nt = repollDownstreamVerdicts; | ||
| GlobalQueue.Zt = witnessAffects; | ||
| export { isPending, latest }; |
| export { ContextNotFoundError, NoOwnerError, NotReadyError } from "./core/error.js"; | ||
| export { clearSnapshots, isEqual, markSnapshotScope, refresh, releaseSnapshotScope, runWithOwner, setSnapshotCapture, untrack } from "./core/core.js"; | ||
| export { enableExternalSource } from "./core/external.js"; | ||
| export { createOwner, createRoot, getNextChildId, getObserver, getOwner, isDisposed, peekNextChildId } from "./core/owner.js"; | ||
| export { createContext, getContext, setContext } from "./core/context.js"; | ||
| export { $REFRESH, SUPPORTS_PROXY } from "./core/constants.js"; | ||
| import "./core/invariants.js"; | ||
| export { enforceLoadingBoundary, flush, resetErrorHalt } from "./core/scheduler.js"; | ||
| export { isPending, latest } from "./core/verdict.js"; | ||
| import "./core/effect.js"; | ||
| export { action } from "./core/action.js"; | ||
| export { createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onSettled, resolve } from "./signals.js"; | ||
| export { affects } from "./affects.js"; | ||
| export { mapArray, repeat } from "./map.js"; | ||
| export { $PROXY, $TARGET, $TRACK, createStore, isWrappable } from "./store/store.js"; | ||
| export { createProjection } from "./store/projection.js"; | ||
| export { createOptimisticStore } from "./store/optimistic.js"; | ||
| export { reconcile } from "./store/reconcile.js"; | ||
| export { storePath } from "./store/storePath.js"; | ||
| export { deep, merge, omit, snapshot } from "./store/utils.js"; | ||
| export { createErrorBoundary, createLoadingBoundary, createRevealOrder, flatten } from "./boundaries.js"; | ||
| const DEV = undefined; | ||
| export { DEV }; |
+264
| import { computed, runWithOwner, signal, setSignal } from "./core/core.js"; | ||
| import { createOwner } from "./core/owner.js"; | ||
| import "./core/scheduler.js"; | ||
| import { CONFIG_AUTO_DISPOSE } from "./core/constants.js"; | ||
| import "./core/invariants.js"; | ||
| import "./core/verdict.js"; | ||
| import "./core/effect.js"; | ||
| import { accessor } from "./signals.js"; | ||
| import { $TRACK } from "./store/store.js"; | ||
| function mapArray(t, s, i) { | ||
| const e = typeof i?.keyed === "function" ? i.keyed : undefined; | ||
| const h = s.length > 1; | ||
| const r = s; | ||
| const n = { | ||
| $t: createOwner(), | ||
| Xt: 0, | ||
| ts: t, | ||
| ss: [], | ||
| es: r, | ||
| hs: [], | ||
| rs: [], | ||
| ns: e, | ||
| cs: e || i?.keyed === false ? [] : undefined, | ||
| fs: h && i?.keyed !== false ? [] : undefined, | ||
| us: i?.keyed === false, | ||
| ps: i?.fallback | ||
| }; | ||
| const o = computed(updateKeyedMap.bind(n)); | ||
| // Untracked reads inside the internal owner resolve via _parentComputed; routing | ||
| // them through node lets store-proxy lookups see pending writes (not stale _value). | ||
| n.$t.Ct = o; | ||
| o.U &= ~CONFIG_AUTO_DISPOSE; | ||
| return accessor(o); | ||
| } | ||
| const pureOptions = { | ||
| ownedWrite: true | ||
| }; | ||
| function updateKeyedMap() { | ||
| const t = this.ts() || [], s = t.length; | ||
| t[$TRACK]; | ||
| // top level tracking | ||
| runWithOwner(this.$t, () => { | ||
| let i, e, h = this.cs ? this.us ? () => { | ||
| this.cs[e] = signal(t[e], pureOptions); | ||
| return this.es(accessor(this.cs[e]), e); | ||
| } : () => { | ||
| this.cs[e] = signal(t[e], pureOptions); | ||
| this.fs && (this.fs[e] = signal(e, pureOptions)); | ||
| return this.es(accessor(this.cs[e]), this.fs ? accessor(this.fs[e]) : undefined); | ||
| } : this.fs ? () => { | ||
| const s = t[e]; | ||
| this.fs[e] = signal(e, pureOptions); | ||
| return this.es(s, accessor(this.fs[e])); | ||
| } : () => { | ||
| const s = t[e]; | ||
| return this.es(s); | ||
| }; | ||
| // fast path for empty arrays | ||
| if (s === 0) { | ||
| if (this.Xt !== 0) { | ||
| this.$t.dispose(false); | ||
| this.rs = []; | ||
| this.ss = []; | ||
| this.hs = []; | ||
| this.Xt = 0; | ||
| this.cs && (this.cs = []); | ||
| this.fs && (this.fs = []); | ||
| } | ||
| if (this.ps && !this.hs[0]) { | ||
| // create fallback | ||
| this.hs[0] = runWithOwner(this.rs[0] = createOwner(), this.ps); | ||
| } | ||
| } | ||
| // fast path for new create | ||
| else if (this.Xt === 0) { | ||
| // dispose previous fallback | ||
| if (this.rs[0]) this.rs[0].dispose(); | ||
| this.hs = new Array(s); | ||
| for (e = 0; e < s; e++) { | ||
| this.ss[e] = t[e]; | ||
| this.hs[e] = runWithOwner(this.rs[e] = createOwner(), h); | ||
| } | ||
| this.Xt = s; | ||
| } else { | ||
| let r, n, o, a, c, f, u, p = new Array(s), l = new Array(s), w = this.cs ? new Array(s) : undefined, O = this.fs ? new Array(s) : undefined; | ||
| // skip common prefix | ||
| for (r = 0, n = Math.min(this.Xt, s); r < n && (this.ss[r] === t[r] || this.cs && compare(this.ns, this.ss[r], t[r])); r++) { | ||
| if (this.cs) setSignal(this.cs[r], t[r]); | ||
| } | ||
| // common suffix | ||
| for (n = this.Xt - 1, o = s - 1; n >= r && o >= r && (this.ss[n] === t[o] || this.cs && compare(this.ns, this.ss[n], t[o])); n--, | ||
| o--) { | ||
| p[o] = this.hs[n]; | ||
| l[o] = this.rs[n]; | ||
| w && (w[o] = this.cs[n]); | ||
| O && (O[o] = this.fs[n]); | ||
| } | ||
| // 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order | ||
| f = new Map; | ||
| u = new Array(o + 1); | ||
| for (e = o; e >= r; e--) { | ||
| a = t[e]; | ||
| c = this.ns ? this.ns(a) : a; | ||
| i = f.get(c); | ||
| u[e] = i === undefined ? -1 : i; | ||
| f.set(c, e); | ||
| } | ||
| // 1) step through all old items and see if they can be found in the new set; if so, save them in a temp array and mark them moved; if not, exit them | ||
| for (i = r; i <= n; i++) { | ||
| a = this.ss[i]; | ||
| c = this.ns ? this.ns(a) : a; | ||
| e = f.get(c); | ||
| if (e !== undefined && e !== -1) { | ||
| p[e] = this.hs[i]; | ||
| l[e] = this.rs[i]; | ||
| w && (w[e] = this.cs[i]); | ||
| O && (O[e] = this.fs[i]); | ||
| e = u[e]; | ||
| f.set(c, e); | ||
| } else this.rs[i].dispose(); | ||
| } | ||
| // 2) set all the new values, pulling from the temp array if copied, otherwise entering the new value | ||
| for (e = r; e < s; e++) { | ||
| if (e in p) { | ||
| this.hs[e] = p[e]; | ||
| this.rs[e] = l[e]; | ||
| if (w) { | ||
| this.cs[e] = w[e]; | ||
| setSignal(this.cs[e], t[e]); | ||
| } | ||
| if (O) { | ||
| this.fs[e] = O[e]; | ||
| setSignal(this.fs[e], e); | ||
| } | ||
| } else { | ||
| this.hs[e] = runWithOwner(this.rs[e] = createOwner(), h); | ||
| } | ||
| } | ||
| // 3) in case the new set is shorter than the old, set the length of the mapped array | ||
| this.hs = this.hs.slice(0, this.Xt = s); | ||
| // 4) save a copy of the mapped items for the next update | ||
| this.ss = t.slice(0); | ||
| } | ||
| }); | ||
| return this.hs; | ||
| } | ||
| /** | ||
| * Reactively renders a callback `count` times, reusing previously-rendered | ||
| * entries when only the count changes. Underlying helper for `<Repeat>`. | ||
| * | ||
| * - `options.from` — start index (default `0`); useful for offset/windowed | ||
| * rendering. | ||
| * - `options.fallback` — accessor returning a value to show when count is `0`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const view = repeat(count, i => `Item ${i}`, { fallback: () => "empty" }); | ||
| * ``` | ||
| * | ||
| * @description https://docs.solidjs.com/reference/reactive-utilities/repeat | ||
| */ function repeat(t, s, i) { | ||
| const e = s; | ||
| const h = computed(updateRepeat.bind({ | ||
| $t: createOwner(), | ||
| Xt: 0, | ||
| ls: 0, | ||
| ws: t, | ||
| es: e, | ||
| rs: [], | ||
| hs: [], | ||
| Os: i?.from, | ||
| ps: i?.fallback | ||
| })); | ||
| h.U &= ~CONFIG_AUTO_DISPOSE; | ||
| return accessor(h); | ||
| } | ||
| function updateRepeat() { | ||
| const t = this.ws(); | ||
| const s = this.Os?.() || 0; | ||
| runWithOwner(this.$t, () => { | ||
| if (t === 0) { | ||
| if (this.Xt !== 0) { | ||
| this.$t.dispose(false); | ||
| this.rs = []; | ||
| this.hs = []; | ||
| this.Xt = 0; | ||
| // Reset offset to match the cleared data. Without this, a subsequent | ||
| // nonzero render with a smaller `from` would enter the end-clear loop | ||
| // with `prevTo = stale_offset + 0` > `to` and dispose `_nodes[-1]` | ||
| // (#2767, repro 2). | ||
| this.ls = 0; | ||
| } | ||
| if (this.ps && !this.hs[0]) { | ||
| // create fallback | ||
| this.hs[0] = runWithOwner(this.rs[0] = createOwner(), this.ps); | ||
| } | ||
| return; | ||
| } | ||
| const i = s + t; | ||
| const e = this.ls + this.Xt; | ||
| // remove fallback | ||
| if (this.Xt === 0 && this.rs[0]) this.rs[0].dispose(); | ||
| // Disjoint windows have no rows to retain; replace them wholesale. | ||
| if (s >= e || i <= this.ls) { | ||
| for (let t = 0; t < this.Xt; t++) this.rs[t].dispose(); | ||
| this.rs = new Array(t); | ||
| this.hs = new Array(t); | ||
| for (let i = 0; i < t; i++) this.hs[i] = runWithOwner(this.rs[i] = createOwner(), () => this.es(s + i)); | ||
| this.ls = s; | ||
| this.Xt = t; | ||
| return; | ||
| } | ||
| // clear the end | ||
| for (let t = i; t < e; t++) this.rs[t - this.ls].dispose(); | ||
| if (this.ls < s) { | ||
| // clear beginning — `_nodes` is local-indexed, so the rows leaving the | ||
| // front sit at local positions 0..(from - _offset), clamped to old len. | ||
| const t = s - this.ls; | ||
| for (let s = 0; s < t && s < this.Xt; s++) this.rs[s].dispose(); | ||
| // shift indexes | ||
| this.rs.splice(0, t); | ||
| this.hs.splice(0, t); | ||
| } else if (this.ls > s) { | ||
| // shift indexes | ||
| let i = e - this.ls - 1; | ||
| let h = this.ls - s; | ||
| this.rs.length = this.hs.length = t; | ||
| while (i >= h) { | ||
| this.rs[i] = this.rs[i - h]; | ||
| this.hs[i] = this.hs[i - h]; | ||
| i--; | ||
| } | ||
| for (let t = 0; t < h; t++) { | ||
| this.hs[t] = runWithOwner(this.rs[t] = createOwner(), () => this.es(t + s)); | ||
| } | ||
| } | ||
| for (let t = e; t < i; t++) { | ||
| this.hs[t - s] = runWithOwner(this.rs[t - s] = createOwner(), () => this.es(t)); | ||
| } | ||
| this.hs = this.hs.slice(0, t); | ||
| this.ls = s; | ||
| this.Xt = t; | ||
| }); | ||
| return this.hs; | ||
| } | ||
| function compare(t, s, i) { | ||
| return t ? t(s) === t(i) : true; | ||
| } | ||
| export { mapArray, repeat }; |
| import { computed, optimisticComputed, setSignal, optimisticSignal, runWithOwner, setMemo, signal, read, untrack } from "./core/core.js"; | ||
| import { cleanup, createRoot, getOwner, dispose } from "./core/owner.js"; | ||
| import { globalQueue } from "./core/scheduler.js"; | ||
| import { CONFIG_CHILDREN_FORBIDDEN, EFFECT_USER, $REFRESH, CONFIG_AUTO_DISPOSE } from "./core/constants.js"; | ||
| import "./core/invariants.js"; | ||
| import "./core/verdict.js"; | ||
| import { effect, trackedEffect } from "./core/effect.js"; | ||
| import { installOptimisticEngine } from "./core/optimistic.js"; | ||
| /** | ||
| * Low-level reactive-cleanup primitive. Registers a callback that runs when | ||
| * the surrounding owner is disposed. | ||
| * | ||
| * **In 2.0 user code this is rare.** The two cases where you might reach for | ||
| * it have better-shaped tools: | ||
| * | ||
| * - **Component lifecycle (mount/unmount, listeners, intervals):** use | ||
| * {@link onSettled} and **return** a cleanup function. Setup and teardown | ||
| * stay paired in one block. This replaces the 1.x `onMount` + `onCleanup` | ||
| * pairing. | ||
| * - **Cleanup tied to an effect run:** `onCleanup` does not belong in | ||
| * `createEffect`'s apply phase. If a compute phase genuinely needs per-run | ||
| * teardown, that's usually a sign the work should be a memo/projection | ||
| * instead, or moved to `onSettled` if it's lifecycle-shaped. | ||
| * | ||
| * Where `onCleanup` is the right tool is **library / custom-primitive | ||
| * internals** — coordinating disposal inside a `createRoot` body, or wiring | ||
| * cleanup to a captured owner via `runWithOwner` from a custom factory. | ||
| * Application code rarely needs to write any of those shapes directly. | ||
| * | ||
| * Must be called inside an owner. Calling outside an owner is a no-op (with a | ||
| * dev-mode warning). | ||
| * | ||
| * Cannot be used inside `createTrackedEffect` or `onSettled` — return a | ||
| * cleanup function from the callback body instead. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Library shape: thread a resource's disposal into a *captured* owner | ||
| * // from a factory that has no settle-phase setup of its own. `onSettled` | ||
| * // would queue a callback we don't need; `onCleanup` is the leaner | ||
| * // primitive when the only job is "register disposal on this owner". | ||
| * function bindToOwner<T extends { dispose(): void }>(owner: Owner, resource: T): T { | ||
| * runWithOwner(owner, () => onCleanup(() => resource.dispose())); | ||
| * return resource; | ||
| * } | ||
| * ``` | ||
| */ function onCleanup(e) { | ||
| return cleanup(e); | ||
| } | ||
| function accessor(e) { | ||
| const t = read.bind(null, e); | ||
| t[$REFRESH] = e; | ||
| return t; | ||
| } | ||
| function createSignal(e, t) { | ||
| if (typeof e === "function") { | ||
| const n = computed(e, t); | ||
| n.U &= ~CONFIG_AUTO_DISPOSE; | ||
| return [ accessor(n), setMemo.bind(null, n) ]; | ||
| } | ||
| const n = signal(e, t); | ||
| return [ accessor(n), setSignal.bind(null, n) ]; | ||
| } | ||
| /** | ||
| * Creates a readonly derived reactive memoized signal. | ||
| * | ||
| * ```typescript | ||
| * const value = createMemo<T>(compute, options?: MemoOptions<T>); | ||
| * ``` | ||
| * @param compute a function that receives its previous value and returns a new value used to react on a computation | ||
| * @param options `MemoOptions` -- id, name, equals, unobserved, lazy | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const [first, setFirst] = createSignal("Ada"); | ||
| * const [last, setLast] = createSignal("Lovelace"); | ||
| * | ||
| * const fullName = createMemo(() => `${first()} ${last()}`); | ||
| * | ||
| * fullName(); // "Ada Lovelace" | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Async memo — reads surface as pending inside <Loading> | ||
| * const user = createMemo(async () => { | ||
| * const res = await fetch(`/users/${id()}`); | ||
| * return res.json(); | ||
| * }); | ||
| * ``` | ||
| * | ||
| * @description https://docs.solidjs.com/reference/basic-reactivity/create-memo | ||
| */ | ||
| // NoInfer keeps the previous-value parameter from influencing T inference, so | ||
| // the memo/effect result type is still driven by the compute return type. | ||
| function createMemo(e, t) { | ||
| return accessor(computed(e, t)); | ||
| } | ||
| function createEffect(e, t, n) { | ||
| effect(e, t.effect || t, t.error, { | ||
| user: true, | ||
| ...n | ||
| }); | ||
| } | ||
| /** | ||
| * Creates a reactive computation that runs during the render phase as DOM elements | ||
| * are created and updated but not necessarily connected. | ||
| * | ||
| * Same compute / effect split as `createEffect`, but scheduled inside the render | ||
| * queue rather than after it. Reach for this only when authoring renderer | ||
| * plumbing (custom DOM bindings, JSX-generated `insert()` / `spread()` calls). | ||
| * App code should use `createEffect`. | ||
| * | ||
| * ```typescript | ||
| * createRenderEffect<T>(compute, effectFn, options?: EffectOptions); | ||
| * ``` | ||
| * @param compute a function that receives its previous value and returns a new value used to react on a computation | ||
| * @param effectFn a function that receives the new value and is used to perform side effects | ||
| * @param options `EffectOptions` -- name, defer, schedule | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Custom directive: bind an element's textContent to a reactive source. | ||
| * function bindText(el: HTMLElement, source: () => string) { | ||
| * createRenderEffect( | ||
| * () => source(), | ||
| * value => { el.textContent = value; } | ||
| * ); | ||
| * } | ||
| * ``` | ||
| * | ||
| * @description https://docs.solidjs.com/reference/secondary-primitives/create-render-effect | ||
| */ function createRenderEffect(e, t, n) { | ||
| effect(e, t, undefined, n); | ||
| } | ||
| /** | ||
| * Creates a tracked reactive effect where dependency tracking and side effects happen | ||
| * in the same scope. | ||
| * | ||
| * WARNING: Because tracking and effects happen in the same scope, this primitive | ||
| * may run multiple times for a single change or show tearing (reading inconsistent | ||
| * state). Use only when dynamic subscription patterns require same-scope tracking. | ||
| * | ||
| * ```typescript | ||
| * createTrackedEffect(compute, options?: { name?: string }); | ||
| * ``` | ||
| * @param compute a function that contains reactive reads to track and returns an optional cleanup function to run on disposal or before next execution | ||
| * @param options -- name | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * createTrackedEffect(() => { | ||
| * const target = focusedNode(); | ||
| * if (!target) return; | ||
| * | ||
| * const handler = () => log(target.value()); | ||
| * target.on("change", handler); | ||
| * | ||
| * return () => target.off("change", handler); | ||
| * }); | ||
| * ``` | ||
| * | ||
| * @description https://docs.solidjs.com/reference/secondary-primitives/create-tracked-effect | ||
| */ function createTrackedEffect(e, t) { | ||
| trackedEffect(e, t); | ||
| } | ||
| /** | ||
| * Creates a reactive computation that runs after the render phase with flexible tracking. | ||
| * | ||
| * ```typescript | ||
| * const track = createReaction(effectFn, options?: EffectOptions); | ||
| * track(() => { // reactive reads }); | ||
| * ``` | ||
| * @param effectFn a function (or `EffectBundle`) that is called when tracked function is invalidated | ||
| * @param options `EffectOptions` -- name, defer | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const [count, setCount] = createSignal(0); | ||
| * | ||
| * const track = createReaction(() => { | ||
| * console.log("count changed once, re-arm to listen again"); | ||
| * track(() => count()); // re-arm | ||
| * }); | ||
| * | ||
| * track(() => count()); // initial arm | ||
| * | ||
| * setCount(1); // logs once, reaction re-armed for next change | ||
| * ``` | ||
| * | ||
| * @description https://docs.solidjs.com/reference/secondary-primitives/create-reaction | ||
| */ function createReaction(e, t) { | ||
| let n = undefined; | ||
| cleanup(() => n?.()); | ||
| const c = getOwner(); | ||
| // The currently armed effect node. `track()` replaces the previous | ||
| // subscription (1.x semantics): without disposing the superseded arm, its | ||
| // sources stayed live (firing the callback for replaced dependencies), each | ||
| // accumulated arm delivered its own fire, and un-fired arms leaked as live | ||
| // effect nodes until the owner disposed (#2861). | ||
| let r; | ||
| return o => { | ||
| if (r) { | ||
| dispose(r); | ||
| r = undefined; | ||
| } | ||
| runWithOwner(c, () => { | ||
| effect(() => (o(), r = getOwner()), t => { | ||
| r = undefined; | ||
| n?.(); | ||
| const c = (e.effect || e)?.(); | ||
| if (false && c !== undefined && typeof c !== "function") ; | ||
| n = c; | ||
| dispose(t); | ||
| }, e.error, { | ||
| ...false ? { | ||
| ...t, | ||
| name: t?.name ?? "effect" | ||
| } : t, | ||
| user: true, | ||
| defer: true | ||
| }); | ||
| }); | ||
| }; | ||
| } | ||
| /** | ||
| * Awaits a reactive expression and returns its first fully-settled value as a | ||
| * `Promise`. Pending async reads (`createMemo` returning a promise, etc.) are | ||
| * waited on; once the expression returns synchronously without `NotReadyError` | ||
| * the promise resolves with that value. If the expression settles with an | ||
| * error instead — including an async source that rejects — the promise | ||
| * rejects with it. | ||
| * | ||
| * Must be called *outside* a tracking scope — it doesn't subscribe, it just | ||
| * resolves the current value once. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const user = createMemo(() => fetch(`/users/${id()}`).then(r => r.json())); | ||
| * | ||
| * // outside any reactive scope | ||
| * const initial = await resolve(() => user()); | ||
| * ``` | ||
| * | ||
| * @param fn a reactive expression to resolve | ||
| */ function resolve(e) { | ||
| return new Promise((t, n) => { | ||
| createRoot(c => { | ||
| // A user effect rather than a bare computed: computeds are pull-based and | ||
| // are only re-enqueued when a pending source *resolves* — a rejection just | ||
| // marks them errored, so nothing would re-run and the promise would never | ||
| // settle (#2842). The effect's error channel is notified on rejection. | ||
| effect(e, e => { | ||
| t(e); | ||
| c(); | ||
| }, e => { | ||
| // The error arm already unwraps StatusError (#2840) — `err` is the | ||
| // user's original error, matching what error boundaries expose. | ||
| n(e); | ||
| c(); | ||
| }, { | ||
| user: true | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| function createOptimistic(e, t) { | ||
| // Install before the node exists: only engine-installed programs can carry | ||
| // an _overrideValue slot (same runtime-install pattern as | ||
| // GlobalQueue._clearOptimisticStore in createOptimisticStore). | ||
| installOptimisticEngine(); | ||
| if (typeof e === "function") { | ||
| const n = optimisticComputed(e, t); | ||
| n.U &= ~CONFIG_AUTO_DISPOSE; | ||
| return [ accessor(n), setSignal.bind(null, n) ]; | ||
| } | ||
| const n = optimisticSignal(e, t); | ||
| return [ accessor(n), setSignal.bind(null, n) ]; | ||
| } | ||
| /** | ||
| * Schedules `callback` to run **once** after the reactive graph has fully | ||
| * settled — i.e. once every pending async read inside the current owner has | ||
| * resolved and the queue has flushed. Each call registers a single fire; it | ||
| * does not create an ongoing subscription. | ||
| * | ||
| * The canonical lifecycle primitive in 2.0. Three main usages: | ||
| * | ||
| * - **Component-level setup-and-teardown** *(the most common shape)*: run | ||
| * setup after the component's first stable render and **return a cleanup | ||
| * function** to dispose it on owner disposal. This is the replacement for | ||
| * the 1.x `onMount` + `onCleanup` pairing — setup and teardown live in one | ||
| * block, and `onCleanup` is no longer the right tool for component | ||
| * bodies. (`onMount` no longer exists in 2.0.) | ||
| * - **Post-settle "ready" hook:** run once after a component's first stable | ||
| * render — analytics ping, focus, scroll-into-view, etc. No cleanup needed. | ||
| * - **Inside an event handler:** schedule work to run after the action / | ||
| * transition triggered by the event has completed. | ||
| * | ||
| * Reactive reads inside the callback are *not* tracked — to react to | ||
| * subsequent settles, register a new `onSettled` each time. | ||
| * | ||
| * `onCleanup` is **not** allowed inside the callback — return a cleanup | ||
| * function instead. The returned cleanup runs on owner disposal. | ||
| * | ||
| * A cleanup return is only honored when `onSettled` is called from an **owned** | ||
| * scope (e.g. a component body). When it fires out of band from an *unowned* | ||
| * scope — an event handler, a tracked effect, or another `onSettled` — there is | ||
| * no owner lifecycle to bind a cleanup to; returning one is a dev-mode error | ||
| * (and is dropped in production). Use the post-settle/event-handler forms below | ||
| * for one-shot work, and keep setup-with-teardown in an owned scope. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // Component-level setup + teardown — replaces onMount + onCleanup. | ||
| * // Subscribe to an external source on mount, unsubscribe on dispose. | ||
| * function useViewportWidth() { | ||
| * const [width, setWidth] = createSignal(window.innerWidth); | ||
| * onSettled(() => { | ||
| * const onResize = () => setWidth(window.innerWidth); | ||
| * window.addEventListener("resize", onResize); | ||
| * return () => window.removeEventListener("resize", onResize); | ||
| * }); | ||
| * return width; | ||
| * } | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // Post-settle "ready" hook — no cleanup needed. | ||
| * function Dashboard() { | ||
| * const data = createMemo(async () => fetchData()); | ||
| * | ||
| * onSettled(() => { | ||
| * analytics.track("dashboard.ready"); | ||
| * }); | ||
| * | ||
| * return <Loading fallback={<Spinner />}><pre>{data()}</pre></Loading>; | ||
| * } | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // Event-handler — runs after the action settles. | ||
| * function SaveButton() { | ||
| * const save = action(function* () { | ||
| * yield api.save(); | ||
| * }); | ||
| * | ||
| * const handleClick = () => { | ||
| * save(); | ||
| * onSettled(() => toast("Saved!")); | ||
| * }; | ||
| * | ||
| * return <button onClick={handleClick}>Save</button>; | ||
| * } | ||
| * ``` | ||
| * | ||
| * @param callback Function to run; may return a cleanup function that fires | ||
| * on owner disposal | ||
| */ function onSettled(e) { | ||
| const t = getOwner(); | ||
| t && !(t.U & CONFIG_CHILDREN_FORBIDDEN) ? createTrackedEffect(() => untrack(e), undefined) : globalQueue.enqueue(EFFECT_USER, () => { | ||
| // Unowned, out-of-band fire (no owner, or a children-forbidden one this | ||
| // one-shot must not bind to): a returned cleanup has no lifecycle to | ||
| // attach to. Reject it in dev; in production the return is simply | ||
| // dropped — never bound to an unrelated owner or run eagerly. | ||
| e(); | ||
| }); | ||
| } | ||
| export { accessor, createEffect, createMemo, createOptimistic, createReaction, createRenderEffect, createSignal, createTrackedEffect, onCleanup, onSettled, resolve }; |
| import { computed } from "../core/core.js"; | ||
| import { CONFIG_AUTO_DISPOSE, NOT_PENDING } from "../core/constants.js"; | ||
| import { GlobalQueue, schedule, setProjectionWriteActive, insertSubs, projectionWriteActive } from "../core/scheduler.js"; | ||
| import "../core/invariants.js"; | ||
| import "../core/verdict.js"; | ||
| import "../core/effect.js"; | ||
| import { installOptimisticEngine } from "../core/optimistic.js"; | ||
| import { runProjectionComputed } from "./projection.js"; | ||
| import { storeSetter, $TARGET, STORE_OPTIMISTIC_OVERRIDE, STORE_NODE, getOverlayLayer, STORE_VALUE, $DELETED, isWrappable, wrap, visibleNodeValue, $TRACK, notifySelf, STORE_WRAP, createStoreProxy, storeTraps, STORE_LOOKUP, STORE_OPTIMISTIC, STORE_FIREWALL } from "./store.js"; | ||
| function createOptimisticStore(t, e, r) { | ||
| // Register clear function with scheduler; store nodes marked | ||
| // STORE_OPTIMISTIC take the engine's write path, so install it before any | ||
| // node can be created. | ||
| installOptimisticEngine(); | ||
| GlobalQueue.Kt ||= clearOptimisticStores; | ||
| const i = typeof t === "function"; | ||
| const o = i ? e : t; | ||
| const c = i ? t : undefined; | ||
| // Create optimistic projection store | ||
| const {store: n} = createOptimisticProjectionInternal(c, o, r); | ||
| return [ n, t => storeSetter(n, t) ]; | ||
| } | ||
| // Clear the optimistic overrides of a settling batch of stores and notify | ||
| // signals. Owns the whole batch (iterate + clear + reschedule) so the | ||
| // scheduler's flush tail carries only a size-guarded hook call. | ||
| function clearOptimisticStores(t) { | ||
| for (const e of t) { | ||
| const t = e[$TARGET]; | ||
| if (t?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(t); | ||
| } | ||
| t.clear(); | ||
| schedule(); | ||
| } | ||
| function clearOptimisticOverride(t) { | ||
| const e = t[STORE_OPTIMISTIC_OVERRIDE]; | ||
| if (!e) return; | ||
| const r = t[STORE_NODE]; | ||
| delete t[STORE_OPTIMISTIC_OVERRIDE]; | ||
| // Notify signals for all overridden properties | ||
| // Use projectionWriteActive to bypass optimistic signal behavior (no lane creation) | ||
| // This ensures reversion effects go to regular queues, not lane queues | ||
| const i = projectionWriteActive; | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| if (r) { | ||
| for (const i of Reflect.ownKeys(e)) { | ||
| if (r[i]) { | ||
| const e = r[i]; | ||
| // Clear lane association so effects go to regular queue | ||
| e.Je = undefined; | ||
| // Re-read from base — the optimistic layer was deleted above, so the | ||
| // overlay resolves to STORE_OVERRIDE or STORE_VALUE. | ||
| const o = getOverlayLayer(t, i); | ||
| const c = o ? o[i] : t[STORE_VALUE][i]; | ||
| const n = c === $DELETED ? undefined : c; | ||
| const s = isWrappable(n) ? wrap(n, t) : n; | ||
| const O = visibleNodeValue(e); | ||
| e.be = NOT_PENDING; | ||
| e.ge = NOT_PENDING; | ||
| e.Ue = s; | ||
| if (!e.Ge || !e.Ge(O, s)) { | ||
| insertSubs(e, true); | ||
| schedule(); | ||
| } | ||
| } | ||
| } | ||
| // Notify $TRACK | ||
| if (r[$TRACK]) { | ||
| r[$TRACK].Je = undefined; | ||
| notifySelf(t); | ||
| } | ||
| } | ||
| } finally { | ||
| setProjectionWriteActive(i); | ||
| } | ||
| } | ||
| function createOptimisticProjectionInternal(t, e, r) { | ||
| let i; | ||
| const o = new WeakMap; | ||
| const wrapper = t => { | ||
| t[STORE_WRAP] = wrapProjection; | ||
| t[STORE_LOOKUP] = o; | ||
| t[STORE_OPTIMISTIC] = true; | ||
| // Mark as optimistic store | ||
| Object.defineProperty(t, STORE_FIREWALL, { | ||
| get() { | ||
| return i; | ||
| }, | ||
| configurable: true | ||
| }); | ||
| }; | ||
| const wrapProjection = t => { | ||
| if (o.has(t)) return o.get(t); | ||
| if (t[$TARGET]?.[STORE_WRAP] === wrapProjection) return t; | ||
| const e = createStoreProxy(t, storeTraps, wrapper); | ||
| o.set(t, e); | ||
| return e; | ||
| }; | ||
| const c = wrapProjection(e); | ||
| // If there's a projection function, create a computed to drive it | ||
| if (t) { | ||
| // All writes inside firewall recompute must go to STORE_OVERRIDE (base), not | ||
| // STORE_OPTIMISTIC_OVERRIDE. The outer wrap covers the sync body (including | ||
| // `fn(draft)` and the initial commit); `wrapCommit` re-applies the flag for | ||
| // async yields because they fire outside any enclosing try/finally. It also | ||
| // consumes stale optimistic overlays once fresh projected data lands. | ||
| const clearProjectionOverride = () => { | ||
| const t = c[$TARGET]; | ||
| if (t?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(t); | ||
| }; | ||
| const wrapCommit = t => { | ||
| const e = projectionWriteActive; | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| t(); | ||
| clearProjectionOverride(); | ||
| } finally { | ||
| setProjectionWriteActive(e); | ||
| } | ||
| }; | ||
| i = computed(() => { | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| runProjectionComputed(c, t, r?.key || "id", wrapCommit, clearProjectionOverride); | ||
| } finally { | ||
| setProjectionWriteActive(false); | ||
| } | ||
| }, undefined); | ||
| i.U &= ~CONFIG_AUTO_DISPOSE; | ||
| } | ||
| return { | ||
| store: c, | ||
| node: i | ||
| }; | ||
| } | ||
| export { createOptimisticStore }; |
| import { computed } from "../core/core.js"; | ||
| import { getOwner } from "../core/owner.js"; | ||
| import { setProjectionWriteActive } from "../core/scheduler.js"; | ||
| import { handleAsync } from "../core/async.js"; | ||
| import "../core/verdict.js"; | ||
| import "../core/effect.js"; | ||
| import { CONFIG_AUTO_DISPOSE } from "../core/constants.js"; | ||
| import { reconcile } from "./reconcile.js"; | ||
| import { storeSetter, $TARGET, STORE_WRAP, createStoreProxy, storeTraps, setWriteOverride, STORE_LOOKUP, STORE_FIREWALL } from "./store.js"; | ||
| function createProjectionInternal(e, r, t) { | ||
| let o; | ||
| const i = new WeakMap; | ||
| const wrapper = e => { | ||
| e[STORE_WRAP] = wrapProjection; | ||
| e[STORE_LOOKUP] = i; | ||
| Object.defineProperty(e, STORE_FIREWALL, { | ||
| get() { | ||
| return o; | ||
| }, | ||
| configurable: true | ||
| }); | ||
| }; | ||
| const wrapProjection = e => { | ||
| if (i.has(e)) return i.get(e); | ||
| if (e[$TARGET]?.[STORE_WRAP] === wrapProjection) return e; | ||
| const r = createStoreProxy(e, storeTraps, wrapper); | ||
| i.set(e, r); | ||
| return r; | ||
| }; | ||
| const n = wrapProjection(r); | ||
| o = computed(() => { | ||
| if (!o) o = getOwner(); | ||
| runProjectionComputed(n, e, t?.key || "id"); | ||
| }, undefined); | ||
| o.U &= ~CONFIG_AUTO_DISPOSE; | ||
| return { | ||
| store: n, | ||
| node: o | ||
| }; | ||
| } | ||
| /** | ||
| * Creates a derived (projected) store. Like `createMemo` but for stores: the | ||
| * derive function receives a mutable draft and either mutates it in place | ||
| * (canonical) or returns a new value. Either way the result is reconciled | ||
| * against the previous draft by `options.key` (default `"id"`), so surviving | ||
| * items keep their proxy identity — only added/removed items are | ||
| * created/disposed. | ||
| * | ||
| * Returns the projected store directly (no setter — reads only). | ||
| * | ||
| * Use this when you want the structural-sharing / per-property tracking | ||
| * behaviour of a store on top of a derived computation. For simple read-only | ||
| * derivations, `createMemo` is lighter. | ||
| * | ||
| * @param fn receives the current draft; mutate it in place or return new | ||
| * data. Return is convenient for filter/derive shapes where mutation is | ||
| * awkward. | ||
| * @param seed the backing store value to wrap and reconcile into | ||
| * @param options `ProjectionOptions` — `name`, `key`. `key` defaults to | ||
| * `"id"`; specify it only when your data uses a different identity field | ||
| * (e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Mutation form — update individual fields on the draft. | ||
| * const summary = createProjection<{ total: number; active: number }>( | ||
| * draft => { | ||
| * draft.total = users().length; | ||
| * draft.active = users().filter(u => u.active).length; | ||
| * }, | ||
| * { total: 0, active: 0 } | ||
| * ); | ||
| * | ||
| * // Return form — produce a derived collection. Reconciled by `id` so each | ||
| * // surviving user keeps the same store identity across recomputes. | ||
| * const activeUsers = createProjection<User[]>( | ||
| * () => allUsers().filter(u => u.active), | ||
| * [] | ||
| * ); | ||
| * ``` | ||
| * | ||
| * @see {@link https://github.com/solidjs/x-reactivity#createprojection} | ||
| */ function createProjection(e, r, t) { | ||
| return createProjectionInternal(e, r, t).store; | ||
| } | ||
| /** | ||
| * Shared projection computed body used by both `createProjection` and the derived | ||
| * form of `createOptimisticStore`. Encapsulates the write-trap draft, `storeSetter` | ||
| * wrapping, the `handleAsync` subscription with a setter callback, and the commit | ||
| * path (which must always go through `storeSetter` so the `writeOnly` guard is | ||
| * engaged during `reconcile`'s property reads). | ||
| * | ||
| * `wrapCommit` is invoked for every commit (sync return and each async yield) and | ||
| * lets callers layer extra context around the write — e.g. the optimistic store | ||
| * re-enters `setProjectionWriteActive` so reconciles target `STORE_OVERRIDE` | ||
| * instead of `STORE_OPTIMISTIC_OVERRIDE` even when an async yield fires outside | ||
| * the outer `setProjectionWriteActive` scope. | ||
| */ function runProjectionComputed(e, r, t, o, i) { | ||
| const n = getOwner(); | ||
| let c = false; | ||
| let s; | ||
| const u = new Proxy(e, createWriteTraps(() => !c || n.Ae === s, i)); | ||
| storeSetter(u, i => { | ||
| s = r(i); | ||
| c = true; | ||
| const commit = r => { | ||
| if (r === i || r === undefined) return; | ||
| const write = () => storeSetter(e, reconcile(r, t)); | ||
| o ? o(write) : write(); | ||
| }; | ||
| commit(handleAsync(n, s, commit)); | ||
| }); | ||
| return n; | ||
| } | ||
| function createWriteTraps(e, r) { | ||
| const t = { | ||
| get(e, r) { | ||
| let o; | ||
| setWriteOverride(true); | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| o = e[r]; | ||
| } finally { | ||
| setWriteOverride(false); | ||
| setProjectionWriteActive(false); | ||
| } | ||
| if (r === $TARGET) return o; | ||
| return typeof o === "object" && o !== null ? new Proxy(o, t) : o; | ||
| }, | ||
| has(e, r) { | ||
| let t; | ||
| setWriteOverride(true); | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| t = r in e; | ||
| } finally { | ||
| setWriteOverride(false); | ||
| setProjectionWriteActive(false); | ||
| } | ||
| return t; | ||
| }, | ||
| set(t, o, i) { | ||
| if (e && !e()) return true; | ||
| setWriteOverride(true); | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| t[o] = i; | ||
| r?.(); | ||
| } finally { | ||
| setWriteOverride(false); | ||
| setProjectionWriteActive(false); | ||
| } | ||
| return true; | ||
| }, | ||
| deleteProperty(t, o) { | ||
| if (e && !e()) return true; | ||
| setWriteOverride(true); | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| delete t[o]; | ||
| r?.(); | ||
| } finally { | ||
| setWriteOverride(false); | ||
| setProjectionWriteActive(false); | ||
| } | ||
| return true; | ||
| } | ||
| }; | ||
| return t; | ||
| } | ||
| export { createProjection, createProjectionInternal, createWriteTraps, runProjectionComputed }; |
| import { setSignal, untrack } from "../core/core.js"; | ||
| import "../core/scheduler.js"; | ||
| import "../core/invariants.js"; | ||
| import "../core/verdict.js"; | ||
| import "../core/effect.js"; | ||
| import { $TARGET, STORE_OVERRIDE, STORE_OPTIMISTIC_OVERRIDE, STORE_VALUE, STORE_NODE, storeLookup, STORE_LOOKUP, $PROXY, isWrappable, wrap, notifySelf, $TRACK, STORE_HAS, getKeys, $DELETED, symbolKeyedRecords } from "./store.js"; | ||
| // Enumerate a node record's keys. Keeps the common string-key path on the | ||
| // `Object.keys` fast path; only records currently holding a user symbol node | ||
| // (marked by `getNode`) pay for symbol enumeration. `$TRACK` is the only | ||
| // internal symbol a node record can hold and callers handle it separately. | ||
| function nodeKeys(e) { | ||
| const t = Object.keys(e); | ||
| if (symbolKeyedRecords.has(e)) { | ||
| const r = Object.getOwnPropertySymbols(e); | ||
| for (let e = 0, n = r.length; e < n; e++) { | ||
| if (r[e] !== $TRACK) t.push(r[e]); | ||
| } | ||
| } | ||
| return t; | ||
| } | ||
| function unwrap(e) { | ||
| // Primitives can't be store proxies; skip the symbol lookups (which box the | ||
| // primitive) for the common leaf case. | ||
| if (e === null || typeof e !== "object") return e; | ||
| return e[$TARGET]?.[STORE_VALUE] ?? e; | ||
| } | ||
| function getOverrideValue(e, t, r, n) { | ||
| if (n && r in n) return n[r]; | ||
| return t && r in t ? t[r] : e[r]; | ||
| } | ||
| // Append `o`'s *enumerable* own symbol keys from a pre-fetched symbol list. | ||
| function addEnumSymbols(e, t, r) { | ||
| for (let n = 0, a = t.length; n < a; n++) { | ||
| if (Object.prototype.propertyIsEnumerable.call(e, t[n])) r.add(t[n]); | ||
| } | ||
| } | ||
| function getAllKeys(e, t, r) { | ||
| const n = getKeys(e, t); | ||
| const a = Object.keys(r); | ||
| // `value` can be a wrapped store (store-in-store) whose ownKeys trap tracks; | ||
| // mirror `getKeys` and enumerate its symbols untracked in that case. | ||
| const i = e[$TARGET] ? untrack(() => Object.getOwnPropertySymbols(e)) : Object.getOwnPropertySymbols(e); | ||
| const o = Object.getOwnPropertySymbols(r); | ||
| // Symbol-free diff (the overwhelmingly common case) stays on the exact | ||
| // pre-#2851 path, including the identical-key-sets fast path from #2756. | ||
| if (i.length === 0 && o.length === 0) { | ||
| if (n.length === a.length) { | ||
| let e = true; | ||
| for (let t = 0; t < n.length; t++) { | ||
| if (n[t] !== a[t]) { | ||
| e = false; | ||
| break; | ||
| } | ||
| } | ||
| if (e) return n; | ||
| } | ||
| const e = new Set(n); | ||
| for (let t = 0; t < a.length; t++) e.add(a[t]); | ||
| return Array.from(e); | ||
| } | ||
| // Symbol-aware diff (#2851): base symbols join the set, then override | ||
| // adds/deletes are re-applied so a `$DELETED` symbol stays deleted, then | ||
| // `next`'s keys (a key present in next is never deleted by the diff). | ||
| const l = new Set(n); | ||
| addEnumSymbols(e, i, l); | ||
| if (t) { | ||
| for (const e of Reflect.ownKeys(t)) { | ||
| t[e] === $DELETED ? l.delete(e) : l.add(e); | ||
| } | ||
| } | ||
| for (let e = 0; e < a.length; e++) l.add(a[e]); | ||
| addEnumSymbols(r, o, l); | ||
| return Array.from(l); | ||
| } | ||
| // Array entries can be `null`/`undefined`/primitives, not just keyed objects. | ||
| // These helpers keep the keyed paths from passing a non-object to `keyFn` (which | ||
| // assumes an object) or to `wrap()` (which assumes a wrappable value). | ||
| function wrapValue(e, t) { | ||
| return isWrappable(e) ? wrap(e, t) : e; | ||
| } | ||
| function itemKey(e, t) { | ||
| return isWrappable(e) ? t(e) : e; | ||
| } | ||
| function keyedMatch(e, t, r) { | ||
| return e === t || isWrappable(e) && isWrappable(t) && r(e) === r(t); | ||
| } | ||
| // Array reconciliation updates the slots it visits, then swaps STORE_VALUE. | ||
| // Previously tracked keys that are absent from `next` still need invalidating, | ||
| // and `in` dependencies should follow the new value's membership. Use | ||
| // membership rather than length arithmetic so sparse arrays and named array | ||
| // props behave like normal property reads. | ||
| function syncArrayNodeMembership(e, t) { | ||
| let r = e[STORE_NODE]; | ||
| if (r) { | ||
| const e = nodeKeys(r); | ||
| for (let n = 0, a = e.length; n < a; n++) { | ||
| const a = e[n]; | ||
| a in t || setSignal(r[a], undefined); | ||
| } | ||
| } | ||
| if (r = e[STORE_HAS]) { | ||
| const e = nodeKeys(r); | ||
| for (let n = 0, a = e.length; n < a; n++) { | ||
| const a = e[n]; | ||
| setSignal(r[a], a in t); | ||
| } | ||
| } | ||
| } | ||
| // Reconcile a single array slot: recurse into a wrappable pair, otherwise replace | ||
| // the node's value outright (covers object→primitive and primitive→object). | ||
| function applyArrayItem(e, t, r, n, a) { | ||
| if (isWrappable(e) && isWrappable(t)) { | ||
| const i = wrap(t, r); | ||
| n && setSignal(n, i); | ||
| applyState(e, i, a); | ||
| } else n && setSignal(n, wrapValue(e, r)); | ||
| } | ||
| // Dispatcher: every applyState call (including recursion) checks for the | ||
| // presence of override / optimistic-override slots once and routes to the | ||
| // appropriate body. The fast body never calls `getOverrideValue` and never | ||
| // branches on a `fastPath` boolean, so V8 sees a tighter, more inlinable | ||
| // shape for the overwhelmingly common case of plain stores. | ||
| function applyState(e, t, r) { | ||
| // Array items and root calls can pass a store proxy as `next`; normalize to | ||
| // its raw value or the swap would set a store's STORE_VALUE to its own proxy. | ||
| e = unwrap(e); | ||
| const n = t?.[$TARGET]; | ||
| if (!n) return; | ||
| if (n[STORE_OVERRIDE] || n[STORE_OPTIMISTIC_OVERRIDE]) { | ||
| applyStateSlow(e, n, r); | ||
| } else { | ||
| applyStateFast(e, n, r); | ||
| } | ||
| } | ||
| function applyStateFast(e, t, r) { | ||
| const n = t[STORE_VALUE]; | ||
| if (e === n) return; | ||
| const a = t[STORE_NODE]; | ||
| // swap | ||
| (t[STORE_LOOKUP] || storeLookup).set(e, t[$PROXY]); | ||
| t[STORE_VALUE] = e; | ||
| // merge | ||
| if (Array.isArray(n)) { | ||
| let i = false; | ||
| const o = n.length; | ||
| if (e.length && o && isWrappable(e[0]) && r(e[0]) != null) { | ||
| let l, s, p, f, c, u, y, S; | ||
| for (p = 0, f = Math.min(o, e.length); p < f && keyedMatch(u = n[p], e[p], r); p++) { | ||
| isWrappable(u) && isWrappable(e[p]) && applyState(e[p], wrap(u, t), r); | ||
| } | ||
| const E = new Array(e.length), O = new Map; | ||
| for (f = o - 1, c = e.length - 1; f >= p && c >= p && keyedMatch(u = n[f], e[c], r); f--, | ||
| c--) { | ||
| E[c] = u; | ||
| } | ||
| if (p > c || p > f) { | ||
| for (s = p; s <= c; s++) { | ||
| i = true; | ||
| a?.[s] && setSignal(a[s], wrapValue(e[s], t)); | ||
| } | ||
| for (;s < e.length; s++) { | ||
| i = true; | ||
| applyArrayItem(e[s], E[s], t, a?.[s], r); | ||
| } | ||
| syncArrayNodeMembership(t, e); | ||
| (i || o !== e.length) && notifySelf(t); | ||
| o !== e.length && a?.length && setSignal(a.length, e.length); | ||
| return; | ||
| } | ||
| y = new Array(c + 1); | ||
| for (s = c; s >= p; s--) { | ||
| u = e[s]; | ||
| S = itemKey(u, r); | ||
| l = O.get(S); | ||
| y[s] = l === undefined ? -1 : l; | ||
| O.set(S, s); | ||
| } | ||
| for (l = p; l <= f; l++) { | ||
| u = n[l]; | ||
| S = itemKey(u, r); | ||
| s = O.get(S); | ||
| if (s !== undefined && s !== -1) { | ||
| E[s] = u; | ||
| s = y[s]; | ||
| O.set(S, s); | ||
| } | ||
| } | ||
| for (s = p; s < e.length; s++) { | ||
| if (s in E) { | ||
| applyArrayItem(e[s], E[s], t, a?.[s], r); | ||
| } else a?.[s] && setSignal(a[s], wrapValue(e[s], t)); | ||
| } | ||
| if (p < e.length) i = true; | ||
| } else if (e.length) { | ||
| for (let o = 0, l = e.length; o < l; o++) { | ||
| const l = n[o]; | ||
| if (isWrappable(l) && isWrappable(e[o])) applyState(e[o], wrap(l, t), r); else { | ||
| if (l !== e[o]) i = true; | ||
| a?.[o] && setSignal(a[o], wrapValue(e[o], t)); | ||
| } | ||
| } | ||
| } | ||
| syncArrayNodeMembership(t, e); | ||
| if (o !== e.length) { | ||
| i = true; | ||
| a?.length && setSignal(a.length, e.length); | ||
| } | ||
| i && notifySelf(t); | ||
| return; | ||
| } | ||
| // values | ||
| let i = t[STORE_NODE]; | ||
| if (i) { | ||
| const a = i[$TRACK]; | ||
| const o = a ? getAllKeys(n, undefined, e) : nodeKeys(i); | ||
| for (let l = 0, s = o.length; l < s; l++) { | ||
| const s = o[l]; | ||
| const p = i[s]; | ||
| const f = unwrap(n[s]); | ||
| let c = unwrap(e[s]); | ||
| if (f === c) continue; | ||
| if (!f || !isWrappable(f) || !isWrappable(c) || Array.isArray(f) !== Array.isArray(c) || r(f) != null && r(f) !== r(c)) { | ||
| a && setSignal(a, void 0); | ||
| p && setSignal(p, isWrappable(c) ? wrap(c, t) : c); | ||
| } else applyState(c, wrap(f, t), r); | ||
| } | ||
| } | ||
| // has | ||
| if (i = t[STORE_HAS]) { | ||
| const t = nodeKeys(i); | ||
| for (let r = 0, n = t.length; r < n; r++) { | ||
| const n = t[r]; | ||
| setSignal(i[n], n in e); | ||
| } | ||
| } | ||
| } | ||
| function applyStateSlow(e, t, r) { | ||
| const n = t[STORE_VALUE]; | ||
| const a = t[STORE_OVERRIDE]; | ||
| const i = t[STORE_OPTIMISTIC_OVERRIDE]; | ||
| let o = t[STORE_NODE]; | ||
| // swap | ||
| (t[STORE_LOOKUP] || storeLookup).set(e, t[$PROXY]); | ||
| t[STORE_VALUE] = e; | ||
| t[STORE_OVERRIDE] = undefined; | ||
| // merge | ||
| if (Array.isArray(n)) { | ||
| let l = false; | ||
| const s = getOverrideValue(n, a, "length", i); | ||
| if (e.length && s && isWrappable(e[0]) && r(e[0]) != null) { | ||
| let p, f, c, u, y, S, E, O; | ||
| for (c = 0, u = Math.min(s, e.length); c < u && keyedMatch(S = getOverrideValue(n, a, c, i), e[c], r); c++) { | ||
| isWrappable(S) && isWrappable(e[c]) && applyState(e[c], wrap(S, t), r); | ||
| } | ||
| const d = new Array(e.length), R = new Map; | ||
| for (u = s - 1, y = e.length - 1; u >= c && y >= c && keyedMatch(S = getOverrideValue(n, a, u, i), e[y], r); u--, | ||
| y--) { | ||
| d[y] = S; | ||
| } | ||
| if (c > y || c > u) { | ||
| for (f = c; f <= y; f++) { | ||
| l = true; | ||
| o?.[f] && setSignal(o[f], wrapValue(e[f], t)); | ||
| } | ||
| for (;f < e.length; f++) { | ||
| l = true; | ||
| applyArrayItem(e[f], d[f], t, o?.[f], r); | ||
| } | ||
| const n = e.length; | ||
| syncArrayNodeMembership(t, e); | ||
| (l || s !== n) && notifySelf(t); | ||
| s !== n && o?.length && setSignal(o.length, n); | ||
| return; | ||
| } | ||
| E = new Array(y + 1); | ||
| for (f = y; f >= c; f--) { | ||
| S = e[f]; | ||
| O = itemKey(S, r); | ||
| p = R.get(O); | ||
| E[f] = p === undefined ? -1 : p; | ||
| R.set(O, f); | ||
| } | ||
| for (p = c; p <= u; p++) { | ||
| S = getOverrideValue(n, a, p, i); | ||
| O = itemKey(S, r); | ||
| f = R.get(O); | ||
| if (f !== undefined && f !== -1) { | ||
| d[f] = S; | ||
| f = E[f]; | ||
| R.set(O, f); | ||
| } | ||
| } | ||
| for (f = c; f < e.length; f++) { | ||
| if (f in d) { | ||
| applyArrayItem(e[f], d[f], t, o?.[f], r); | ||
| } else o?.[f] && setSignal(o[f], wrapValue(e[f], t)); | ||
| } | ||
| if (c < e.length) l = true; | ||
| } else if (e.length) { | ||
| for (let s = 0, p = e.length; s < p; s++) { | ||
| const p = getOverrideValue(n, a, s, i); | ||
| if (isWrappable(p) && isWrappable(e[s])) applyState(e[s], wrap(p, t), r); else { | ||
| if (p !== e[s]) l = true; | ||
| o?.[s] && setSignal(o[s], wrapValue(e[s], t)); | ||
| } | ||
| } | ||
| } | ||
| const p = e.length; | ||
| syncArrayNodeMembership(t, e); | ||
| if (s !== p) { | ||
| l = true; | ||
| o?.length && setSignal(o.length, p); | ||
| } | ||
| l && notifySelf(t); | ||
| return; | ||
| } | ||
| // values | ||
| if (o) { | ||
| const l = o[$TRACK]; | ||
| const s = l ? getAllKeys(n, a, e) : nodeKeys(o); | ||
| for (let p = 0, f = s.length; p < f; p++) { | ||
| const f = s[p]; | ||
| const c = o[f]; | ||
| const u = unwrap(getOverrideValue(n, a, f, i)); | ||
| let y = unwrap(e[f]); | ||
| if (u === y) continue; | ||
| if (!u || !isWrappable(u) || !isWrappable(y) || Array.isArray(u) !== Array.isArray(y) || r(u) != null && r(u) !== r(y)) { | ||
| l && setSignal(l, void 0); | ||
| c && setSignal(c, isWrappable(y) ? wrap(y, t) : y); | ||
| } else applyState(y, wrap(u, t), r); | ||
| } | ||
| } | ||
| // has | ||
| if (o = t[STORE_HAS]) { | ||
| const t = nodeKeys(o); | ||
| for (let r = 0, n = t.length; r < n; r++) { | ||
| const n = t[r]; | ||
| setSignal(o[n], n in e); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Returns a draft-mutating function that smart-merges `value` into a store, | ||
| * preserving the identity of items whose `key` field matches between old and | ||
| * new states. Useful when applying server payloads or full-replacement data | ||
| * onto an existing store without losing fine-grained reactivity. | ||
| * | ||
| * Items with the same key are updated in place (only changed properties | ||
| * trigger updates). Items added or removed update the corresponding signals. | ||
| * | ||
| * @param value the next state to merge in | ||
| * @param key property name (string) or extractor function for stable identity | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const [todos, setTodos] = createStore<Todo[]>([]); | ||
| * | ||
| * async function refresh() { | ||
| * const fresh = await api.getTodos(); | ||
| * setTodos(reconcile(fresh, "id")); // diff-merge by `id` | ||
| * } | ||
| * ``` | ||
| */ function reconcile(e, t) { | ||
| return r => { | ||
| if (r == null) throw new Error(""); | ||
| const n = typeof t === "string" ? e => e[t] : t; | ||
| const a = n(r); | ||
| if (a !== undefined && n(e) !== a) throw new Error(""); | ||
| applyState(e, r, n); | ||
| }; | ||
| } | ||
| export { reconcile }; |
| import { $REFRESH, STORE_SNAPSHOT_PROPS, NOT_PENDING, STATUS_PENDING, NO_SNAPSHOT } from "../core/constants.js"; | ||
| import { suppressComputedRecompute, isEqual, signal, pendingCheckActive, untrack, setSignal, read, snapshotCaptureActive, snapshotSources } from "../core/core.js"; | ||
| import { DEV } from "../core/dev.js"; | ||
| import { getObserver } from "../core/owner.js"; | ||
| import { GlobalQueue, registerTransientStoreNode, projectionWriteActive, globalQueue } from "../core/scheduler.js"; | ||
| import "../core/invariants.js"; | ||
| import "../core/verdict.js"; | ||
| import "../core/effect.js"; | ||
| import { createProjectionInternal } from "./projection.js"; | ||
| /** | ||
| * Brand symbols used internally by the store proxy / projection plumbing. | ||
| * Cross-package wiring; not part of the user-facing API. | ||
| * | ||
| * @internal | ||
| */ const $TRACK = Symbol(0), $TARGET = Symbol(0), $PROXY = Symbol(0), $DELETED = Symbol(0), | ||
| // Node-map slot carrying a record-level `affects()` mark: any read through | ||
| // the record witnesses it into the active isPending() probe. | ||
| $AFFECTS = Symbol(0); | ||
| const STORE_VALUE = "v", STORE_OVERRIDE = "o", STORE_OPTIMISTIC_OVERRIDE = "x", STORE_NODE = "n", STORE_HAS = "h", STORE_CUSTOM_PROTO = "c", STORE_WRAP = "w", STORE_LOOKUP = "l", STORE_FIREWALL = "f", STORE_OPTIMISTIC = "p"; | ||
| const STORE_SELF_PENDING = Symbol(0); | ||
| function createStoreProxy(e, t = storeTraps, r) { | ||
| let n; | ||
| if (Array.isArray(e)) { | ||
| n = []; | ||
| n.v = e; | ||
| } else { | ||
| n = { | ||
| v: e | ||
| }; | ||
| const t = e?.[$TARGET]?.[STORE_VALUE] ?? e; | ||
| const r = Object.getPrototypeOf(t); | ||
| if (r !== null && r !== Object.prototype) { | ||
| n[STORE_CUSTOM_PROTO] = true; | ||
| } | ||
| } | ||
| r && r(n); | ||
| return n[$PROXY] = new Proxy(n, t); | ||
| } | ||
| const storeLookup = new WeakMap; | ||
| // Node records that hold at least one user (non-`$TRACK`) symbol-keyed node. | ||
| // Lets reconcile enumerate symbols only for records that need it (#2851). | ||
| const symbolKeyedRecords = new WeakSet; | ||
| function wrap(e, t) { | ||
| if (t?.[STORE_WRAP]) return t[STORE_WRAP](e, t); | ||
| let r = e[$PROXY] || storeLookup.get(e); | ||
| if (!r) storeLookup.set(e, r = createStoreProxy(e)); | ||
| return r; | ||
| } | ||
| function isWrappable(e) { | ||
| if (e == null || typeof e !== "object" || Object.isFrozen(e)) return false; | ||
| // Dynamic Node check (kept dynamic so test/SSR overrides of `globalThis.Node` | ||
| // are observed at call time). | ||
| return typeof Node === "undefined" || !(e instanceof Node); | ||
| } | ||
| let writeOverride = false; | ||
| function setWriteOverride(e) { | ||
| writeOverride = e; | ||
| } | ||
| function writeOnly(e) { | ||
| return writeOverride || !!Writing?.has(e); | ||
| } | ||
| function unwrapStoreValue(e, t, r) { | ||
| const n = e?.[$TARGET] || r?.get(e)?.[$TARGET]; | ||
| if (!n) return e; | ||
| const o = n[STORE_OVERRIDE]; | ||
| if (!o) return n[STORE_VALUE]; | ||
| if (!t) t = new Map; | ||
| if (t.has(e)) return t.get(e); | ||
| const i = n[STORE_VALUE]; | ||
| const s = Array.isArray(i); | ||
| const E = s ? [] : Object.create(Object.getPrototypeOf(i)); | ||
| t.set(e, E); | ||
| r = n[STORE_LOOKUP] ?? storeLookup; | ||
| for (const e of getKeys(i, o)) { | ||
| if (s && e === "length") continue; | ||
| const n = e in o ? o[e] : i[e]; | ||
| if (n !== $DELETED) E[e] = unwrapStoreValue(n, t, r); | ||
| } | ||
| if (s) E.length = o.length ?? i.length; | ||
| return E; | ||
| } | ||
| function isPrototypePollutionKey(e) { | ||
| return e === "__proto__" || e === "constructor" || e === "prototype"; | ||
| } | ||
| // Own enumerable keys including symbols (`Object.keys` drops symbol-keyed props). #2769 | ||
| function ownEnumerableKeys(e) { | ||
| return Reflect.ownKeys(e).filter(t => Object.prototype.propertyIsEnumerable.call(e, t)); | ||
| } | ||
| /** | ||
| * Single chokepoint for the store's layered value resolution: returns the | ||
| * override layer (optimistic first, then regular) that shadows `property`, or | ||
| * `undefined` when the base `STORE_VALUE` is authoritative. Every trap must | ||
| * resolve through this — hand-inlining the layer order is how the optimistic | ||
| * layer gets missed (#2850). | ||
| */ function getOverlayLayer(e, t) { | ||
| const r = e[STORE_OPTIMISTIC_OVERRIDE]; | ||
| if (r && t in r) return r; | ||
| const n = e[STORE_OVERRIDE]; | ||
| if (n && t in n) return n; | ||
| return undefined; | ||
| } | ||
| /** | ||
| * The value a store leaf's backing signal currently shows to readers: active | ||
| * override, else held pending value, else committed value. | ||
| */ function visibleNodeValue(e) { | ||
| return e.be !== undefined && e.be !== NOT_PENDING ? e.be : e.ge !== NOT_PENDING ? e.ge : e.Ue; | ||
| } | ||
| function hasOwnStoreProperty(e, t) { | ||
| // Override layers are null-prototype objects, so `in` is an own check. | ||
| const r = getOverlayLayer(e, t); | ||
| if (r) return r[t] !== $DELETED; | ||
| return Object.prototype.hasOwnProperty.call(unwrapStoreValue(e[STORE_VALUE]), t); | ||
| } | ||
| function hasInheritedAccessor(e, t) { | ||
| let r = Object.getPrototypeOf(e); | ||
| while (r && r !== Object.prototype) { | ||
| const e = Reflect.getOwnPropertyDescriptor(r, t); | ||
| if (e) return !!e.get; | ||
| r = Object.getPrototypeOf(r); | ||
| } | ||
| return false; | ||
| } | ||
| function getNodes(e, t) { | ||
| let r = e[t]; | ||
| if (!r) e[t] = r = Object.create(null); | ||
| return r; | ||
| } | ||
| function getNode(e, t, r, n, o = isEqual, i) { | ||
| if (t[r]) return t[r]; | ||
| const s = signal(n, { | ||
| equals: o, | ||
| unobserved() { | ||
| if (t[r] === s) { | ||
| delete t[r]; | ||
| // Drop the symbol-record mark once the last user symbol node is | ||
| // gone, so reconcile's fast path stops probing a now string-only | ||
| // record. Runs only on symbol-node cleanup (cold), never on reconcile. | ||
| if (typeof r === "symbol" && r !== $TRACK && r !== $AFFECTS && symbolKeyedRecords.has(t)) { | ||
| const e = Object.getOwnPropertySymbols(t); | ||
| let r = false; | ||
| for (let t = 0, n = e.length; t < n; t++) { | ||
| if (e[t] !== $TRACK && e[t] !== $AFFECTS) { | ||
| r = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!r) symbolKeyedRecords.delete(t); | ||
| } | ||
| } | ||
| } | ||
| }, e[STORE_FIREWALL]); | ||
| if (e[STORE_OPTIMISTIC]) { | ||
| s.be = NOT_PENDING; | ||
| } | ||
| if (i && r in i) { | ||
| const e = i[r]; | ||
| s.Ye = e === undefined ? NO_SNAPSHOT : e; | ||
| snapshotSources?.add(s); | ||
| } | ||
| if (typeof r === "symbol" && r !== $TRACK && r !== $AFFECTS) symbolKeyedRecords.add(t); | ||
| // A node born inside a live keyless mark's identity scope inherits the mark | ||
| // (the declaration walk could only cover nodes that existed then). The | ||
| // record's own $AFFECTS carrier is the mark's channel, never a member. | ||
| if (r !== $AFFECTS && affectsScopes.size) inheritAffectsMarks(s, e[STORE_VALUE]); | ||
| return t[r] = s; | ||
| } | ||
| /** | ||
| * Scope inheritance for late-created nodes: every live keyless mark whose | ||
| * identity scope contains the owning record's raw gets counted on the new | ||
| * node. Inherited marks live exactly as long as the scope's carrier — the | ||
| * release hook below drops them with the entry. | ||
| */ function inheritAffectsMarks(e, t) { | ||
| // A live scope exists, so affects.ts already installed the mark engine. | ||
| for (const [r, n] of affectsScopes) { | ||
| if (r.P && n.scope.has(t)) { | ||
| GlobalQueue.D(e); | ||
| n.inherited.push(e); | ||
| } | ||
| } | ||
| } | ||
| const affectsScopes = new Map; | ||
| /** | ||
| * Snapshots the identities reachable from `value` into `scope`, reading | ||
| * through write overlays (an optimistic row pushed before the declaration is | ||
| * in motion too). Untracked by construction: walks raw values, never traps. | ||
| * Every LIVE node under each reachable record — property leaves, `$TRACK`, | ||
| * and has-nodes — collects into `found`: those are the graph edges existing | ||
| * readers subscribed through, so the mark registers on them directly and | ||
| * rides the status rails to everything derived. (Nodes born later inherit | ||
| * from the scope in `getNode`.) | ||
| */ function walkAffectsScope(e, t, r, n, | ||
| // Cycle guard, fresh per declaration: the scope itself can't serve — a | ||
| // re-declaration on the same carrier unions into a scope that already | ||
| // holds the root, and must still descend to pick up records added since. | ||
| o) { | ||
| if (!isWrappable(e)) return; | ||
| const i = e[$TARGET] || (n ?? storeLookup).get(e)?.[$TARGET]; | ||
| const s = i ? i[STORE_VALUE] : e; | ||
| if (o.has(s)) return; | ||
| o.add(s); | ||
| t.scope.add(s); | ||
| let E; | ||
| if (i) { | ||
| collectRecordNodes(i[STORE_NODE], r); | ||
| collectRecordNodes(i[STORE_HAS], r); | ||
| E = mergedOverlay(i); | ||
| n = i[STORE_LOOKUP] ?? n; | ||
| } | ||
| if (Array.isArray(s)) { | ||
| const e = E?.length ?? s.length; | ||
| for (let i = 0; i < e; i++) { | ||
| const e = E && i in E ? E[i] : s[i]; | ||
| if (e !== $DELETED) walkAffectsScope(e, t, r, n, o); | ||
| } | ||
| } else { | ||
| const e = getKeys(s, E); | ||
| for (let i = 0, O = e.length; i < O; i++) { | ||
| const O = getPropertyDescriptor(s, E, e[i]); | ||
| if (!O || O.get) continue; | ||
| walkAffectsScope(O.value, t, r, n, o); | ||
| } | ||
| } | ||
| } | ||
| /** All live signal nodes of one record's node map (string + symbol keyed). */ function collectRecordNodes(e, t) { | ||
| if (!e) return; | ||
| for (const r of Object.keys(e)) t.push(e[r]); | ||
| const r = Object.getOwnPropertySymbols(e); | ||
| for (let n = 0, o = r.length; n < o; n++) { | ||
| // Another mark's carrier is its own channel — counting it here would | ||
| // extend that sibling scope's lifetime to this declaration's. | ||
| if (r[n] !== $AFFECTS) t.push(e[r[n]]); | ||
| } | ||
| } | ||
| /** | ||
| * Witness live mark coverage of a record into the active isPending() probe. | ||
| * Tracked reads don't need this — they go through real signal nodes, which | ||
| * carry marks directly (declaration walk or birth inheritance). This covers | ||
| * UNTRACKED probes reading through records whose nodes never materialized | ||
| * (no observer ever subscribed, so no node exists to carry the mark). | ||
| * Callers guard on `pendingCheckActive`, so plain reads never pay for this. | ||
| * | ||
| * @internal | ||
| */ function witnessAffectsMark(e) { | ||
| // Callers guard on `pendingCheckActive`, which only flips inside | ||
| // isPending() — the verdict layer is loaded and its hook installed. | ||
| const t = e[STORE_NODE]?.[$AFFECTS]; | ||
| if (t?.P) GlobalQueue.Zt(t); | ||
| if (affectsScopes.size) { | ||
| const r = e[STORE_VALUE]; | ||
| for (const [e, n] of affectsScopes) { | ||
| if (e !== t && e.P && n.scope.has(r)) GlobalQueue.Zt(e); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Resolves the store nodes an `affects()` declaration marks: with a `key`, | ||
| * the named slot's leaf node (upserted so the mark has an addressable | ||
| * carrier); without, the record's $AFFECTS carrier plus every LIVE node in | ||
| * its subtree (the edges existing readers subscribed through), with the | ||
| * subtree's identities snapshotted into the mark's scope so nodes created | ||
| * during the window — and untracked probes over captured proxies — resolve | ||
| * against it (#2882). | ||
| * | ||
| * @internal | ||
| */ function getStoreAffectsNodes(e, t) { | ||
| const r = getNodes(e, STORE_NODE); | ||
| if (t === undefined) { | ||
| const t = getNode(e, r, $AFFECTS, undefined, false); | ||
| GlobalQueue.T ||= e => { | ||
| const t = affectsScopes.get(e); | ||
| if (!t) return; | ||
| affectsScopes.delete(e); | ||
| for (let e = 0; e < t.inherited.length; e++) GlobalQueue.F(t.inherited[e]); | ||
| }; | ||
| let n = affectsScopes.get(t); | ||
| if (!n) affectsScopes.set(t, n = { | ||
| scope: new Set, | ||
| inherited: [] | ||
| }); | ||
| const o = [ t ]; | ||
| walkAffectsScope(e[$PROXY], n, o, e[STORE_LOOKUP], new Set); | ||
| return o; | ||
| } | ||
| if (r[t]) return [ r[t] ]; | ||
| const n = getOverlayLayer(e, t); | ||
| const o = n ? n[t] : e[STORE_VALUE][t]; | ||
| const i = o === $DELETED ? undefined : o; | ||
| return [ upsertStoreNode(e, r, t, i, e[STORE_SNAPSHOT_PROPS]) ]; | ||
| } | ||
| function trackSelf(e, t = $TRACK) { | ||
| if (!getObserver()) return; | ||
| read(getNode(e, getNodes(e, STORE_NODE), t, undefined, false)); | ||
| // Store-in-store: structural notifications (reconcile, notifySelf) land on | ||
| // the wrapped source's own self-node, never on this wrapper view's. Chain | ||
| // the read through so enumeration/$TRACK on the wrapper observes them | ||
| // (#2864). Property reads already chain naturally via the inner get trap. | ||
| // An override layer on the view is a hold (A17) — the shown structure is | ||
| // the overlay's, so don't subscribe through it; clearing the layer notifies | ||
| // this view's own self-node and the re-run re-establishes the chain. | ||
| if (t === $TRACK && !e[STORE_OVERRIDE] && !e[STORE_OPTIMISTIC_OVERRIDE] && e[STORE_VALUE][$TARGET]) e[STORE_VALUE][$TRACK]; | ||
| } | ||
| function notifySelf(e) { | ||
| const t = e[STORE_NODE]?.[$TRACK]; | ||
| t && setSignal(t, e[STORE_OPTIMISTIC] && !projectionWriteActive ? STORE_SELF_PENDING : undefined); | ||
| } | ||
| /** | ||
| * The write overlay a walk must read through: optimistic writes shadow | ||
| * regular pending writes, the same resolution order as every proxy trap and | ||
| * `reconcile` (#2850). Merging allocates only in the rare both-present case | ||
| * (a derived optimistic store with an in-flight projection commit). | ||
| */ function mergedOverlay(e) { | ||
| const t = e[STORE_OVERRIDE]; | ||
| const r = e[STORE_OPTIMISTIC_OVERRIDE]; | ||
| return t && r ? { | ||
| ...t, | ||
| ...r | ||
| } : r ?? t; | ||
| } | ||
| function getKeys(e, t, r = true) { | ||
| // Plain objects can't trigger proxy traps — only pay for the untrack | ||
| // closure when the source is itself a wrapped store (store-in-store). | ||
| const n = e[$TARGET] ? untrack(() => r ? Object.keys(e) : Reflect.ownKeys(e)) : r ? Object.keys(e) : Reflect.ownKeys(e); | ||
| if (!t) return n; | ||
| const o = new Set(n); | ||
| const i = Reflect.ownKeys(t); | ||
| for (const e of i) { | ||
| if (t[e] !== $DELETED) o.add(e); else o.delete(e); | ||
| } | ||
| return Array.from(o); | ||
| } | ||
| function getPropertyDescriptor(e, t, r) { | ||
| if (t && r in t) { | ||
| if (t[r] === $DELETED) return void 0; | ||
| const n = Reflect.getOwnPropertyDescriptor(t, r); | ||
| if (n?.get || n?.set) return n; | ||
| // Plain writes live in the override while the source keeps its old value. | ||
| // Preserve the source descriptor flags, but report the current override | ||
| // value. Source accessors cannot be patched with a value, and inherited | ||
| // properties have no source own descriptor, so those keep their descriptor. | ||
| const o = Reflect.getOwnPropertyDescriptor(e, r); | ||
| if (!o) return n; | ||
| if (o.get || o.set) return o; | ||
| // Reflect returns a fresh descriptor, so patching in place is safe and | ||
| // avoids an allocation on Object.keys/spread over written stores. | ||
| o.value = t[r]; | ||
| return o; | ||
| } | ||
| return Reflect.getOwnPropertyDescriptor(e, r); | ||
| } | ||
| function prepareStoreWrite(e, t, r) { | ||
| if (e[STORE_OPTIMISTIC]) { | ||
| const t = e[STORE_FIREWALL]; | ||
| if (t?.ve) { | ||
| globalQueue.initTransition(t.ve); | ||
| } | ||
| } | ||
| const n = e[STORE_VALUE]; | ||
| const o = n[r]; | ||
| if (snapshotCaptureActive && typeof r !== "symbol" && !((e[STORE_FIREWALL]?.i ?? 0) & STATUS_PENDING)) { | ||
| if (!e[STORE_SNAPSHOT_PROPS]) { | ||
| e[STORE_SNAPSHOT_PROPS] = Object.create(null); | ||
| snapshotSources?.add(e); | ||
| } | ||
| if (!(r in e[STORE_SNAPSHOT_PROPS])) { | ||
| e[STORE_SNAPSHOT_PROPS][r] = o; | ||
| } | ||
| } | ||
| const i = e[STORE_OPTIMISTIC] && !projectionWriteActive; | ||
| const s = i ? STORE_OPTIMISTIC_OVERRIDE : STORE_OVERRIDE; | ||
| return { | ||
| base: o, | ||
| overrideKey: s, | ||
| state: n | ||
| }; | ||
| } | ||
| /** | ||
| * Registers the store for transition reversion. Called only once a write is | ||
| * known to be effective — ineffective writes (same value, delete of an absent | ||
| * property) are no-ops and must not entangle the store. Optimistic writes are | ||
| * verdict-inert (question-scoped pending model): no mask is armed — the write | ||
| * neither pends its own slot nor silences anyone else's. | ||
| */ function armOptimisticStoreWrite(e, t) { | ||
| // STORE_OPTIMISTIC is only set by createOptimisticStore, which installs the | ||
| // optimistic engine before wrapping. | ||
| if (e[STORE_OPTIMISTIC] && !projectionWriteActive) { | ||
| GlobalQueue.Vt(t); | ||
| } | ||
| } | ||
| function upsertStoreNode(e, t, r, n, o) { | ||
| if (t[r]) return t[r]; | ||
| const i = isWrappable(n) ? wrap(n, e) : n; | ||
| const s = getNode(e, t, r, i, isEqual, o); | ||
| registerTransientStoreNode(s); | ||
| return s; | ||
| } | ||
| function notifyStoreProperty(e, t, r, n, o, i) { | ||
| // Cold writes upsert a transient pending node so untracked reads batch like signals. | ||
| // Skip for projection writes (different commit semantics) and for optimistic stores | ||
| // (whose whole purpose is immediate visibility via STORE_OPTIMISTIC_OVERRIDE). | ||
| const s = projectionWriteActive || e[STORE_OPTIMISTIC]; | ||
| const E = r !== "delete"; | ||
| const O = e[STORE_HAS]?.[t]; | ||
| if (O) { | ||
| setSignal(O, E); | ||
| } else if (!s && r !== "invalidate" && i !== E) { | ||
| const r = upsertStoreNode(e, getNodes(e, STORE_HAS), t, i); | ||
| setSignal(r, E); | ||
| } | ||
| const c = getNodes(e, STORE_NODE); | ||
| if (r === "set") { | ||
| if (c[t]) { | ||
| setSignal(c[t], () => isWrappable(n) ? wrap(n, e) : n); | ||
| } else if (!s) { | ||
| const r = upsertStoreNode(e, c, t, o, e[STORE_SNAPSHOT_PROPS]); | ||
| setSignal(r, () => isWrappable(n) ? wrap(n, e) : n); | ||
| } | ||
| } else if (r === "invalidate") { | ||
| if (c[t]) { | ||
| setSignal(c[t], {}); | ||
| delete c[t]; | ||
| } | ||
| } else { | ||
| if (c[t]) { | ||
| setSignal(c[t], undefined); | ||
| } else if (!s) { | ||
| const r = upsertStoreNode(e, c, t, o, e[STORE_SNAPSHOT_PROPS]); | ||
| setSignal(r, undefined); | ||
| } | ||
| } | ||
| notifySelf(e); | ||
| } | ||
| let Writing = null; | ||
| const storeTraps = { | ||
| get(e, t, r) { | ||
| if (t === $TARGET) return e; | ||
| if (t === $PROXY) return r; | ||
| if (t === $REFRESH) return e[STORE_FIREWALL]; | ||
| if (pendingCheckActive) witnessAffectsMark(e); | ||
| if (t === $TRACK) { | ||
| trackSelf(e); | ||
| return r; | ||
| } | ||
| const n = getObserver() === e[STORE_FIREWALL]; | ||
| const o = getNodes(e, STORE_NODE); | ||
| const i = n ? undefined : o[t]; | ||
| const s = e[STORE_VALUE]; | ||
| if (!i && !e[STORE_OVERRIDE] && !e[STORE_OPTIMISTIC_OVERRIDE] && !e[STORE_CUSTOM_PROTO] && !e[STORE_OPTIMISTIC] && !e[STORE_SNAPSHOT_PROPS] && !s[$TARGET] && !(t in s) && getObserver() && !n && !writeOnly(r)) { | ||
| return read(getNode(e, o, t, undefined)); | ||
| } | ||
| const E = getOverlayLayer(e, t); | ||
| const O = !!E; | ||
| const c = !!e[STORE_VALUE][$TARGET]; | ||
| const f = E ?? e[STORE_VALUE]; | ||
| if (!i) { | ||
| const n = Object.getOwnPropertyDescriptor(f, t); | ||
| if (n && n.get) return n.get.call(r); | ||
| if (!n && !O && e[STORE_CUSTOM_PROTO]) { | ||
| const e = unwrapStoreValue(f); | ||
| if (hasInheritedAccessor(e, t)) { | ||
| return Reflect.get(f, t, r); | ||
| } | ||
| } | ||
| } | ||
| if (writeOnly(r)) { | ||
| if (isPrototypePollutionKey(t) && !hasOwnStoreProperty(e, t)) return undefined; | ||
| let r = i && (O || !c) ? visibleNodeValue(i) : f[t]; | ||
| r === $DELETED && (r = undefined); | ||
| if (!isWrappable(r)) return r; | ||
| const n = wrap(r, e); | ||
| Writing?.add(n); | ||
| return n; | ||
| } | ||
| let S = i ? O || !c ? read(o[t]) : (read(o[t]), f[t]) : f[t]; | ||
| S === $DELETED && (S = undefined); | ||
| if (!i) { | ||
| if (!O && typeof S === "function" && !Object.prototype.hasOwnProperty.call(f, t)) { | ||
| let t; | ||
| return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? S.bind(f) : S; | ||
| } else if (getObserver() && !n) { | ||
| return read(getNode(e, o, t, isWrappable(S) ? wrap(S, e) : S, isEqual, e[STORE_SNAPSHOT_PROPS])); | ||
| } | ||
| } | ||
| return isWrappable(S) ? wrap(S, e) : S; | ||
| }, | ||
| has(e, t) { | ||
| if (t === $PROXY || t === $TRACK || t === "__proto__") return true; | ||
| if (pendingCheckActive) witnessAffectsMark(e); | ||
| const r = getOverlayLayer(e, t); | ||
| const n = r ? r[t] !== $DELETED : t in e[STORE_VALUE]; | ||
| if (writeOnly(e[$PROXY]) || getObserver() === e[STORE_FIREWALL]) return n; | ||
| const o = getNodes(e, STORE_HAS); | ||
| // If a has-node already exists, it carries the batched presence — `read()` | ||
| // returns `_value` (committed) for untracked reads and the pending value for | ||
| // downstream computes. This keeps `in` consistent with value reads. | ||
| if (o[t]) return read(o[t]); | ||
| // No node yet: `has` reflects committed presence (no pending write could change | ||
| // it without first upserting a has-node at the write site). Create + read only | ||
| // when tracking; leave untracked reads node-free. | ||
| if (getObserver()) { | ||
| return read(getNode(e, o, t, n)); | ||
| } | ||
| return n; | ||
| }, | ||
| set(e, t, r) { | ||
| if (t === "__proto__") return true; | ||
| const n = e[$PROXY]; | ||
| if (writeOnly(n)) { | ||
| untrack(() => { | ||
| const {base: o, overrideKey: i, state: s} = prepareStoreWrite(e, n, t); | ||
| const E = getOverlayLayer(e, t); | ||
| const O = E ? E[t] : o; | ||
| const c = E ? E[t] !== $DELETED : t in e[STORE_VALUE]; | ||
| const f = unwrapStoreValue(r); | ||
| // Symbol-keyed writes on arrays are metadata, not index writes — never run | ||
| // them through the numeric index/length machinery (`parseInt` on a symbol | ||
| // throws). #2769 | ||
| const S = typeof t === "string" ? Number(t) : -1; | ||
| const R = Array.isArray(s) && Number.isInteger(S) && S >= 0 && S < 4294967295 && String(S) === t; | ||
| const T = R ? S + 1 : 0; | ||
| const u = R && (getOverlayLayer(e, "length") ?? s).length; | ||
| const l = R && T > u ? T : undefined; | ||
| if (O === f && l === undefined) return true; | ||
| armOptimisticStoreWrite(e, n); | ||
| if (f !== undefined && f === o && l === undefined) delete e[i]?.[t]; else { | ||
| const r = e[i] || (e[i] = Object.create(null)); | ||
| r[t] = f; | ||
| if (l !== undefined) r.length = l; | ||
| } | ||
| notifyStoreProperty(e, t, "set", f, O, c); | ||
| // Shrinking an array's length must remove the truncated indices, otherwise | ||
| // they leak through `has`, `ownKeys`, and (tracked) index reads from the | ||
| // underlying value. Mark each as deleted and notify so reactive reads update. #2768 | ||
| if (Array.isArray(s) && t === "length" && typeof f === "number" && typeof O === "number" && f < O) { | ||
| const t = e[i] || (e[i] = Object.create(null)); | ||
| for (let r = f; r < O; r++) { | ||
| if (t[r] === $DELETED) continue; | ||
| const n = r in t ? t[r] : s[r]; | ||
| if (!(r in t) && !(r in s)) continue; | ||
| t[r] = $DELETED; | ||
| notifyStoreProperty(e, r, "delete", undefined, n, true); | ||
| } | ||
| } | ||
| // notify length change | ||
| if (Array.isArray(s) && t !== "length" && l !== undefined) { | ||
| const t = getNodes(e, STORE_NODE); | ||
| if (t.length) { | ||
| setSignal(t.length, l); | ||
| } else if (!projectionWriteActive && !e[STORE_OPTIMISTIC]) { | ||
| const r = upsertStoreNode(e, t, "length", u, e[STORE_SNAPSHOT_PROPS]); | ||
| setSignal(r, l); | ||
| } | ||
| } | ||
| if (false) ; | ||
| }); | ||
| } | ||
| return true; | ||
| }, | ||
| defineProperty(e, t, r) { | ||
| if (t === "__proto__") return true; | ||
| const n = e[$PROXY]; | ||
| if (writeOnly(n)) { | ||
| untrack(() => { | ||
| const {base: o, overrideKey: i} = prepareStoreWrite(e, n, t); | ||
| armOptimisticStoreWrite(e, n); | ||
| const s = "value" in r ? { | ||
| ...r, | ||
| value: unwrapStoreValue(r.value) | ||
| } : r; | ||
| Object.defineProperty(e[i] || (e[i] = Object.create(null)), t, s); | ||
| notifyStoreProperty(e, t, "invalidate"); | ||
| if (false) ; | ||
| }); | ||
| } | ||
| return true; | ||
| }, | ||
| deleteProperty(e, t) { | ||
| if (t === "__proto__") return true; | ||
| // Check both optimistic and regular override for existing $DELETED | ||
| const r = e[STORE_OPTIMISTIC_OVERRIDE]?.[t] === $DELETED; | ||
| const n = e[STORE_OVERRIDE]?.[t] === $DELETED; | ||
| if (writeOnly(e[$PROXY]) && !r && !n) { | ||
| untrack(() => { | ||
| const r = e[STORE_OPTIMISTIC] && !projectionWriteActive; | ||
| const n = r ? STORE_OPTIMISTIC_OVERRIDE : STORE_OVERRIDE; | ||
| const o = getOverlayLayer(e, t); | ||
| const i = o ? o[t] : e[STORE_VALUE][t]; | ||
| if (t in e[STORE_VALUE] || e[STORE_OVERRIDE] && t in e[STORE_OVERRIDE]) { | ||
| armOptimisticStoreWrite(e, e[$PROXY]); | ||
| (e[n] || (e[n] = Object.create(null)))[t] = $DELETED; | ||
| } else if (e[n] && t in e[n]) { | ||
| armOptimisticStoreWrite(e, e[$PROXY]); | ||
| delete e[n][t]; | ||
| } else return true; | ||
| notifyStoreProperty(e, t, "delete", undefined, i, true); | ||
| }); | ||
| } | ||
| return true; | ||
| }, | ||
| ownKeys(e) { | ||
| if (pendingCheckActive) witnessAffectsMark(e); | ||
| if (getObserver() !== e[STORE_FIREWALL]) trackSelf(e); | ||
| // Merge optimistic override with regular override for key enumeration | ||
| let t = getKeys(e[STORE_VALUE], e[STORE_OVERRIDE], false); | ||
| if (e[STORE_OPTIMISTIC_OVERRIDE]) { | ||
| const r = new Set(t); | ||
| for (const t of Reflect.ownKeys(e[STORE_OPTIMISTIC_OVERRIDE])) { | ||
| if (e[STORE_OPTIMISTIC_OVERRIDE][t] !== $DELETED) r.add(t); else r.delete(t); | ||
| } | ||
| t = Array.from(r); | ||
| } | ||
| return t; | ||
| }, | ||
| getOwnPropertyDescriptor(e, t) { | ||
| if (t === $PROXY) return { | ||
| value: e[$PROXY], | ||
| writable: true, | ||
| configurable: true | ||
| }; | ||
| // Check optimistic override first, but use base descriptor structure for compatibility | ||
| if (e[STORE_OPTIMISTIC_OVERRIDE] && t in e[STORE_OPTIMISTIC_OVERRIDE]) { | ||
| if (e[STORE_OPTIMISTIC_OVERRIDE][t] === $DELETED) return undefined; | ||
| const r = Reflect.getOwnPropertyDescriptor(e[STORE_OPTIMISTIC_OVERRIDE], t); | ||
| if (r?.get || r?.set || !(t in e[STORE_VALUE])) return r; | ||
| // Get base descriptor structure, override just the value | ||
| const n = getPropertyDescriptor(e[STORE_VALUE], e[STORE_OVERRIDE], t); | ||
| if (n) { | ||
| const r = Reflect.getOwnPropertyDescriptor(e, t); | ||
| const o = !r || r.configurable ? true : n.configurable; | ||
| return { | ||
| ...n, | ||
| configurable: o, | ||
| value: e[STORE_OPTIMISTIC_OVERRIDE][t] | ||
| }; | ||
| } | ||
| return { | ||
| value: e[STORE_OPTIMISTIC_OVERRIDE][t], | ||
| writable: true, | ||
| enumerable: true, | ||
| configurable: true | ||
| }; | ||
| } | ||
| const r = getPropertyDescriptor(e[STORE_VALUE], e[STORE_OVERRIDE], t); | ||
| // The proxy target is an internal node object, not the original source. When the | ||
| // source has a non-configurable property that does not also exist as non-configurable | ||
| // on the proxy target, the proxy invariant is violated: the engine requires that a | ||
| // property reported as non-configurable must actually be non-configurable on the | ||
| // target object. Override configurable to true only in that case. | ||
| if (r && !r.configurable) { | ||
| const n = Reflect.getOwnPropertyDescriptor(e, t); | ||
| if (!n || n.configurable) return { | ||
| ...r, | ||
| configurable: true | ||
| }; | ||
| } | ||
| return r; | ||
| }, | ||
| getPrototypeOf(e) { | ||
| return Object.getPrototypeOf(e[STORE_VALUE]); | ||
| } | ||
| }; | ||
| function storeSetter(e, t) { | ||
| const r = Writing; | ||
| Writing = new Set; | ||
| Writing.add(e); | ||
| try { | ||
| const r = t(e); | ||
| if (r !== e && r !== undefined) { | ||
| if (Array.isArray(r)) { | ||
| for (let t = 0, n = r.length; t < n; t++) e[t] = r[t]; | ||
| e.length = r.length; | ||
| } else { | ||
| const t = new Set([ ...ownEnumerableKeys(e), ...ownEnumerableKeys(r) ]); | ||
| t.forEach(t => { | ||
| if (t in r) e[t] = r[t]; else delete e[t]; | ||
| }); | ||
| } | ||
| } | ||
| } finally { | ||
| Writing.clear(); | ||
| Writing = r; | ||
| } | ||
| } | ||
| function createStore(e, t, r) { | ||
| const n = typeof e === "function", o = n ? createProjectionInternal(e, t, r).store : wrap(e); | ||
| return [ o, n ? e => { | ||
| // Mark the projection as manually written before notifying property nodes. | ||
| suppressComputedRecompute(o[$REFRESH]); | ||
| storeSetter(o, e); | ||
| } : e => storeSetter(o, e) ]; | ||
| } | ||
| export { $AFFECTS, $DELETED, $PROXY, $TARGET, $TRACK, STORE_CUSTOM_PROTO, STORE_FIREWALL, STORE_HAS, STORE_LOOKUP, STORE_NODE, STORE_OPTIMISTIC, STORE_OPTIMISTIC_OVERRIDE, STORE_OVERRIDE, STORE_VALUE, STORE_WRAP, createStore, createStoreProxy, getKeys, getOverlayLayer, getPropertyDescriptor, getStoreAffectsNodes, isWrappable, mergedOverlay, notifySelf, ownEnumerableKeys, setWriteOverride, storeLookup, storeSetter, storeTraps, symbolKeyedRecords, trackSelf, visibleNodeValue, witnessAffectsMark, wrap }; |
| import { isWrappable, ownEnumerableKeys } from "./store.js"; | ||
| const DELETE = Symbol(0); | ||
| function isPrototypePollutionKey(t) { | ||
| return t === "__proto__" || t === "constructor" || t === "prototype"; | ||
| } | ||
| function updatePath(t, e, o = 0) { | ||
| let r, n = t; | ||
| if (o < e.length - 1) { | ||
| r = e[o]; | ||
| const i = typeof r; | ||
| const f = Array.isArray(t); | ||
| if (i === "string" && isPrototypePollutionKey(r)) return; | ||
| if (Array.isArray(r)) { | ||
| for (let n = 0; n < r.length; n++) { | ||
| e[o] = r[n]; | ||
| updatePath(t, e, o); | ||
| } | ||
| e[o] = r; | ||
| return; | ||
| } else if (f && i === "function") { | ||
| for (let n = 0; n < t.length; n++) { | ||
| if (r(t[n], n)) { | ||
| e[o] = n; | ||
| updatePath(t, e, o); | ||
| } | ||
| } | ||
| e[o] = r; | ||
| return; | ||
| } else if (f && i === "object") { | ||
| const {from: n = 0, to: i = t.length - 1, by: f = 1} = r; | ||
| for (let r = n; r <= i; r += f) { | ||
| e[o] = r; | ||
| updatePath(t, e, o); | ||
| } | ||
| e[o] = r; | ||
| return; | ||
| } else if (o < e.length - 2) { | ||
| updatePath(t[r], e, o + 1); | ||
| return; | ||
| } | ||
| n = t[r]; | ||
| } | ||
| let i = e[e.length - 1]; | ||
| if (typeof i === "function") { | ||
| i = i(n); | ||
| if (i === n) return; | ||
| } | ||
| if (r === undefined && i == undefined) return; | ||
| if (i === DELETE) { | ||
| delete t[r]; | ||
| } else if (r === undefined || isWrappable(n) && isWrappable(i) && !Array.isArray(i)) { | ||
| const e = r !== undefined ? t[r] : t; | ||
| const o = ownEnumerableKeys(i); | ||
| for (let t = 0; t < o.length; t++) { | ||
| const r = o[t]; | ||
| if (typeof r === "string" && isPrototypePollutionKey(r)) continue; | ||
| const n = Object.getOwnPropertyDescriptor(i, r); | ||
| if (n.get || n.set) Object.defineProperty(e, r, n); else e[r] = n.value; | ||
| } | ||
| } else { | ||
| t[r] = i; | ||
| } | ||
| } | ||
| /** | ||
| * Path-based setter helper for `createStore`. Call `storePath(...path, value)` | ||
| * to produce a draft-mutating function suitable for passing to `setStore`. | ||
| * | ||
| * The canonical setter form in Solid 2.0 is the draft-mutating callback | ||
| * (`setStore(s => { s.user.name = "Ada"; })`). `storePath` is a backwards- | ||
| * compatibility helper for users porting from Solid 1.x's | ||
| * `setStore("user", "name", "Ada")` style — it's optional and you can mix the | ||
| * two styles freely. | ||
| * | ||
| * Path parts can be: | ||
| * - a single key — `"user"`, `0` | ||
| * - an array of keys — `[0, 1, 2]` | ||
| * - a range over an array — `{ from?, to?, by? }` | ||
| * - a filter `(item, index) => boolean` for arrays | ||
| * | ||
| * The final argument is the new value or an updater `(prev) => next`. Use | ||
| * `storePath.DELETE` to remove a property. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const [state, setState] = createStore({ user: { name: "Ada" }, todos: [] }); | ||
| * | ||
| * setState(storePath("user", "name", "Grace")); | ||
| * setState(storePath("todos", t => !t.done, "done", true)); // mark all undone as done | ||
| * setState(storePath("user", "nickname", storePath.DELETE)); | ||
| * ``` | ||
| */ const storePath = /* @__PURE__ */ Object.assign(function storePath(...t) { | ||
| return e => { | ||
| updatePath(e, t); | ||
| }; | ||
| }, { | ||
| DELETE: DELETE | ||
| }); | ||
| export { storePath }; |
| import { pendingCheckActive } from "../core/core.js"; | ||
| import { SUPPORTS_PROXY } from "../core/constants.js"; | ||
| import "../core/scheduler.js"; | ||
| import "../core/invariants.js"; | ||
| import "../core/verdict.js"; | ||
| import "../core/effect.js"; | ||
| import { createMemo } from "../signals.js"; | ||
| import { ownEnumerableKeys, $PROXY, isWrappable, $TARGET, trackSelf, witnessAffectsMark, mergedOverlay, STORE_VALUE, storeLookup, STORE_LOOKUP, $DELETED, wrap, getKeys, getPropertyDescriptor, $TRACK } from "./store.js"; | ||
| function snapshotImpl(e, t, r, n) { | ||
| let o, s, c, i, f, u; | ||
| if (!isWrappable(e)) return e; | ||
| if (r && r.has(e)) return r.get(e); | ||
| if (!r) r = new Map; | ||
| if (o = e[$TARGET] || n?.get(e)?.[$TARGET]) { | ||
| if (t) { | ||
| trackSelf(o, $TRACK); | ||
| // A tracked walk reads THROUGH the record without touching the proxy | ||
| // traps — witness the record's affects() channel like a trap read would. | ||
| if (pendingCheckActive) witnessAffectsMark(o); | ||
| } | ||
| c = mergedOverlay(o); | ||
| s = Array.isArray(o[STORE_VALUE]); | ||
| r.set(e, c ? i = s ? [] : Object.create(Object.getPrototypeOf(o[STORE_VALUE])) : o[STORE_VALUE]); | ||
| e = o[STORE_VALUE]; | ||
| n = o[STORE_LOOKUP] ?? storeLookup; | ||
| } else { | ||
| s = Array.isArray(e); | ||
| r.set(e, e); | ||
| } | ||
| if (s) { | ||
| const s = c?.length ?? e.length; | ||
| for (let p = 0; p < s; p++) { | ||
| u = c && p in c ? c[p] : e[p]; | ||
| if (u === $DELETED) continue; | ||
| if (t && isWrappable(u)) wrap(u, o); | ||
| if ((f = snapshotImpl(u, t, r, n)) !== u || i) { | ||
| if (!i) r.set(e, i = [ ...e ]); | ||
| i[p] = f; | ||
| } | ||
| } | ||
| // Deleted trailing slots are skipped above, so restore length to preserve | ||
| // holes instead of truncating the copy (#2846) — mirrors unwrapStoreValue. | ||
| if (i) i.length = s; | ||
| } else if (!c) { | ||
| // Specialized walk for the common no-overlay case (from #2756): the own | ||
| // descriptor gives the value directly, so each property is read once with | ||
| // no overlay membership checks. | ||
| const s = getKeys(e, undefined); | ||
| for (let c = 0, p = s.length; c < p; c++) { | ||
| const p = s[c]; | ||
| const l = Object.getOwnPropertyDescriptor(e, p); | ||
| if (l.get) continue; | ||
| u = l.value; | ||
| if (t && isWrappable(u)) wrap(u, o); | ||
| if ((f = snapshotImpl(u, t, r, n)) !== u || i) { | ||
| if (!i) { | ||
| i = Object.create(Object.getPrototypeOf(e)); | ||
| Object.assign(i, e); | ||
| } | ||
| i[p] = f; | ||
| } | ||
| } | ||
| } else { | ||
| const s = getKeys(e, c); | ||
| for (let p = 0, l = s.length; p < l; p++) { | ||
| let l = s[p]; | ||
| const a = getPropertyDescriptor(e, c, l); | ||
| if (a.get) continue; | ||
| u = l in c ? c[l] : e[l]; | ||
| if (t && isWrappable(u)) wrap(u, o); | ||
| if ((f = snapshotImpl(u, t, r, n)) !== e[l] || i) { | ||
| if (!i) { | ||
| i = Object.create(Object.getPrototypeOf(e)); | ||
| Object.assign(i, e); | ||
| } | ||
| i[l] = f; | ||
| } | ||
| } | ||
| } | ||
| return i || e; | ||
| } | ||
| function snapshot(e, t, r) { | ||
| return snapshotImpl(e, false, t, r); | ||
| } | ||
| /** | ||
| * Returns a plain (non-proxy) deep copy **and** subscribes the current | ||
| * tracking scope to every nested change in the source store. Any write | ||
| * anywhere in the subtree invalidates the consumer. | ||
| * | ||
| * Use this when you need plain data inside a reactive scope and want to | ||
| * react to deep mutations (e.g. passing a snapshot to `reconcile()` or to a | ||
| * memo that should rerun on any nested change). For most read paths, prefer | ||
| * direct property access — Solid stores already track per-property reads | ||
| * with no `deep()` wrapper needed. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const [state] = createStore({ a: { b: { c: 1 } } }); | ||
| * | ||
| * createEffect( | ||
| * () => deep(state), // reruns on any nested change | ||
| * plain => sendToWorker(plain) // worker gets a non-proxy copy | ||
| * ); | ||
| * ``` | ||
| */ function deep(e) { | ||
| return snapshotImpl(e, true); | ||
| } | ||
| function trueFn() { | ||
| return true; | ||
| } | ||
| const propTraps = { | ||
| get(e, t, r) { | ||
| if (t === $PROXY) return r; | ||
| return e.get(t); | ||
| }, | ||
| has(e, t) { | ||
| if (t === $PROXY) return true; | ||
| return e.has(t); | ||
| }, | ||
| set: trueFn, | ||
| deleteProperty: trueFn, | ||
| getOwnPropertyDescriptor(e, t) { | ||
| return { | ||
| configurable: true, | ||
| enumerable: true, | ||
| get() { | ||
| return e.get(t); | ||
| }, | ||
| set: trueFn, | ||
| deleteProperty: trueFn | ||
| }; | ||
| }, | ||
| ownKeys(e) { | ||
| return e.keys(); | ||
| } | ||
| }; | ||
| function resolveSource(e) { | ||
| return !(e = typeof e === "function" ? e() : e) ? {} : e; | ||
| } | ||
| const $SOURCES = Symbol(0); | ||
| /** | ||
| * Merges multiple props-like objects into a single proxy that *preserves | ||
| * reactivity*. Reads are forwarded to the right-most source that defines the | ||
| * property, so later sources override earlier ones (like `Object.assign`). | ||
| * | ||
| * Function arguments are treated as memo-backed sources — useful for passing | ||
| * derived defaults whose computation should track reactively. | ||
| * | ||
| * Use this in component bodies to merge defaults / overrides without losing | ||
| * Solid's per-property tracking. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * function Button(_props: { label: string; type?: string; disabled?: boolean }) { | ||
| * const props = merge({ type: "button", disabled: false }, _props); | ||
| * | ||
| * return <button type={props.type} disabled={props.disabled}>{props.label}</button>; | ||
| * } | ||
| * ``` | ||
| */ function merge(...e) { | ||
| if (e.length === 1 && typeof e[0] !== "function") return e[0]; | ||
| let t = false; | ||
| const r = []; | ||
| for (let n = 0; n < e.length; n++) { | ||
| const o = e[n]; | ||
| t = t || !!o && $PROXY in o; | ||
| const s = !!o && o[$SOURCES]; | ||
| if (s) { | ||
| for (let e = 0; e < s.length; e++) r.push(s[e]); | ||
| } else r.push(typeof o === "function" ? (t = true, createMemo(o)) : o); | ||
| } | ||
| if (SUPPORTS_PROXY && t) { | ||
| return new Proxy({ | ||
| get(e) { | ||
| if (e === $SOURCES) return r; | ||
| for (let t = r.length - 1; t >= 0; t--) { | ||
| const n = resolveSource(r[t]); | ||
| if (e in n) return n[e]; | ||
| } | ||
| }, | ||
| has(e) { | ||
| for (let t = r.length - 1; t >= 0; t--) { | ||
| if (e in resolveSource(r[t])) return true; | ||
| } | ||
| return false; | ||
| }, | ||
| keys() { | ||
| const e = new Set; | ||
| for (let t = 0; t < r.length; t++) { | ||
| const n = ownEnumerableKeys(resolveSource(r[t])); | ||
| for (let t = 0; t < n.length; t++) e.add(n[t]); | ||
| } | ||
| return [ ...e ]; | ||
| } | ||
| }, propTraps); | ||
| } | ||
| const n = Object.create(null); | ||
| let o = false; | ||
| let s = r.length - 1; | ||
| for (let e = s; e >= 0; e--) { | ||
| const t = r[e]; | ||
| if (!t) { | ||
| e === s && s--; | ||
| continue; | ||
| } | ||
| const c = Object.getOwnPropertyNames(t); | ||
| for (let r = c.length - 1; r >= 0; r--) { | ||
| const i = c[r]; | ||
| if (i === "__proto__" || i === "constructor") continue; | ||
| if (!n[i]) { | ||
| o = o || e !== s; | ||
| const r = Object.getOwnPropertyDescriptor(t, i); | ||
| n[i] = r.get ? { | ||
| enumerable: true, | ||
| configurable: true, | ||
| get: r.get.bind(t) | ||
| } : r; | ||
| } | ||
| } | ||
| } | ||
| if (!o) return r[s]; | ||
| const c = {}; | ||
| const i = Object.keys(n); | ||
| for (let e = i.length - 1; e >= 0; e--) { | ||
| const t = i[e], r = n[t]; | ||
| if (r.get) Object.defineProperty(c, t, r); else c[t] = r.value; | ||
| } | ||
| c[$SOURCES] = r; | ||
| return c; | ||
| } | ||
| /** | ||
| * Returns a reactive proxy of `props` with the listed keys hidden. Tracking | ||
| * on the remaining keys is preserved. | ||
| * | ||
| * Use it to forward "rest" props to a child element while pulling out the | ||
| * keys your component handles itself — the equivalent of `splitProps(p, ["a","b"])[1]`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * function Input(props: { label: string; value: string; onInput: (v: string) => void } & JSX.HTMLAttributes<HTMLInputElement>) { | ||
| * const rest = omit(props, "label", "value", "onInput"); | ||
| * | ||
| * return ( | ||
| * <label> | ||
| * {props.label} | ||
| * <input | ||
| * {...rest} | ||
| * value={props.value} | ||
| * onInput={e => props.onInput(e.currentTarget.value)} | ||
| * /> | ||
| * </label> | ||
| * ); | ||
| * } | ||
| * ``` | ||
| */ function omit(e, ...t) { | ||
| if (SUPPORTS_PROXY && $PROXY in e) { | ||
| return new Proxy({ | ||
| get(r) { | ||
| return t.includes(r) ? undefined : e[r]; | ||
| }, | ||
| has(r) { | ||
| return !t.includes(r) && r in e; | ||
| }, | ||
| keys() { | ||
| return ownEnumerableKeys(e).filter(e => !t.includes(e)); | ||
| } | ||
| }, propTraps); | ||
| } | ||
| const r = {}; | ||
| const n = Object.getOwnPropertyNames(e); | ||
| const o = t.length > 4 && n.length > t.length ? new Set(t) : undefined; | ||
| for (const s of n) { | ||
| if (o ? !o.has(s) : !t.includes(s)) { | ||
| const t = Object.getOwnPropertyDescriptor(e, s); | ||
| !t.get && !t.set && t.enumerable && t.writable && t.configurable ? r[s] = t.value : Object.defineProperty(r, s, t); | ||
| } | ||
| } | ||
| return r; | ||
| } | ||
| export { deep, merge, omit, snapshot }; |
| /** | ||
| * Installs the engine's hooks. Idempotent; called by every module that can | ||
| * create optimistic state (verdict.ts at module top level, createOptimistic | ||
| * and createOptimisticStore at first call) BEFORE any optimistic node exists. | ||
| */ | ||
| export declare function installOptimisticEngine(): void; |
| export declare function latest<T>(fn: () => T): T; | ||
| export declare function isPending(fn: () => any): boolean; |
| /** | ||
| * Installs the engine's hooks. Idempotent; called by every module that can | ||
| * create optimistic state (verdict.ts at module top level, createOptimistic | ||
| * and createOptimisticStore at first call) BEFORE any optimistic node exists. | ||
| */ | ||
| export declare function installOptimisticEngine(): void; |
| export declare function latest<T>(fn: () => T): T; | ||
| export declare function isPending(fn: () => any): boolean; |
@@ -0,3 +1,3 @@ | ||
| import type { Accessor } from "./signals.cjs"; | ||
| import { type Store } from "./store/store.cjs"; | ||
| import type { Accessor } from "./signals.cjs"; | ||
| /** | ||
@@ -4,0 +4,0 @@ * Declares that in-flight work will change the targeted data: the named |
@@ -23,3 +23,3 @@ import { Queue, type Computed, type Effect } from "./core/index.cjs"; | ||
| _forEachOwnedSlot(fn: (slot: RevealSlot) => boolean | void): boolean; | ||
| isReady(): boolean; | ||
| _isReady(): boolean; | ||
| /** | ||
@@ -32,6 +32,6 @@ * "Minimally ready" = this group has something visible to show under its own policy. | ||
| */ | ||
| isMinimallyReady(): boolean; | ||
| register(slot: RevealSlot): void; | ||
| unregister(slot: RevealSlot): void; | ||
| evaluate(disabledOverride?: boolean, collapsedOverride?: boolean): void; | ||
| _isMinimallyReady(): boolean; | ||
| _register(slot: RevealSlot): void; | ||
| _unregister(slot: RevealSlot): void; | ||
| _evaluate(disabledOverride?: boolean, collapsedOverride?: boolean): void; | ||
| } | ||
@@ -53,3 +53,3 @@ export declare class CollectionQueue extends Queue { | ||
| notify(node: Effect<any>, type: number, flags: number, error?: any): boolean; | ||
| checkSources(): void; | ||
| _checkSources(): void; | ||
| } | ||
@@ -56,0 +56,0 @@ /** |
| import { type OptimisticLane } from "./lanes.cjs"; | ||
| import type { Computed, Signal } from "./types.cjs"; | ||
| import type { Computed, Link } from "./types.cjs"; | ||
| export declare function addPendingSource(el: Computed<any>, source: Computed<any>): boolean; | ||
| export declare function setPendingError(el: Computed<any>, source?: Computed<any>, error?: any): void; | ||
| export declare function forEachDependent(el: Computed<any>, fn: (node: Computed<any>, link: Link) => void): void; | ||
| export declare function settlePendingSource(el: Computed<any>, source?: Computed<any>, snap?: boolean): void; | ||
@@ -8,38 +11,1 @@ export declare function isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T>; | ||
| export declare function notifyStatus(el: Computed<any>, status: number, error: any, blockStatus?: boolean, lane?: OptimisticLane): void; | ||
| /** | ||
| * The pending-source identity of a live `affects()` mark on `node` (lazy, | ||
| * one per node, shared by overlapping registrations via the refcount). | ||
| * | ||
| * A mark rides the SAME status rails as real in-flight async — downstream | ||
| * subscribers hold the sentinel in `_pendingSources` — but under its own | ||
| * identity so the two channels can't clear each other: | ||
| * - `_reask` is permanently `false`: a mark is by definition a declared | ||
| * value change, so `quietPending` never silences a window it participates | ||
| * in — even when the mark rides over an otherwise-quiet `refresh()` | ||
| * re-ask of the same node (the whole point of declaring one). | ||
| * - A landing on the marked node settles only the node's OWN source entry; | ||
| * the sentinel entry survives until the mark's transaction releases it. | ||
| * - The sentinel itself never carries `STATUS_PENDING`, so | ||
| * `transitionComplete` never counts a mark as a blocker of its own | ||
| * transaction (release happens AT settle — self-blocking would deadlock), | ||
| * and reads of the marked node never throw (marks are value-transparent | ||
| * at the source; pendingness is what propagates). | ||
| */ | ||
| export declare function getAffectsSentinel(node: Signal<any> | Computed<any>): Computed<any>; | ||
| /** | ||
| * Push a live mark's pendingness downstream from the marked node through the | ||
| * normal status rails. Runs on every registration (dedup in `notifyStatus` | ||
| * stops re-descent at already-covered subscribers). Subscribers that | ||
| * recompute mid-window shed this via `clearStatus` and re-acquire it through | ||
| * the read path (`applyAffectsReads`) — the same shape as real async, where | ||
| * the re-throw on read re-establishes the source. | ||
| */ | ||
| export declare function propagateAffectsMark(node: Signal<any> | Computed<any>): void; | ||
| /** | ||
| * Re-establish mark pendingness on a computed that read marked sources | ||
| * during its recompute (`clearStatus` at the top of the commit path wiped | ||
| * any sentinel entries it held). Called by `recompute` after the commit — | ||
| * not before, because setting `_error` earlier would make the commit path | ||
| * treat the node as errored and skip the value write. | ||
| */ | ||
| export declare function applyAffectsReads(el: Computed<any>, sources: (Signal<any> | Computed<any>)[]): void; |
@@ -8,2 +8,8 @@ import { type Refreshable } from "./constants.cjs"; | ||
| export declare let tracking: boolean; | ||
| /** @internal verdict-module glue */ | ||
| export declare function setPendingCheckActive(v: boolean): void; | ||
| /** @internal verdict-module glue */ | ||
| export declare function setLatestReadActive(v: boolean): void; | ||
| /** @internal verdict-module glue */ | ||
| export declare function setContextInternal(v: Owner | null): void; | ||
| export declare let stale: boolean; | ||
@@ -64,2 +70,8 @@ export declare let pendingCheckActive: boolean; | ||
| export declare function untrack<T>(fn: () => T, strictReadLabel?: string | false): T; | ||
| /** | ||
| * Bring a computed to a readable state: lazy/disposed nodes are (re)computed; | ||
| * an isPending() probe (`refresh`) additionally pulls the node fully up to | ||
| * date so its status flags reflect the current graph. | ||
| */ | ||
| export declare function prepareComputed(comp: Computed<unknown>, refresh: boolean): void; | ||
| export declare function read<T>(el: Signal<T> | Computed<T>): T; | ||
@@ -102,82 +114,4 @@ export declare function setSignal<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T)): T; | ||
| export declare function runWithOwner<T>(owner: Owner | null, fn: () => T): T; | ||
| /** | ||
| * Adds a node to the active isPending() probe without reading it. The store's | ||
| * untracked-probe fallback (`witnessAffectsMark`) calls this with `affects()` | ||
| * carrier nodes: an untracked read through a marked record may touch no real | ||
| * signal node at all, so the probe collects the mark's carrier directly. | ||
| * | ||
| * @internal | ||
| */ | ||
| export declare function witnessAffects(node: Signal<any> | Computed<any>): void; | ||
| /** | ||
| * Keep the lazily-created isPending()/latest() companion nodes in sync with a | ||
| * new value. Every path that produces a value for `el` — direct set, async | ||
| * resolution, transition-held sync recompute — must route through here so a | ||
| * new write path can't silently skip the companions (#2831). | ||
| */ | ||
| export declare function syncCompanions<T>(el: Signal<T> | Computed<T>, value: T): void; | ||
| /** | ||
| * Update _pendingSignal when pending state changes. When the override clears | ||
| * (pending -> not pending), merge the sub-lane into the source's lane so | ||
| * isPending effects are blocked until the full scope resolves. | ||
| */ | ||
| export declare function updatePendingSignal(el: Signal<any> | Computed<any>): void; | ||
| /** | ||
| * A firewall's status change re-derives the verdicts of its probed leaves: | ||
| * leaf companions consult the firewall (broad inheritance), so async | ||
| * starting/settling on the firewall must poke them or they keep a stale | ||
| * verdict forever (V4 stuck-companion class, #2838). | ||
| */ | ||
| export declare function updateChildCompanions(el: Computed<any>): void; | ||
| /** | ||
| * Settlement checkpoint (#2838): re-derive a node's companions directly from | ||
| * its committed state. Called when the transition machinery for the node is | ||
| * done with it — a pending commit or an optimistic revert. Verdicts are | ||
| * written committed (not through setSignal) because a transition-scoped | ||
| * override window opened here would itself need a settlement, re-scheduling | ||
| * forever while async is still in flight. This is what keeps companions | ||
| * coherent past transition completion: a verdict is a property of the data | ||
| * (A19), so it must survive the transition that happened to produce it. | ||
| */ | ||
| export declare function snapCompanionsToState(owner: Signal<any> | Computed<any>): void; | ||
| export declare function staleValues<T>(fn: () => T, set?: boolean): T; | ||
| /** | ||
| * Reads reactive expressions while bypassing any pending async overlay — i.e. | ||
| * always returns the most-recently-committed value, even when newer reads | ||
| * inside `fn` are still in flight. | ||
| * | ||
| * Useful inside a `<Loading>` boundary's children when you want to keep | ||
| * showing the previous resolved data instead of the fallback while the next | ||
| * value loads. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Loading fallback={<Skeleton />}> | ||
| * {/* During a transition, render the previous user instead of skeleton: *\/} | ||
| * <UserCard user={latest(() => user())} /> | ||
| * </Loading> | ||
| * ``` | ||
| */ | ||
| export declare function latest<T>(fn: () => T): T; | ||
| /** | ||
| * Returns `true` if any reactive read inside `fn` is showing a stale value | ||
| * while newer async work is pending. Does not subscribe — pair with a tracked | ||
| * memo if you want to react to pending status changes. | ||
| * | ||
| * Useful for showing inline transition indicators alongside the previous | ||
| * value (rather than swapping to a `<Loading>` fallback). | ||
| * Because `fn` is read normally, `isPending` participates in Loading/SSR | ||
| * readiness the same way the read itself would. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const pending = createMemo(() => isPending(() => user())); | ||
| * | ||
| * <button disabled={pending()}>{pending() ? "Saving…" : "Save"}</button> | ||
| * | ||
| * <button disabled={isPending(() => user())}>Save</button> | ||
| * ``` | ||
| */ | ||
| export declare function isPending(fn: () => any): boolean; | ||
| /** | ||
| * Invalidates one reactive source, forcing it to re-execute even if its inputs | ||
@@ -184,0 +118,0 @@ * haven't changed. |
@@ -14,33 +14,3 @@ export type ExternalSourceFactory = (fn: (prev: any) => any, trigger: () => void) => ExternalSource; | ||
| } | null; | ||
| /** | ||
| * Registers a factory that bridges external reactive systems (e.g. MobX, Vue refs) | ||
| * into Solid's tracking graph. Every computation will be wrapped so that the | ||
| * external library can track its own dependencies alongside Solid's. | ||
| * | ||
| * Multiple calls pipe together: each new factory wraps the previous one. | ||
| * | ||
| * @param config.factory receives `(fn, trigger)` — wrap fn execution in external tracking, | ||
| * call trigger when external deps change. Return `{ track, dispose }`. | ||
| * @param config.untrack optional wrapper for `untrack` — disables external tracking too. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Bridge an external "subscribe / notify" library into Solid's graph. | ||
| * // `factory` wraps every Solid compute so the external library can attach | ||
| * // its own dependency tracker; `trigger` re-runs the compute on external | ||
| * // change. `untrack` mirrors Solid's `untrack()` into the external library | ||
| * // so that reads inside `untrack(...)` don't get tracked twice. | ||
| * enableExternalSource({ | ||
| * factory: (compute, trigger) => { | ||
| * const sub = externalLib.subscribe(trigger); | ||
| * return { | ||
| * track: prev => externalLib.run(() => compute(prev)), | ||
| * dispose: () => sub.unsubscribe() | ||
| * }; | ||
| * }, | ||
| * untrack: fn => externalLib.untracked(fn) | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export declare function enableExternalSource(config: ExternalSourceConfig): void; | ||
| export declare function _resetExternalSourceConfig(): void; |
| import type { Computed } from "./types.cjs"; | ||
| /** The queue a node belongs to, picked from its own zombie flag. */ | ||
| export declare function queueFor(n: Computed<any>): Heap; | ||
| /** | ||
| * Schedule one subscriber to re-run on the next flush: tracked effects bypass | ||
| * the heap and go directly to their effect queue; everything else is inserted | ||
| * into its own (zombie-flag-routed) heap with the `_min` cursor pulled down. | ||
| */ | ||
| export declare function enqueueSub(node: Computed<any>): void; | ||
| export interface Heap { | ||
@@ -3,0 +11,0 @@ _heap: (Computed<unknown> | undefined)[]; |
| export { ContextNotFoundError, NoOwnerError, NotReadyError } from "./error.cjs"; | ||
| export { isEqual, untrack, runWithOwner, computed, signal, read, setSignal, setMemo, suppressComputedRecompute, optimisticSignal, optimisticComputed, isPending, latest, refresh, staleValues, setSnapshotCapture, markSnapshotScope, releaseSnapshotScope, clearSnapshots } from "./core.cjs"; | ||
| export { isEqual, untrack, runWithOwner, computed, signal, read, setSignal, setMemo, suppressComputedRecompute, optimisticSignal, optimisticComputed, refresh, staleValues, setSnapshotCapture, markSnapshotScope, releaseSnapshotScope, clearSnapshots } from "./core.cjs"; | ||
| export { enableExternalSource, _resetExternalSourceConfig, type ExternalSourceFactory, type ExternalSource, type ExternalSourceConfig } from "./external.cjs"; | ||
@@ -7,7 +7,8 @@ export { createOwner, createRoot, dispose, getNextChildId, getObserver, getOwner, isDisposed, cleanup, peekNextChildId } from "./owner.cjs"; | ||
| export { handleAsync } from "./async.cjs"; | ||
| export { isPending, latest } from "./verdict.cjs"; | ||
| export type { Computed, Disposable, FirewallSignal, Link, Owner, Root, Signal, NodeOptions } from "./types.cjs"; | ||
| export { effect, trackedEffect, type Effect, type TrackedEffect } from "./effect.cjs"; | ||
| export { action } from "./action.cjs"; | ||
| export { flush, Queue, GlobalQueue, trackOptimisticStore, enforceLoadingBoundary, resetErrorHalt, type IQueue, type QueueCallback } from "./scheduler.cjs"; | ||
| export { flush, Queue, GlobalQueue, enforceLoadingBoundary, resetErrorHalt, type IQueue, type QueueCallback } from "./scheduler.cjs"; | ||
| export { DEV, type Dev, type DevHooks, type DiagnosticCapture, type DiagnosticCode, type DiagnosticEvent, type DiagnosticKind, type Diagnostics, type DiagnosticSeverity } from "./dev.cjs"; | ||
| export * from "./constants.cjs"; |
@@ -13,2 +13,10 @@ import type { Computed, Disposable, Owner, Root } from "./types.cjs"; | ||
| /** | ||
| * The id a freshly-created node inherits: an explicit `options.id` wins; | ||
| * transparent nodes share their parent's id; otherwise the parent's next | ||
| * child id is consumed (or `undefined` outside an id-carrying tree). | ||
| */ | ||
| export declare function inheritId(options: { | ||
| id?: string; | ||
| } | undefined, transparent: boolean, parent: Owner | null | undefined): string | undefined; | ||
| /** | ||
| * Returns the *next* child id for `owner` without consuming it. Used by | ||
@@ -15,0 +23,0 @@ * hydration plumbing to peek at the id a future child will receive. |
| import { type Heap } from "./heap.cjs"; | ||
| import { activeLanes, assignOrMergeLane, findLane } from "./lanes.cjs"; | ||
| import { activeLanes, assignOrMergeLane, findLane, type OptimisticLane } from "./lanes.cjs"; | ||
| import type { Computed, Signal } from "./types.cjs"; | ||
@@ -23,3 +23,2 @@ export { activeLanes, assignOrMergeLane, findLane }; | ||
| export declare function enforceLoadingBoundary(enabled: boolean): void; | ||
| export declare function shouldReadStashedOptimisticValue(node: Signal<any>): boolean; | ||
| export declare function setProjectionWriteActive(value: boolean): void; | ||
@@ -51,3 +50,3 @@ export declare function setTrackedQueueCallback(value: boolean): void; | ||
| */ | ||
| export declare function haltReactivity(): void; | ||
| export declare function haltReactivity(cause?: unknown): void; | ||
| /** @internal Test/dev-reload hook. Revives scheduling after a halt. */ | ||
@@ -89,6 +88,36 @@ export declare function resetErrorHalt(): void; | ||
| static _runEffect: (el: Computed<unknown>) => void; | ||
| static _clearOptimisticStore: ((store: any) => void) | null; | ||
| static _clearOptimisticStores: ((stores: Set<any>) => void) | null; | ||
| static _releaseAffectsScope: ((node: OptimisticNode) => void) | null; | ||
| static _propagateAffects: ((node: OptimisticNode) => void) | null; | ||
| static _settleAffects: ((node: OptimisticNode) => void) | null; | ||
| static _applyAffectsReads: ((el: Computed<any>, sources: (Signal<any> | Computed<any>)[]) => void) | null; | ||
| static _releaseAffectsMarks: ((nodes: OptimisticNode[]) => void) | null; | ||
| static _markAffects: ((node: OptimisticNode) => void) | null; | ||
| static _releaseAffectsMark: ((node: OptimisticNode) => void) | null; | ||
| static _onlyMarkPending: ((el: Computed<any>) => boolean) | null; | ||
| static _collectMarkSources: ((el: Computed<any>, into: OptimisticNode[]) => void) | null; | ||
| static _wireExternalSource: ((self: Computed<any>) => void) | null; | ||
| static _externalUntrack: (<T>(fn: () => T) => T) | null; | ||
| static _syncCompanions: (<T>(el: Signal<T> | Computed<T>, value: T) => void) | null; | ||
| static _updatePendingSignal: ((el: OptimisticNode) => void) | null; | ||
| static _updateChildCompanions: ((el: Computed<any>) => void) | null; | ||
| static _snapCompanions: ((el: OptimisticNode) => void) | null; | ||
| static _latestRead: (<T>(el: Signal<T> | Computed<T>) => T) | null; | ||
| static _pendingCheck: ((el: OptimisticNode, c: Computed<any> | null, owner: OptimisticNode, firewall: Computed<any> | null) => void) | null; | ||
| static _recordFresh: ((el: OptimisticNode, value: any) => void) | null; | ||
| static _applyReask: ((el: Computed<any>, hadReask: boolean) => boolean) | null; | ||
| static _repollVerdicts: ((el: Computed<any>) => void) | null; | ||
| static _witnessAffects: ((node: OptimisticNode) => void) | null; | ||
| static _optimisticWrite: (<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T)) => T) | null; | ||
| static _resolveOptimistic: ((nodes: OptimisticNode[]) => void) | null; | ||
| static _stashOptimistic: ((stashedTransition: Transition) => void) | null; | ||
| static _transitionBlocked: ((transition: Transition) => boolean) | null; | ||
| static _cleanupLanes: ((completingTransition: Transition | null) => void) | null; | ||
| static _runLaneEffects: ((type: number) => void) | null; | ||
| static _readStashed: ((el: Signal<any>) => boolean) | null; | ||
| static _gatedRead: ((el: Signal<any>, owner: OptimisticNode, c: Computed<any>) => boolean) | null; | ||
| static _laneSuspends: ((owner: OptimisticNode) => boolean) | null; | ||
| static _laneReadsCommitted: ((el: OptimisticNode, owner: OptimisticNode, c: Computed<any>) => boolean) | null; | ||
| static _recomputeLane: ((el: Computed<any>, own: boolean) => OptimisticLane | null) | null; | ||
| static _laneAsyncPending: ((el: Computed<any>) => void) | null; | ||
| static _laneAsyncSettled: ((el: Computed<any>) => void) | null; | ||
| static _trackOptimisticStore: ((store: any) => void) | null; | ||
| flush(): void; | ||
@@ -102,3 +131,2 @@ notify(node: Computed<any>, mask: number, flags: number, error?: any): boolean; | ||
| export declare function finalizePureQueue(completingTransition?: Transition | null, incomplete?: boolean): void; | ||
| export declare function trackOptimisticStore(store: any): void; | ||
| /** | ||
@@ -111,32 +139,9 @@ * Count of live `affects()` registrations across the system (including | ||
| /** | ||
| * The counting half of a mark, shared by direct registration and store-scope | ||
| * inheritance (a node created inside a live keyless mark's identity scope): | ||
| * bumps the refcount and pokes the node's verdict companions so an | ||
| * already-materialized `false` flips reactively. | ||
| * Counter mutation seam for the mark engine in affects.ts: an imported `let` | ||
| * binding is read-only, and the read-path gate above must stay a plain module | ||
| * variable so `read()` pays one integer compare, not a function call. | ||
| * | ||
| * @internal | ||
| */ | ||
| export declare function markAffects(node: OptimisticNode): void; | ||
| /** | ||
| * Registers one `affects()` mark on a node: counts it, records the | ||
| * registration with the current transaction (after initTransition the queue's | ||
| * array aliases the active transition's, mirroring `_optimisticNodes`), and | ||
| * propagates STATUS_PENDING downstream on the status rails so everything | ||
| * DERIVED from the marked data reads pending too. Propagation runs on every | ||
| * registration (not just the first): subscribers gained since an earlier | ||
| * overlapping registration get covered, and dedup stops re-descent early. | ||
| * | ||
| * @internal | ||
| */ | ||
| export declare function registerAffectsMark(node: OptimisticNode): void; | ||
| /** | ||
| * Releases one registration. When the node's last mark drops, settles the | ||
| * mark's sentinel out of every downstream `_pendingSources` (waking blocked | ||
| * nodes and re-deriving verdicts along the walk). Companion writes go through | ||
| * the settlement snap (committed, not transition-scoped) so releasing a mark | ||
| * can't open a fresh override window that would itself need settlement. | ||
| * | ||
| * @internal | ||
| */ | ||
| export declare function releaseAffectsMark(node: OptimisticNode): void; | ||
| export declare function shiftAffectsMarks(delta: 1 | -1): void; | ||
| export declare const globalQueue: GlobalQueue; | ||
@@ -143,0 +148,0 @@ /** |
@@ -66,2 +66,1 @@ import { type Computed, type Refreshable } from "../core/index.cjs"; | ||
| export declare function createWriteTraps(isActive?: () => boolean, onDraftWrite?: () => void): ProxyHandler<any>; | ||
| export declare const writeTraps: ProxyHandler<any>; |
@@ -0,3 +1,3 @@ | ||
| import type { Accessor } from "./signals.js"; | ||
| import { type Store } from "./store/store.js"; | ||
| import type { Accessor } from "./signals.js"; | ||
| /** | ||
@@ -4,0 +4,0 @@ * Declares that in-flight work will change the targeted data: the named |
@@ -23,3 +23,3 @@ import { Queue, type Computed, type Effect } from "./core/index.js"; | ||
| _forEachOwnedSlot(fn: (slot: RevealSlot) => boolean | void): boolean; | ||
| isReady(): boolean; | ||
| _isReady(): boolean; | ||
| /** | ||
@@ -32,6 +32,6 @@ * "Minimally ready" = this group has something visible to show under its own policy. | ||
| */ | ||
| isMinimallyReady(): boolean; | ||
| register(slot: RevealSlot): void; | ||
| unregister(slot: RevealSlot): void; | ||
| evaluate(disabledOverride?: boolean, collapsedOverride?: boolean): void; | ||
| _isMinimallyReady(): boolean; | ||
| _register(slot: RevealSlot): void; | ||
| _unregister(slot: RevealSlot): void; | ||
| _evaluate(disabledOverride?: boolean, collapsedOverride?: boolean): void; | ||
| } | ||
@@ -53,3 +53,3 @@ export declare class CollectionQueue extends Queue { | ||
| notify(node: Effect<any>, type: number, flags: number, error?: any): boolean; | ||
| checkSources(): void; | ||
| _checkSources(): void; | ||
| } | ||
@@ -56,0 +56,0 @@ /** |
| import { type OptimisticLane } from "./lanes.js"; | ||
| import type { Computed, Signal } from "./types.js"; | ||
| import type { Computed, Link } from "./types.js"; | ||
| export declare function addPendingSource(el: Computed<any>, source: Computed<any>): boolean; | ||
| export declare function setPendingError(el: Computed<any>, source?: Computed<any>, error?: any): void; | ||
| export declare function forEachDependent(el: Computed<any>, fn: (node: Computed<any>, link: Link) => void): void; | ||
| export declare function settlePendingSource(el: Computed<any>, source?: Computed<any>, snap?: boolean): void; | ||
@@ -8,38 +11,1 @@ export declare function isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T>; | ||
| export declare function notifyStatus(el: Computed<any>, status: number, error: any, blockStatus?: boolean, lane?: OptimisticLane): void; | ||
| /** | ||
| * The pending-source identity of a live `affects()` mark on `node` (lazy, | ||
| * one per node, shared by overlapping registrations via the refcount). | ||
| * | ||
| * A mark rides the SAME status rails as real in-flight async — downstream | ||
| * subscribers hold the sentinel in `_pendingSources` — but under its own | ||
| * identity so the two channels can't clear each other: | ||
| * - `_reask` is permanently `false`: a mark is by definition a declared | ||
| * value change, so `quietPending` never silences a window it participates | ||
| * in — even when the mark rides over an otherwise-quiet `refresh()` | ||
| * re-ask of the same node (the whole point of declaring one). | ||
| * - A landing on the marked node settles only the node's OWN source entry; | ||
| * the sentinel entry survives until the mark's transaction releases it. | ||
| * - The sentinel itself never carries `STATUS_PENDING`, so | ||
| * `transitionComplete` never counts a mark as a blocker of its own | ||
| * transaction (release happens AT settle — self-blocking would deadlock), | ||
| * and reads of the marked node never throw (marks are value-transparent | ||
| * at the source; pendingness is what propagates). | ||
| */ | ||
| export declare function getAffectsSentinel(node: Signal<any> | Computed<any>): Computed<any>; | ||
| /** | ||
| * Push a live mark's pendingness downstream from the marked node through the | ||
| * normal status rails. Runs on every registration (dedup in `notifyStatus` | ||
| * stops re-descent at already-covered subscribers). Subscribers that | ||
| * recompute mid-window shed this via `clearStatus` and re-acquire it through | ||
| * the read path (`applyAffectsReads`) — the same shape as real async, where | ||
| * the re-throw on read re-establishes the source. | ||
| */ | ||
| export declare function propagateAffectsMark(node: Signal<any> | Computed<any>): void; | ||
| /** | ||
| * Re-establish mark pendingness on a computed that read marked sources | ||
| * during its recompute (`clearStatus` at the top of the commit path wiped | ||
| * any sentinel entries it held). Called by `recompute` after the commit — | ||
| * not before, because setting `_error` earlier would make the commit path | ||
| * treat the node as errored and skip the value write. | ||
| */ | ||
| export declare function applyAffectsReads(el: Computed<any>, sources: (Signal<any> | Computed<any>)[]): void; |
@@ -8,2 +8,8 @@ import { type Refreshable } from "./constants.js"; | ||
| export declare let tracking: boolean; | ||
| /** @internal verdict-module glue */ | ||
| export declare function setPendingCheckActive(v: boolean): void; | ||
| /** @internal verdict-module glue */ | ||
| export declare function setLatestReadActive(v: boolean): void; | ||
| /** @internal verdict-module glue */ | ||
| export declare function setContextInternal(v: Owner | null): void; | ||
| export declare let stale: boolean; | ||
@@ -64,2 +70,8 @@ export declare let pendingCheckActive: boolean; | ||
| export declare function untrack<T>(fn: () => T, strictReadLabel?: string | false): T; | ||
| /** | ||
| * Bring a computed to a readable state: lazy/disposed nodes are (re)computed; | ||
| * an isPending() probe (`refresh`) additionally pulls the node fully up to | ||
| * date so its status flags reflect the current graph. | ||
| */ | ||
| export declare function prepareComputed(comp: Computed<unknown>, refresh: boolean): void; | ||
| export declare function read<T>(el: Signal<T> | Computed<T>): T; | ||
@@ -102,82 +114,4 @@ export declare function setSignal<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T)): T; | ||
| export declare function runWithOwner<T>(owner: Owner | null, fn: () => T): T; | ||
| /** | ||
| * Adds a node to the active isPending() probe without reading it. The store's | ||
| * untracked-probe fallback (`witnessAffectsMark`) calls this with `affects()` | ||
| * carrier nodes: an untracked read through a marked record may touch no real | ||
| * signal node at all, so the probe collects the mark's carrier directly. | ||
| * | ||
| * @internal | ||
| */ | ||
| export declare function witnessAffects(node: Signal<any> | Computed<any>): void; | ||
| /** | ||
| * Keep the lazily-created isPending()/latest() companion nodes in sync with a | ||
| * new value. Every path that produces a value for `el` — direct set, async | ||
| * resolution, transition-held sync recompute — must route through here so a | ||
| * new write path can't silently skip the companions (#2831). | ||
| */ | ||
| export declare function syncCompanions<T>(el: Signal<T> | Computed<T>, value: T): void; | ||
| /** | ||
| * Update _pendingSignal when pending state changes. When the override clears | ||
| * (pending -> not pending), merge the sub-lane into the source's lane so | ||
| * isPending effects are blocked until the full scope resolves. | ||
| */ | ||
| export declare function updatePendingSignal(el: Signal<any> | Computed<any>): void; | ||
| /** | ||
| * A firewall's status change re-derives the verdicts of its probed leaves: | ||
| * leaf companions consult the firewall (broad inheritance), so async | ||
| * starting/settling on the firewall must poke them or they keep a stale | ||
| * verdict forever (V4 stuck-companion class, #2838). | ||
| */ | ||
| export declare function updateChildCompanions(el: Computed<any>): void; | ||
| /** | ||
| * Settlement checkpoint (#2838): re-derive a node's companions directly from | ||
| * its committed state. Called when the transition machinery for the node is | ||
| * done with it — a pending commit or an optimistic revert. Verdicts are | ||
| * written committed (not through setSignal) because a transition-scoped | ||
| * override window opened here would itself need a settlement, re-scheduling | ||
| * forever while async is still in flight. This is what keeps companions | ||
| * coherent past transition completion: a verdict is a property of the data | ||
| * (A19), so it must survive the transition that happened to produce it. | ||
| */ | ||
| export declare function snapCompanionsToState(owner: Signal<any> | Computed<any>): void; | ||
| export declare function staleValues<T>(fn: () => T, set?: boolean): T; | ||
| /** | ||
| * Reads reactive expressions while bypassing any pending async overlay — i.e. | ||
| * always returns the most-recently-committed value, even when newer reads | ||
| * inside `fn` are still in flight. | ||
| * | ||
| * Useful inside a `<Loading>` boundary's children when you want to keep | ||
| * showing the previous resolved data instead of the fallback while the next | ||
| * value loads. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Loading fallback={<Skeleton />}> | ||
| * {/* During a transition, render the previous user instead of skeleton: *\/} | ||
| * <UserCard user={latest(() => user())} /> | ||
| * </Loading> | ||
| * ``` | ||
| */ | ||
| export declare function latest<T>(fn: () => T): T; | ||
| /** | ||
| * Returns `true` if any reactive read inside `fn` is showing a stale value | ||
| * while newer async work is pending. Does not subscribe — pair with a tracked | ||
| * memo if you want to react to pending status changes. | ||
| * | ||
| * Useful for showing inline transition indicators alongside the previous | ||
| * value (rather than swapping to a `<Loading>` fallback). | ||
| * Because `fn` is read normally, `isPending` participates in Loading/SSR | ||
| * readiness the same way the read itself would. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const pending = createMemo(() => isPending(() => user())); | ||
| * | ||
| * <button disabled={pending()}>{pending() ? "Saving…" : "Save"}</button> | ||
| * | ||
| * <button disabled={isPending(() => user())}>Save</button> | ||
| * ``` | ||
| */ | ||
| export declare function isPending(fn: () => any): boolean; | ||
| /** | ||
| * Invalidates one reactive source, forcing it to re-execute even if its inputs | ||
@@ -184,0 +118,0 @@ * haven't changed. |
@@ -14,33 +14,3 @@ export type ExternalSourceFactory = (fn: (prev: any) => any, trigger: () => void) => ExternalSource; | ||
| } | null; | ||
| /** | ||
| * Registers a factory that bridges external reactive systems (e.g. MobX, Vue refs) | ||
| * into Solid's tracking graph. Every computation will be wrapped so that the | ||
| * external library can track its own dependencies alongside Solid's. | ||
| * | ||
| * Multiple calls pipe together: each new factory wraps the previous one. | ||
| * | ||
| * @param config.factory receives `(fn, trigger)` — wrap fn execution in external tracking, | ||
| * call trigger when external deps change. Return `{ track, dispose }`. | ||
| * @param config.untrack optional wrapper for `untrack` — disables external tracking too. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Bridge an external "subscribe / notify" library into Solid's graph. | ||
| * // `factory` wraps every Solid compute so the external library can attach | ||
| * // its own dependency tracker; `trigger` re-runs the compute on external | ||
| * // change. `untrack` mirrors Solid's `untrack()` into the external library | ||
| * // so that reads inside `untrack(...)` don't get tracked twice. | ||
| * enableExternalSource({ | ||
| * factory: (compute, trigger) => { | ||
| * const sub = externalLib.subscribe(trigger); | ||
| * return { | ||
| * track: prev => externalLib.run(() => compute(prev)), | ||
| * dispose: () => sub.unsubscribe() | ||
| * }; | ||
| * }, | ||
| * untrack: fn => externalLib.untracked(fn) | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export declare function enableExternalSource(config: ExternalSourceConfig): void; | ||
| export declare function _resetExternalSourceConfig(): void; |
| import type { Computed } from "./types.js"; | ||
| /** The queue a node belongs to, picked from its own zombie flag. */ | ||
| export declare function queueFor(n: Computed<any>): Heap; | ||
| /** | ||
| * Schedule one subscriber to re-run on the next flush: tracked effects bypass | ||
| * the heap and go directly to their effect queue; everything else is inserted | ||
| * into its own (zombie-flag-routed) heap with the `_min` cursor pulled down. | ||
| */ | ||
| export declare function enqueueSub(node: Computed<any>): void; | ||
| export interface Heap { | ||
@@ -3,0 +11,0 @@ _heap: (Computed<unknown> | undefined)[]; |
| export { ContextNotFoundError, NoOwnerError, NotReadyError } from "./error.js"; | ||
| export { isEqual, untrack, runWithOwner, computed, signal, read, setSignal, setMemo, suppressComputedRecompute, optimisticSignal, optimisticComputed, isPending, latest, refresh, staleValues, setSnapshotCapture, markSnapshotScope, releaseSnapshotScope, clearSnapshots } from "./core.js"; | ||
| export { isEqual, untrack, runWithOwner, computed, signal, read, setSignal, setMemo, suppressComputedRecompute, optimisticSignal, optimisticComputed, refresh, staleValues, setSnapshotCapture, markSnapshotScope, releaseSnapshotScope, clearSnapshots } from "./core.js"; | ||
| export { enableExternalSource, _resetExternalSourceConfig, type ExternalSourceFactory, type ExternalSource, type ExternalSourceConfig } from "./external.js"; | ||
@@ -7,7 +7,8 @@ export { createOwner, createRoot, dispose, getNextChildId, getObserver, getOwner, isDisposed, cleanup, peekNextChildId } from "./owner.js"; | ||
| export { handleAsync } from "./async.js"; | ||
| export { isPending, latest } from "./verdict.js"; | ||
| export type { Computed, Disposable, FirewallSignal, Link, Owner, Root, Signal, NodeOptions } from "./types.js"; | ||
| export { effect, trackedEffect, type Effect, type TrackedEffect } from "./effect.js"; | ||
| export { action } from "./action.js"; | ||
| export { flush, Queue, GlobalQueue, trackOptimisticStore, enforceLoadingBoundary, resetErrorHalt, type IQueue, type QueueCallback } from "./scheduler.js"; | ||
| export { flush, Queue, GlobalQueue, enforceLoadingBoundary, resetErrorHalt, type IQueue, type QueueCallback } from "./scheduler.js"; | ||
| export { DEV, type Dev, type DevHooks, type DiagnosticCapture, type DiagnosticCode, type DiagnosticEvent, type DiagnosticKind, type Diagnostics, type DiagnosticSeverity } from "./dev.js"; | ||
| export * from "./constants.js"; |
@@ -13,2 +13,10 @@ import type { Computed, Disposable, Owner, Root } from "./types.js"; | ||
| /** | ||
| * The id a freshly-created node inherits: an explicit `options.id` wins; | ||
| * transparent nodes share their parent's id; otherwise the parent's next | ||
| * child id is consumed (or `undefined` outside an id-carrying tree). | ||
| */ | ||
| export declare function inheritId(options: { | ||
| id?: string; | ||
| } | undefined, transparent: boolean, parent: Owner | null | undefined): string | undefined; | ||
| /** | ||
| * Returns the *next* child id for `owner` without consuming it. Used by | ||
@@ -15,0 +23,0 @@ * hydration plumbing to peek at the id a future child will receive. |
| import { type Heap } from "./heap.js"; | ||
| import { activeLanes, assignOrMergeLane, findLane } from "./lanes.js"; | ||
| import { activeLanes, assignOrMergeLane, findLane, type OptimisticLane } from "./lanes.js"; | ||
| import type { Computed, Signal } from "./types.js"; | ||
@@ -23,3 +23,2 @@ export { activeLanes, assignOrMergeLane, findLane }; | ||
| export declare function enforceLoadingBoundary(enabled: boolean): void; | ||
| export declare function shouldReadStashedOptimisticValue(node: Signal<any>): boolean; | ||
| export declare function setProjectionWriteActive(value: boolean): void; | ||
@@ -51,3 +50,3 @@ export declare function setTrackedQueueCallback(value: boolean): void; | ||
| */ | ||
| export declare function haltReactivity(): void; | ||
| export declare function haltReactivity(cause?: unknown): void; | ||
| /** @internal Test/dev-reload hook. Revives scheduling after a halt. */ | ||
@@ -89,6 +88,36 @@ export declare function resetErrorHalt(): void; | ||
| static _runEffect: (el: Computed<unknown>) => void; | ||
| static _clearOptimisticStore: ((store: any) => void) | null; | ||
| static _clearOptimisticStores: ((stores: Set<any>) => void) | null; | ||
| static _releaseAffectsScope: ((node: OptimisticNode) => void) | null; | ||
| static _propagateAffects: ((node: OptimisticNode) => void) | null; | ||
| static _settleAffects: ((node: OptimisticNode) => void) | null; | ||
| static _applyAffectsReads: ((el: Computed<any>, sources: (Signal<any> | Computed<any>)[]) => void) | null; | ||
| static _releaseAffectsMarks: ((nodes: OptimisticNode[]) => void) | null; | ||
| static _markAffects: ((node: OptimisticNode) => void) | null; | ||
| static _releaseAffectsMark: ((node: OptimisticNode) => void) | null; | ||
| static _onlyMarkPending: ((el: Computed<any>) => boolean) | null; | ||
| static _collectMarkSources: ((el: Computed<any>, into: OptimisticNode[]) => void) | null; | ||
| static _wireExternalSource: ((self: Computed<any>) => void) | null; | ||
| static _externalUntrack: (<T>(fn: () => T) => T) | null; | ||
| static _syncCompanions: (<T>(el: Signal<T> | Computed<T>, value: T) => void) | null; | ||
| static _updatePendingSignal: ((el: OptimisticNode) => void) | null; | ||
| static _updateChildCompanions: ((el: Computed<any>) => void) | null; | ||
| static _snapCompanions: ((el: OptimisticNode) => void) | null; | ||
| static _latestRead: (<T>(el: Signal<T> | Computed<T>) => T) | null; | ||
| static _pendingCheck: ((el: OptimisticNode, c: Computed<any> | null, owner: OptimisticNode, firewall: Computed<any> | null) => void) | null; | ||
| static _recordFresh: ((el: OptimisticNode, value: any) => void) | null; | ||
| static _applyReask: ((el: Computed<any>, hadReask: boolean) => boolean) | null; | ||
| static _repollVerdicts: ((el: Computed<any>) => void) | null; | ||
| static _witnessAffects: ((node: OptimisticNode) => void) | null; | ||
| static _optimisticWrite: (<T>(el: Signal<T> | Computed<T>, v: T | ((prev: T) => T)) => T) | null; | ||
| static _resolveOptimistic: ((nodes: OptimisticNode[]) => void) | null; | ||
| static _stashOptimistic: ((stashedTransition: Transition) => void) | null; | ||
| static _transitionBlocked: ((transition: Transition) => boolean) | null; | ||
| static _cleanupLanes: ((completingTransition: Transition | null) => void) | null; | ||
| static _runLaneEffects: ((type: number) => void) | null; | ||
| static _readStashed: ((el: Signal<any>) => boolean) | null; | ||
| static _gatedRead: ((el: Signal<any>, owner: OptimisticNode, c: Computed<any>) => boolean) | null; | ||
| static _laneSuspends: ((owner: OptimisticNode) => boolean) | null; | ||
| static _laneReadsCommitted: ((el: OptimisticNode, owner: OptimisticNode, c: Computed<any>) => boolean) | null; | ||
| static _recomputeLane: ((el: Computed<any>, own: boolean) => OptimisticLane | null) | null; | ||
| static _laneAsyncPending: ((el: Computed<any>) => void) | null; | ||
| static _laneAsyncSettled: ((el: Computed<any>) => void) | null; | ||
| static _trackOptimisticStore: ((store: any) => void) | null; | ||
| flush(): void; | ||
@@ -102,3 +131,2 @@ notify(node: Computed<any>, mask: number, flags: number, error?: any): boolean; | ||
| export declare function finalizePureQueue(completingTransition?: Transition | null, incomplete?: boolean): void; | ||
| export declare function trackOptimisticStore(store: any): void; | ||
| /** | ||
@@ -111,32 +139,9 @@ * Count of live `affects()` registrations across the system (including | ||
| /** | ||
| * The counting half of a mark, shared by direct registration and store-scope | ||
| * inheritance (a node created inside a live keyless mark's identity scope): | ||
| * bumps the refcount and pokes the node's verdict companions so an | ||
| * already-materialized `false` flips reactively. | ||
| * Counter mutation seam for the mark engine in affects.ts: an imported `let` | ||
| * binding is read-only, and the read-path gate above must stay a plain module | ||
| * variable so `read()` pays one integer compare, not a function call. | ||
| * | ||
| * @internal | ||
| */ | ||
| export declare function markAffects(node: OptimisticNode): void; | ||
| /** | ||
| * Registers one `affects()` mark on a node: counts it, records the | ||
| * registration with the current transaction (after initTransition the queue's | ||
| * array aliases the active transition's, mirroring `_optimisticNodes`), and | ||
| * propagates STATUS_PENDING downstream on the status rails so everything | ||
| * DERIVED from the marked data reads pending too. Propagation runs on every | ||
| * registration (not just the first): subscribers gained since an earlier | ||
| * overlapping registration get covered, and dedup stops re-descent early. | ||
| * | ||
| * @internal | ||
| */ | ||
| export declare function registerAffectsMark(node: OptimisticNode): void; | ||
| /** | ||
| * Releases one registration. When the node's last mark drops, settles the | ||
| * mark's sentinel out of every downstream `_pendingSources` (waking blocked | ||
| * nodes and re-deriving verdicts along the walk). Companion writes go through | ||
| * the settlement snap (committed, not transition-scoped) so releasing a mark | ||
| * can't open a fresh override window that would itself need settlement. | ||
| * | ||
| * @internal | ||
| */ | ||
| export declare function releaseAffectsMark(node: OptimisticNode): void; | ||
| export declare function shiftAffectsMarks(delta: 1 | -1): void; | ||
| export declare const globalQueue: GlobalQueue; | ||
@@ -143,0 +148,0 @@ /** |
@@ -66,2 +66,1 @@ import { type Computed, type Refreshable } from "../core/index.js"; | ||
| export declare function createWriteTraps(isActive?: () => boolean, onDraftWrite?: () => void): ProxyHandler<any>; | ||
| export declare const writeTraps: ProxyHandler<any>; |
+8
-6
| { | ||
| "name": "@solidjs/signals", | ||
| "version": "2.0.0-beta.18", | ||
| "version": "2.0.0-beta.19", | ||
| "description": "Solid's reactive primitives: signals, memos, effects, stores, and async-aware computations.", | ||
@@ -17,3 +17,5 @@ "author": "Ryan Carniato", | ||
| "main": "./dist/node.cjs", | ||
| "module": "./dist/prod.js", | ||
| "module": "./dist/prod/index.js", | ||
| "unpkg": "./dist/prod/index.js", | ||
| "jsdelivr": "./dist/prod/index.js", | ||
| "types": "./dist/types/index.d.ts", | ||
@@ -32,3 +34,3 @@ "type": "module", | ||
| "development": "./dist/dev.js", | ||
| "default": "./dist/prod.js" | ||
| "default": "./dist/prod/index.js" | ||
| }, | ||
@@ -45,3 +47,2 @@ "require": { | ||
| "@rollup/plugin-replace": "^6.0.3", | ||
| "@rollup/plugin-terser": "^0.4.4", | ||
| "@rollup/plugin-typescript": "^12.3.0", | ||
@@ -52,2 +53,3 @@ "@types/node": "^25.0.8", | ||
| "rollup-plugin-prettier": "^4.1.2", | ||
| "terser": "^5.49.0", | ||
| "tslib": "^2.8.1", | ||
@@ -60,4 +62,4 @@ "typescript": "^6.0.3", | ||
| "build": "npm-run-all -nl build:* && pnpm types", | ||
| "build:clean": "rimraf dist/dev.js dist/prod.js dist/node.cjs", | ||
| "build:js": "rollup -c", | ||
| "build:clean": "rimraf dist/dev dist/prod dist/node dist/dev.js dist/prod.js dist/node.cjs", | ||
| "build:js": "rollup -c && node ./scripts/mangle-props.mjs dist/prod dist/node.cjs && node ./scripts/check-pure.mjs dist/prod", | ||
| "types": "tsc -p tsconfig.build.json && node ../../scripts/sync-dual-types.mjs ./dist/types ./dist/types-cjs", | ||
@@ -64,0 +66,0 @@ "test": "vitest run", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
1003370
55.27%96
47.69%22568
31.97%