@solidjs/signals
Advanced tools
+17
-21
@@ -1,4 +0,4 @@ | ||
| import { forEachDependent, notifyStatus, addPendingSource, setPendingError, settlePendingSource } from "./core/async.js"; | ||
| import { addPendingSource, setPendingError, settlePendingSource, forEachDependent, notifyStatus } from "./core/async.js"; | ||
| import { $REFRESH, STATUS_PENDING } from "./core/constants.js"; | ||
| import { STATUS_PENDING, $REFRESH } from "./core/constants.js"; | ||
@@ -9,3 +9,3 @@ import { NotReadyError } from "./core/error.js"; | ||
| import { globalQueue, schedule, GlobalQueue, shiftAffectsMarks } from "./core/scheduler.js"; | ||
| import { GlobalQueue, shiftAffectsMarks, globalQueue, schedule } from "./core/scheduler.js"; | ||
@@ -72,3 +72,3 @@ import "./core/verdict.js"; | ||
| forEachDependent(e, e => { | ||
| if (e.m !== t && !e.M?.has(t)) { | ||
| if (!e.m?.has(t)) { | ||
| notifyStatus(e, STATUS_PENDING, r); | ||
@@ -89,3 +89,3 @@ } | ||
| const o = t[f]; | ||
| if (!o.P) continue; | ||
| if (!o.M) continue; | ||
| const s = getAffectsSentinel(o); | ||
@@ -98,3 +98,3 @@ if (addPendingSource(e, s)) { | ||
| } | ||
| if (r && GlobalQueue._ !== null) GlobalQueue._(e); | ||
| if (r && GlobalQueue.P !== null) GlobalQueue.P(e); | ||
| } | ||
@@ -108,7 +108,7 @@ | ||
| */ function markAffects(e) { | ||
| e.P = (e.P || 0) + 1; | ||
| e.M = (e.M || 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); | ||
| if (e.M === 1 && GlobalQueue.P !== null) GlobalQueue.P(e); | ||
| } | ||
@@ -119,3 +119,3 @@ | ||
| * registration with the current transaction (after initTransition the queue's | ||
| * array aliases the active transition's, mirroring `_optimisticNodes`), and | ||
| * batch IS the active transition, mirroring `_optimisticNodes`), and | ||
| * propagates STATUS_PENDING downstream on the status rails so everything | ||
@@ -127,3 +127,3 @@ * DERIVED from the marked data reads pending too. Propagation runs on every | ||
| markAffects(e); | ||
| globalQueue.N.push(e); | ||
| globalQueue.N._.push(e); | ||
| propagateAffectsMark(e); | ||
@@ -141,4 +141,4 @@ schedule(); | ||
| shiftAffectsMarks(-1); | ||
| e.P--; | ||
| if (!e.P) { | ||
| e.M--; | ||
| if (!e.M) { | ||
| const t = e.t; | ||
@@ -167,3 +167,3 @@ if (t) settlePendingSource(e, t, true); | ||
| */ function onlyMarkPending(e) { | ||
| const t = e.M; | ||
| const t = e.m; | ||
| if (t) { | ||
@@ -173,3 +173,3 @@ for (const e of t) if (!e.l) return false; | ||
| } | ||
| return !!e.m?.l; | ||
| return false; | ||
| } | ||
@@ -188,10 +188,6 @@ | ||
| */ 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) { | ||
| if (e.m) { | ||
| for (const r of e.m) { | ||
| const e = r.l; | ||
| if (e && e.P) t.push(e); | ||
| if (e && e.M) t.push(e); | ||
| } | ||
@@ -198,0 +194,0 @@ } |
@@ -20,7 +20,19 @@ import { globalQueue, activeTransition, currentTransition, setActiveTransition, schedule, flush } from "./scheduler.js"; | ||
| * | ||
| * 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. | ||
| * `yield` is the transaction-safe suspension point: the action waits for a | ||
| * yielded promise and re-enters the transaction before running the code after | ||
| * it. A plain `await` does NOT — the runtime has no hook into an async | ||
| * generator's internal await continuations, so writes to fresh signals | ||
| * between an `await` and the next `yield` escape the transaction and commit | ||
| * immediately. `await` is still the ergonomic choice for typed results; just | ||
| * put a bare `yield` before any writes that follow it: | ||
| * | ||
| * ```ts | ||
| * const saved = await api.createTodo(text); // typed result | ||
| * yield; // re-enter the transaction before writing | ||
| * setTodos(t => { ... }); | ||
| * ``` | ||
| * | ||
| * (For the same reason, don't call `flush()` inside an action body — it | ||
| * drains the transaction mid-step.) | ||
| * | ||
| * Each call returns a `Promise` that resolves with the generator's return | ||
@@ -35,6 +47,7 @@ * value, or rejects if it throws. Pair with `createOptimistic` / | ||
| * | ||
| * const addTodo = action(function* (text: string) { | ||
| * const addTodo = action(async 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 | ||
| * const saved = await api.createTodo(text); // network round-trip, typed | ||
| * yield; // re-enter the transaction | ||
| * setTodos(t => { | ||
@@ -41,0 +54,0 @@ * const i = t.findIndex(x => x.id === tempId); |
+57
-71
@@ -17,15 +17,12 @@ import { STATUS_UNINITIALIZED, STATUS_ERROR, STATUS_PENDING, REACTIVE_DIRTY, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING } from "./constants.js"; | ||
| import { globalQueue, GlobalQueue, clock, schedule, queuePendingNode, flush, insertSubs } from "./scheduler.js"; | ||
| import { globalQueue, GlobalQueue, clock, schedule, flush, queuePendingNode, insertSubs } from "./scheduler.js"; | ||
| // The lazily-created Set is the ONE container for pending sources. Its | ||
| // predecessor — a singular slot promoted to a Set on the second source — | ||
| // created dual state whose migration invariant was easy to break: a third | ||
| // overlapping source landed beside the Set and removePendingSource refused | ||
| // to clear it, stranding the Set members' pending forever (#2893). | ||
| 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; | ||
| } | ||
| if (e.m?.has(n)) return false; | ||
| (e.m ??= new Set).add(n); | ||
| return true; | ||
@@ -35,14 +32,4 @@ } | ||
| 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; | ||
| } | ||
| if (!e.m?.delete(n)) return false; | ||
| if (e.m.size === 0) e.m = undefined; | ||
| return true; | ||
@@ -52,5 +39,4 @@ } | ||
| function clearPendingSources(e) { | ||
| e.m?.clear(); | ||
| e.m = undefined; | ||
| e.M?.clear(); | ||
| e.M = undefined; | ||
| } | ||
@@ -90,17 +76,17 @@ | ||
| let t = false; | ||
| const u = new Set; | ||
| const o = new Set; | ||
| // Companion updates no-op without the verdict layer (null hooks). | ||
| const o = r ? GlobalQueue.R : GlobalQueue._; | ||
| const u = r ? GlobalQueue.R : GlobalQueue.P; | ||
| const settle = e => { | ||
| if (u.has(e) || !removePendingSource(e, n)) return; | ||
| u.add(e); | ||
| if (o.has(e) || !removePendingSource(e, n)) return; | ||
| o.add(e); | ||
| e.Ie = clock; | ||
| const r = e.m ?? e.M?.values().next().value; | ||
| const r = e.m?.values().next().value; | ||
| if (r) { | ||
| setPendingError(e, r); | ||
| o !== null && o(e); | ||
| u !== null && u(e); | ||
| } else { | ||
| e.i &= ~STATUS_PENDING; | ||
| setPendingError(e); | ||
| o !== null && o(e); | ||
| u !== null && u(e); | ||
| if (e.Re) { | ||
@@ -125,10 +111,10 @@ enqueueSub(e); | ||
| let t = false; | ||
| let u = false; | ||
| let o = false; | ||
| if (typeof n === "object" && n !== null) { | ||
| untrack(() => { | ||
| t = n[Symbol.asyncIterator]; | ||
| u = !t && isThenable(n); | ||
| o = !t && isThenable(n); | ||
| }); | ||
| } | ||
| if (!u && !t) { | ||
| if (!o && !t) { | ||
| e.Ae = null; | ||
@@ -138,3 +124,3 @@ return n; | ||
| e.Ae = n; | ||
| let o; | ||
| let u; | ||
| const handleError = r => { | ||
@@ -147,3 +133,3 @@ if (e.Ae !== n) return; | ||
| }; | ||
| const asyncWrite = (t, u) => { | ||
| const asyncWrite = (t, o) => { | ||
| if (e.Ae !== n) return; | ||
@@ -155,3 +141,3 @@ // If the node was dirtied by a newer write (optimistic override or regular), | ||
| globalQueue.initTransition(resolveTransition(e)); | ||
| const o = !!(e.i & STATUS_UNINITIALIZED); | ||
| const u = !!(e.i & STATUS_UNINITIALIZED); | ||
| trimStaleDeps(e); | ||
@@ -163,3 +149,3 @@ clearStatus(e); | ||
| r(t); | ||
| if (o) clearStatus(e, true); | ||
| if (u) clearStatus(e, true); | ||
| } else if (e.be !== undefined) { | ||
@@ -191,5 +177,5 @@ // Optimistic node — resting OR covered by an active override — holds | ||
| const r = e.Ue; | ||
| const u = e.Ge; | ||
| const o = e.Ge; | ||
| try { | ||
| if (!n && o || !u || !u(t, r)) { | ||
| if (!n && u || !o || !o(t, r)) { | ||
| e.Ue = t; | ||
@@ -222,9 +208,9 @@ e.Ie = clock; | ||
| flush(); | ||
| u?.(); | ||
| o?.(); | ||
| }; | ||
| if (u) { | ||
| let r = false, t = false, u, l = true; | ||
| if (o) { | ||
| let r = false, t = false, o, l = true; | ||
| n.then(e => { | ||
| if (l) { | ||
| o = e; | ||
| u = e; | ||
| r = true; | ||
@@ -234,3 +220,3 @@ } else asyncWrite(e); | ||
| if (l) { | ||
| u = e; | ||
| o = e; | ||
| t = true; | ||
@@ -244,4 +230,4 @@ } else handleError(e); | ||
| // momentarily read as `undefined`. | ||
| handleError(u); | ||
| throw u; | ||
| handleError(o); | ||
| throw o; | ||
| } else if (!r) { | ||
@@ -255,7 +241,7 @@ globalQueue.initTransition(resolveTransition(e)); | ||
| let t = false; | ||
| let u = false; | ||
| let o = false; | ||
| let l = true; | ||
| cleanup(() => { | ||
| if (u) return; | ||
| u = true; | ||
| if (o) return; | ||
| o = true; | ||
| try { | ||
@@ -272,3 +258,3 @@ const e = r.return?.(); | ||
| f = true; | ||
| if (r.done) u = true; | ||
| if (r.done) o = true; | ||
| } else if (e.Ae !== n) { | ||
@@ -280,3 +266,3 @@ return; | ||
| } else { | ||
| u = true; | ||
| o = true; | ||
| if (t) { | ||
@@ -295,3 +281,3 @@ schedule(); | ||
| } else if (e.Ae === n) { | ||
| u = true; | ||
| o = true; | ||
| handleError(r); | ||
@@ -303,3 +289,3 @@ } | ||
| // Match the promise branch, but only rethrow during the initial read. | ||
| u = true; | ||
| o = true; | ||
| handleError(s); | ||
@@ -310,3 +296,3 @@ if (l) throw s; | ||
| if (f && !i.done) { | ||
| o = i.value; | ||
| u = i.value; | ||
| t = true; | ||
@@ -325,7 +311,7 @@ return iterate(); | ||
| } | ||
| return o; | ||
| return u; | ||
| } | ||
| function clearStatus(e, n = false) { | ||
| if (e.m || e.M) clearPendingSources(e); | ||
| if (e.m) clearPendingSources(e); | ||
| if (e.Re) e.Re = false; | ||
@@ -340,3 +326,3 @@ // The pending window is over; its quiet classification dies with it. | ||
| // once the verdict layer created them, which installs the hooks). | ||
| if (e.ye || e.pe) GlobalQueue._(e); | ||
| if (e.ye || e.pe) GlobalQueue.P(e); | ||
| if (e.G && GlobalQueue.me !== null) GlobalQueue.me(e); | ||
@@ -346,6 +332,6 @@ if (e.C) e.C(); | ||
| function notifyStatus(e, n, r, t, u) { | ||
| function notifyStatus(e, n, r, t, o) { | ||
| // 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; | ||
| const u = n === STATUS_PENDING && r instanceof NotReadyError ? r.source : undefined; | ||
| // Mark-sourced propagation must not capture subscribers into the marking | ||
@@ -356,3 +342,3 @@ // action's transaction (#2893): they carry no held value needing a | ||
| // settles. Real async keeps queuing (its commits ride the transition). | ||
| const l = o?.l !== undefined; | ||
| const l = u?.l !== undefined; | ||
| // A real error is a settled verdict a mark must not erase (#2893): landing | ||
@@ -364,12 +350,12 @@ // STATUS_PENDING here would clobber `_error` with the sentinel's | ||
| if (l && e.i & STATUS_ERROR) return; | ||
| const i = o === e; | ||
| const i = u === 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); | ||
| if (n === STATUS_PENDING && u) { | ||
| addPendingSource(e, u); | ||
| 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); | ||
| setPendingError(e, u, r); | ||
| } else { | ||
@@ -380,10 +366,10 @@ clearPendingSources(e); | ||
| } | ||
| GlobalQueue._ !== null && GlobalQueue._(e); | ||
| GlobalQueue.P !== null && GlobalQueue.P(e); | ||
| if (e.G && GlobalQueue.me !== null) GlobalQueue.me(e); | ||
| } | ||
| if (u && !t) { | ||
| assignOrMergeLane(e, u); | ||
| if (o && !t) { | ||
| assignOrMergeLane(e, o); | ||
| } | ||
| const a = t || f; | ||
| const c = t || s ? undefined : u; | ||
| const c = t || s ? undefined : o; | ||
| if (e.C) { | ||
@@ -402,3 +388,3 @@ if (t && n === STATUS_PENDING) { | ||
| 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)) { | ||
| if (n === STATUS_PENDING && u && !e.m?.has(u) || n !== STATUS_PENDING && (e.k !== r || e.m)) { | ||
| // A pending-observer link is the subscription an `isPending` read created. | ||
@@ -405,0 +391,0 @@ // It exists so the observer re-runs when the source settles, but it must |
@@ -64,2 +64,16 @@ const REACTIVE_NONE = 0; | ||
| /** | ||
| * Stand-in stored in `_overrideValue` for an optimistic write of literal | ||
| * `undefined` (#2898). The slot doubles as the optimistic-node brand | ||
| * (`undefined` = not optimistic, `NOT_PENDING` = at rest), so the raw value | ||
| * would erase the node's optimistic identity: the write turns invisible and | ||
| * follow-up writes route off the optimistic path and commit permanently. | ||
| * Same shape as NO_SNAPSHOT. Sites that surface the override VALUE unwrap | ||
| * via `visibleOverrideValue`; slot identity tests stay raw. | ||
| */ const OVERRIDE_UNDEFINED = {}; | ||
| /** Unwrap an active override's stored value for surfacing to readers (#2898). */ function unwrapOverride(E) { | ||
| return E === OVERRIDE_UNDEFINED ? undefined : E; | ||
| } | ||
| const STORE_SNAPSHOT_PROPS = "sp"; | ||
@@ -79,2 +93,2 @@ | ||
| 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 }; | ||
| 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, OVERRIDE_UNDEFINED, 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, unwrapOverride }; |
+39
-39
| 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 { EFFECT_TRACKED, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING, STATUS_UNINITIALIZED, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, CONFIG_SYNC, STATUS_PENDING, STATUS_ERROR, REACTIVE_NONE, REACTIVE_SNAPSHOT_STALE, unwrapOverride, OVERRIDE_UNDEFINED, CONFIG_AUTO_DISPOSE, REACTIVE_LAZY, REACTIVE_DISPOSED, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, defaultContext, CONFIG_IN_SNAPSHOT_SCOPE, CONFIG_TRANSPARENT, CONFIG_OWNED_WRITE, CONFIG_NO_SNAPSHOT, $REFRESH, REACTIVE_MANUAL_WRITE, NO_SNAPSHOT, STORE_SNAPSHOT_PROPS, EFFECT_USER } from "./constants.js"; | ||
@@ -9,9 +9,9 @@ import { NotReadyError } from "./error.js"; | ||
| import { insertIntoHeap, queueFor, deleteFromHeap, insertIntoHeapHeight, markNode, markHeap } from "./heap.js"; | ||
| import { deleteFromHeap, queueFor, insertIntoHeapHeight, markNode, markHeap, insertIntoHeap } from "./heap.js"; | ||
| import { schedule, GlobalQueue, globalQueue, clock, activeTransition, queuePendingNode, insertSubs, dirtyQueue, activeAffectsMarks, runInTransition, projectionWriteActive, armReaskClear } from "./scheduler.js"; | ||
| import { GlobalQueue, activeTransition, globalQueue, clock, insertSubs, queuePendingNode, activeAffectsMarks, runInTransition, schedule, dirtyQueue, projectionWriteActive, armReaskClear } from "./scheduler.js"; | ||
| import "./invariants.js"; | ||
| import { inheritId, disposeChildren, markDisposal } from "./owner.js"; | ||
| import { disposeChildren, markDisposal, inheritId } from "./owner.js"; | ||
@@ -64,3 +64,3 @@ GlobalQueue.Ce = recompute; | ||
| if (e.ke) return true; | ||
| e = e.He; | ||
| e = e.Fe; | ||
| } | ||
@@ -86,3 +86,3 @@ return false; | ||
| function releaseSubtree(e) { | ||
| let t = e.Fe; | ||
| let t = e.He; | ||
| while (t) { | ||
@@ -126,8 +126,8 @@ if (t.ke) { | ||
| // 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) { | ||
| if (e.ve || n === EFFECT_TRACKED) disposeChildren(e); else if (e.He !== null || e.Me !== null) { | ||
| markDisposal(e); | ||
| e.We = e.Me; | ||
| e.Ze = e.Fe; | ||
| e.Ze = e.He; | ||
| e.Me = null; | ||
| e.Fe = null; | ||
| e.He = null; | ||
| e.je = 0; | ||
@@ -172,5 +172,5 @@ } else ; | ||
| } | ||
| const T = n && n !== EFFECT_USER; | ||
| const N = stale; | ||
| if (T) stale = true; | ||
| const N = n && n !== EFFECT_USER; | ||
| const T = stale; | ||
| if (N) stale = true; | ||
| try { | ||
@@ -209,3 +209,3 @@ if (!false && e.U & CONFIG_SYNC) { | ||
| tracking = c; | ||
| if (T) stale = N; | ||
| if (N) stale = T; | ||
| e.u = REACTIVE_NONE | (t ? e.u & REACTIVE_SNAPSHOT_STALE : 0); | ||
@@ -218,3 +218,3 @@ context = o; | ||
| trimStaleDeps(e); | ||
| const s = l ? e.be : e.ge === NOT_PENDING ? e.Ue : e.ge; | ||
| const s = l ? unwrapOverride(e.be) : e.ge === NOT_PENDING ? e.Ue : e.ge; | ||
| let o = false; | ||
@@ -250,3 +250,3 @@ try { | ||
| if (l && i) { | ||
| e.be = a; | ||
| e.be = a === undefined ? OVERRIDE_UNDEFINED : a; | ||
| e.ge = NOT_PENDING; | ||
@@ -290,3 +290,3 @@ } | ||
| 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)); | ||
| const p = 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 | ||
@@ -297,3 +297,3 @@ // to their own transition's schedule (A18 re-rule) and is unobservable | ||
| // _transition. | ||
| S && (!t || e.i & STATUS_PENDING) && (!e.ve || l) && queuePendingNode(e); | ||
| p && (!t || e.i & STATUS_PENDING) && (!e.ve || l) && queuePendingNode(e); | ||
| e.ve && n && activeTransition !== e.ve && runInTransition(e.ve, () => recompute(e)); | ||
@@ -343,6 +343,6 @@ } | ||
| Et: null, | ||
| He: context, | ||
| Fe: context, | ||
| Ve: null, | ||
| Tt: null, | ||
| Fe: null, | ||
| Nt: null, | ||
| He: null, | ||
| u: t?.lazy ? REACTIVE_LAZY : REACTIVE_NONE, | ||
@@ -389,6 +389,6 @@ i: STATUS_UNINITIALIZED, | ||
| Et: null, | ||
| He: context, | ||
| Fe: context, | ||
| Ve: null, | ||
| Tt: null, | ||
| Fe: null, | ||
| Nt: null, | ||
| He: null, | ||
| u: REACTIVE_LAZY, | ||
@@ -404,6 +404,6 @@ i: STATUS_UNINITIALIZED, | ||
| it: false, | ||
| Nt: undefined, | ||
| Tt: undefined, | ||
| It: t, | ||
| St: n, | ||
| At: undefined, | ||
| dt: undefined, | ||
| De: i, | ||
@@ -422,11 +422,11 @@ C: l | ||
| e.ft = e; | ||
| const n = context?.dt ? context.Ct : context; | ||
| const n = context?.At ? context.Ct : context; | ||
| if (context) { | ||
| const t = context.Fe; | ||
| const t = context.He; | ||
| if (t === null) { | ||
| context.Fe = e; | ||
| context.He = e; | ||
| } else { | ||
| e.Ve = t; | ||
| t.Tt = e; | ||
| context.Fe = e; | ||
| t.Nt = e; | ||
| context.He = e; | ||
| } | ||
@@ -537,3 +537,3 @@ } | ||
| let t = context; | ||
| if (t?.dt) t = t.Ct; | ||
| if (t?.At) t = t.Ct; | ||
| const n = e; | ||
@@ -556,3 +556,3 @@ const i = e.rt; | ||
| // the enclosing memo would make an `isPending` wrapper itself pending). | ||
| if (activeAffectsMarks !== 0 && e.P && !pendingCheckActive) (affectsReads ??= []).push(e); | ||
| if (activeAffectsMarks !== 0 && e.M && !pendingCheckActive) (affectsReads ??= []).push(e); | ||
| } | ||
@@ -569,3 +569,3 @@ return !t || e.ge === NOT_PENDING ? e.Ue : e.ge; | ||
| if (activeAffectsMarks !== 0 && !pendingCheckActive) { | ||
| if (e.P) (affectsReads ??= []).push(e); | ||
| if (e.M) (affectsReads ??= []).push(e); | ||
| if (l.i & STATUS_PENDING) GlobalQueue.I(l, affectsReads ??= []); | ||
@@ -582,3 +582,3 @@ } | ||
| // parent check is shallow, might need to be recursive | ||
| if (i >= t.qe && e.He !== t) { | ||
| if (i >= t.qe && e.Fe !== t) { | ||
| t.qe = i + 1; | ||
@@ -632,4 +632,4 @@ } | ||
| // the value for every reader — that check itself stays right here). | ||
| if (t && stale && GlobalQueue.gt(e)) return e.Ue; | ||
| return e.be; | ||
| if (t && stale && GlobalQueue.Dt(e)) return e.Ue; | ||
| return unwrapOverride(e.be); | ||
| } | ||
@@ -643,3 +643,3 @@ // Entanglement gate: a reader recomputing under an optimistic lane that reads | ||
| // engine — a non-null lane implies it is installed.) | ||
| if (currentOptimisticLane !== null && activeTransition !== null && t !== null && GlobalQueue.Dt(e, l, t)) { | ||
| if (currentOptimisticLane !== null && activeTransition !== null && t !== null && GlobalQueue.gt(e, l, t)) { | ||
| return e.Ue; | ||
@@ -651,6 +651,6 @@ } | ||
| // (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; | ||
| const u = !t || currentOptimisticLane !== null && GlobalQueue.vt(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 (pendingCheckActive) GlobalQueue.kt(e, u); | ||
| if (!t && l === e && typeof n.xe === "function" && e.U & CONFIG_AUTO_DISPOSE && !(l.i & STATUS_PENDING) && !e.p) { | ||
@@ -657,0 +657,0 @@ unobserved(e); |
+14
-14
@@ -1,2 +0,2 @@ | ||
| import { EFFECT_USER, CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, EFFECT_TRACKED, STATUS_ERROR, STATUS_PENDING, EFFECT_RENDER, REACTIVE_DISPOSED } from "./constants.js"; | ||
| import { REACTIVE_DISPOSED, STATUS_ERROR, EFFECT_USER, CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, EFFECT_TRACKED, STATUS_PENDING, EFFECT_RENDER } from "./constants.js"; | ||
@@ -7,3 +7,3 @@ import { createEffectNode, recompute, computed, staleValues } from "./core.js"; | ||
| import { haltReactivity, GlobalQueue } from "./scheduler.js"; | ||
| import { GlobalQueue, haltReactivity } from "./scheduler.js"; | ||
@@ -66,8 +66,8 @@ /** | ||
| const E = unwrapStatusError(t.k); | ||
| t.Nt = t.Ue; | ||
| t.Tt = t.Ue; | ||
| t.it = false; | ||
| try { | ||
| t.St ? t.St(E, () => { | ||
| const E = t.At; | ||
| t.At = undefined; | ||
| const E = t.dt; | ||
| t.dt = undefined; | ||
| E?.(); | ||
@@ -83,10 +83,10 @@ }) : console.error(E); | ||
| } | ||
| const E = t.At; | ||
| t.At = undefined; | ||
| const E = t.dt; | ||
| t.dt = undefined; | ||
| try { | ||
| E?.(); | ||
| const e = t.It(t.Ue, t.Nt); | ||
| const e = t.It(t.Ue, t.Tt); | ||
| if (false && e !== undefined && typeof e !== "function") ; | ||
| // The final cleanup is invoked by disposeChildren at true disposal. | ||
| t.At = e; | ||
| t.dt = e; | ||
| } catch (E) { | ||
@@ -100,3 +100,3 @@ t.k = new StatusError(t, E); | ||
| } finally { | ||
| t.Nt = t.Ue; | ||
| t.Tt = t.Ue; | ||
| t.it = false; | ||
@@ -121,7 +121,7 @@ } | ||
| const e = computed(() => { | ||
| const E = e.At; | ||
| e.At = undefined; | ||
| const E = e.dt; | ||
| e.dt = undefined; | ||
| E?.(); | ||
| const R = staleValues(t); | ||
| e.At = R; | ||
| e.dt = R; | ||
| }, { | ||
@@ -131,3 +131,3 @@ ...E, | ||
| }); | ||
| e.At = undefined; | ||
| e.dt = undefined; | ||
| e.U = e.U & ~CONFIG_AUTO_DISPOSE | CONFIG_CHILDREN_FORBIDDEN; | ||
@@ -134,0 +134,0 @@ e.it = true; |
@@ -1,2 +0,2 @@ | ||
| import { REACTIVE_RECOMPUTING_DEPS, CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE } from "./constants.js"; | ||
| import { CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE, REACTIVE_RECOMPUTING_DEPS } from "./constants.js"; | ||
@@ -3,0 +3,0 @@ import { deleteFromHeap, queueFor } from "./heap.js"; |
@@ -1,2 +0,2 @@ | ||
| 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 { REACTIVE_IN_HEAP, EFFECT_TRACKED, EFFECT_USER, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_RECOMPUTING_DEPS, REACTIVE_MANUAL_WRITE, REACTIVE_ZOMBIE, REACTIVE_CHECK, REACTIVE_DIRTY } from "./constants.js"; | ||
@@ -28,3 +28,3 @@ import { zombieQueue, dirtyQueue } from "./scheduler.js"; | ||
| function actualInsertIntoHeap(e, E) { | ||
| const t = (e.He?.dt ? e.He.Ct?.qe : e.He?.qe) ?? -1; | ||
| const t = (e.Fe?.At ? e.Fe.Ct?.qe : e.Fe?.qe) ?? -1; | ||
| if (t >= e.qe) e.qe = t + 1; | ||
@@ -31,0 +31,0 @@ const n = e.qe; |
+23
-13
| import { NOT_PENDING } from "./constants.js"; | ||
| import { activeTransition } from "./scheduler.js"; | ||
| import { currentTransition, activeTransition } from "./scheduler.js"; | ||
@@ -22,10 +22,10 @@ // Map from optimistic signal to its lane (reused for multiple writes to same signal) | ||
| const i = n.en; | ||
| const a = i?.Je ? findLane(i.Je) : null; | ||
| const r = i?.Je ? findLane(i.Je) : null; | ||
| e = { | ||
| an: n, | ||
| rn: n, | ||
| Pe: new Set, | ||
| tn: [ [], [] ], | ||
| rn: null, | ||
| an: null, | ||
| ve: activeTransition, | ||
| sn: a | ||
| sn: r | ||
| }; | ||
@@ -49,6 +49,6 @@ signalLanes.set(n, e); | ||
| if (!i) return; | ||
| const a = findLane(i); | ||
| const r = 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; | ||
| if (r !== e && r.rn === n && !r.sn) r.sn = e; | ||
| } | ||
@@ -59,3 +59,3 @@ | ||
| */ function findLane(n) { | ||
| while (n.rn) n = n.rn; | ||
| while (n.an) n = n.an; | ||
| return n; | ||
@@ -70,3 +70,3 @@ } | ||
| if (n === e) return n; | ||
| e.rn = n; | ||
| e.an = n; | ||
| // Move (not copy) the merged lane's work: after the merge all routing goes | ||
@@ -96,2 +96,12 @@ // through findLane() to the root, so anything left behind here is dead — | ||
| function resolveTransition(n) { | ||
| // An active override answers with its owner, not its lane: lanes are | ||
| // scheduling affinity and a shared subscriber merges them across | ||
| // transactions (#2912) — the merged root's _transition would hand this | ||
| // node's override to whichever action wrote last through the shared | ||
| // reader. Chase merge chains; a dead owner settled through another path. | ||
| if (hasActiveOverride(n) && n.fn) { | ||
| const e = n.fn = currentTransition(n.fn); | ||
| if (e.cn !== true) return e; | ||
| n.fn = null; | ||
| } | ||
| return resolveLane(n)?.ve ?? n.ve; | ||
@@ -111,12 +121,12 @@ } | ||
| const i = findLane(e); | ||
| const a = n.Je; | ||
| if (a) { | ||
| const r = n.Je; | ||
| if (r) { | ||
| // 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) { | ||
| if (r.an) { | ||
| n.Je = e; | ||
| return; | ||
| } | ||
| const t = findLane(a); | ||
| const t = findLane(r); | ||
| if (activeLanes.has(t)) { | ||
@@ -123,0 +133,0 @@ if (t !== i && !hasActiveOverride(n)) { |
+107
-99
@@ -1,4 +0,4 @@ | ||
| import { NOT_PENDING, STATUS_UNINITIALIZED, STATUS_PENDING, CONFIG_OWNED_WRITE, REACTIVE_MANUAL_WRITE, REACTIVE_OPTIMISTIC_DIRTY, EFFECT_RENDER, EFFECT_USER } from "./constants.js"; | ||
| import { NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, OVERRIDE_UNDEFINED, 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 { latestReadActive, currentOptimisticLane, stale } from "./core.js"; | ||
@@ -32,29 +32,36 @@ import { NotReadyError } from "./error.js"; | ||
| /** 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); | ||
| /** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */ function optimisticWrite(e, n) { | ||
| const t = e.be !== NOT_PENDING; | ||
| const i = t ? unwrapOverride(e.be) : e.Ue; | ||
| if (typeof n === "function") n = n(i); | ||
| const u = !!(e.i & STATUS_UNINITIALIZED) || !e.Ge || !e.Ge(i, n); | ||
| 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); | ||
| if (t) { | ||
| const n = resolveTransition(e); | ||
| if (n && activeTransition !== n) globalQueue.initTransition(n); | ||
| } | ||
| return t; | ||
| return n; | ||
| } | ||
| if (n) globalQueue.initTransition(resolveTransition(e)); | ||
| if (t) 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); | ||
| else globalQueue.N.Be.push(e); | ||
| // Stamp ownership on the node (post-merge, so entangled writers share the | ||
| // joint root). resolveTransition prefers this over the lane's _transition, | ||
| // which a shared subscriber can merge across transactions (#2912). | ||
| e.fn = activeTransition; | ||
| const s = getOrCreateLane(e); | ||
| e.Je = s; | ||
| e.be = t; | ||
| GlobalQueue._e !== null && GlobalQueue._e(e, t); | ||
| // Literal undefined must not land raw: the slot doubles as the optimistic | ||
| // brand, and erasing it makes the write invisible and routes follow-up | ||
| // writes off the optimistic path into permanent commits (#2898). | ||
| e.be = n === undefined ? OVERRIDE_UNDEFINED : n; | ||
| GlobalQueue._e !== null && GlobalQueue._e(e, n); | ||
| e.Ie = clock; | ||
| insertSubs(e, true); | ||
| schedule(); | ||
| return t; | ||
| return n; | ||
| } | ||
@@ -67,4 +74,4 @@ | ||
| function queueStashedOptimisticEffects(e) { | ||
| for (let t = e.p; t !== null; t = t.de) { | ||
| const e = t.Ee; | ||
| for (let n = e.p; n !== null; n = n.de) { | ||
| const e = n.Ee; | ||
| if (!e.De) continue; | ||
@@ -80,7 +87,7 @@ enqueueSub(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); | ||
| for (let n = 0; n < e.Be.length; n++) { | ||
| const t = e.Be[n]; | ||
| if (t.xe || t.U & CONFIG_OWNED_WRITE) continue; | ||
| stashedOptimisticReads.add(t); | ||
| queueStashedOptimisticEffects(t); | ||
| } | ||
@@ -99,9 +106,9 @@ try { | ||
| */ 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 && | ||
| for (let n = 0; n < e.Be.length; n++) { | ||
| const t = e.Be[n]; | ||
| if (hasActiveOverride(t) && "i" in t && t.i & STATUS_PENDING && t.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) { | ||
| !t.k.source?.l) { | ||
| return true; | ||
@@ -117,14 +124,15 @@ } | ||
| // 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; | ||
| const n = e.length; | ||
| for (let t = 0; t < n; t++) { | ||
| const n = e[t]; | ||
| n.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; | ||
| if (!(n.i & STATUS_PENDING)) n.i &= ~STATUS_UNINITIALIZED; | ||
| const i = n.be; | ||
| n.be = NOT_PENDING; | ||
| if (i !== NOT_PENDING && n.Ue !== unwrapOverride(i)) insertSubs(n, true); | ||
| n.ve = null; | ||
| n.fn = null; | ||
| } | ||
@@ -134,13 +142,13 @@ // Settlement checkpoint (#2838): companions caught in this batch (or owned | ||
| // 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); | ||
| for (let t = 0; t < n; t++) { | ||
| const n = e[t]; | ||
| if (n.ye || n.pe) GlobalQueue.R(n); | ||
| const i = n.en; | ||
| if (i && (i.ye === n || i.pe === n)) GlobalQueue.R(i); | ||
| } | ||
| e.splice(0, t); | ||
| e.splice(0, n); | ||
| } | ||
| function runQueue(e, t) { | ||
| for (let n = 0; n < e.length; n++) e[n](t); | ||
| function runQueue(e, n) { | ||
| for (let t = 0; t < e.length; t++) e[t](n); | ||
| } | ||
@@ -151,8 +159,8 @@ | ||
| */ 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); | ||
| for (const n of activeLanes) { | ||
| if (n.an || n.Pe.size > 0) continue; | ||
| const t = n.tn[e - 1]; | ||
| if (t.length) { | ||
| n.tn[e - 1] = []; | ||
| runQueue(t, e); | ||
| } | ||
@@ -163,15 +171,15 @@ } | ||
| 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); | ||
| for (const n of activeLanes) { | ||
| const t = e ? n.ve === e : !n.ve; | ||
| if (!t) continue; | ||
| if (!n.an) { | ||
| if (n.tn[0].length) runQueue(n.tn[0], EFFECT_RENDER); | ||
| if (n.tn[1].length) runQueue(n.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); | ||
| if (n.rn.Je === n) n.rn.Je = undefined; | ||
| n.Pe.clear(); | ||
| n.tn[0].length = 0; | ||
| n.tn[1].length = 0; | ||
| activeLanes.delete(n); | ||
| signalLanes.delete(n.rn); | ||
| } | ||
@@ -184,5 +192,5 @@ } | ||
| // 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); | ||
| const n = e.Je; | ||
| if (!n) return false; | ||
| return findLane(n) === findLane(currentOptimisticLane) && !hasActiveOverride(e); | ||
| } | ||
@@ -194,7 +202,7 @@ | ||
| * 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)) { | ||
| */ function gatedRead(e, n, t) { | ||
| if (latestReadActive || e.ge === NOT_PENDING || e.xe || n !== e && !(n.u & REACTIVE_MANUAL_WRITE)) { | ||
| return false; | ||
| } | ||
| activeTransition.Qt.add(n); | ||
| activeTransition.un.add(t); | ||
| return true; | ||
@@ -206,4 +214,4 @@ } | ||
| * 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); | ||
| */ function laneReadsCommitted(e, n, t) { | ||
| return e.be !== undefined || !!e.Je || n === e && stale && t.en !== e || !!(n.i & STATUS_PENDING); | ||
| } | ||
@@ -215,12 +223,12 @@ | ||
| * 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) { | ||
| */ function recomputeLane(e, n) { | ||
| if (n) return resolveLane(e) ?? null; | ||
| for (let n = e.S; n; n = n.st) { | ||
| const t = n.ot; | ||
| if (t.u & REACTIVE_OPTIMISTIC_DIRTY) { | ||
| const n = resolveLane(t); | ||
| if (n) { | ||
| e.u |= REACTIVE_OPTIMISTIC_DIRTY; | ||
| assignOrMergeLane(e, t); | ||
| return t; | ||
| assignOrMergeLane(e, n); | ||
| return n; | ||
| } | ||
@@ -233,7 +241,7 @@ } | ||
| /** 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); | ||
| const n = findLane(currentOptimisticLane); | ||
| if (n.rn !== e) { | ||
| n.Pe.add(e); | ||
| e.Je = n; | ||
| GlobalQueue.P !== null && GlobalQueue.P(n.rn); | ||
| } | ||
@@ -243,6 +251,6 @@ } | ||
| /** 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); | ||
| const n = resolveLane(e); | ||
| if (n) { | ||
| n.Pe.delete(e); | ||
| GlobalQueue.P !== null && GlobalQueue.P(n.rn); | ||
| } | ||
@@ -252,4 +260,4 @@ } | ||
| function trackOptimisticStore(e) { | ||
| // After initTransition, globalQueue._optimisticStores IS activeTransition._optimisticStores (same reference) | ||
| globalQueue.Lt.add(e); | ||
| // After initTransition, globalQueue._batch IS activeTransition (same reference) | ||
| globalQueue.N.dn.add(e); | ||
| schedule(); | ||
@@ -265,17 +273,17 @@ } | ||
| 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.En = resolveOptimisticNodes; | ||
| GlobalQueue.Tn = stashOptimistic; | ||
| GlobalQueue.In = transitionBlocked; | ||
| GlobalQueue.Nn = cleanupCompletedLanes; | ||
| GlobalQueue.On = runLaneEffects; | ||
| GlobalQueue.Dt = readStashed; | ||
| GlobalQueue.gt = gatedRead; | ||
| GlobalQueue.ht = laneSuspends; | ||
| GlobalQueue.kt = laneReadsCommitted; | ||
| GlobalQueue.vt = laneReadsCommitted; | ||
| GlobalQueue.$e = recomputeLane; | ||
| GlobalQueue.et = laneAsyncPending; | ||
| GlobalQueue.Xe = laneAsyncSettled; | ||
| GlobalQueue.Vt = trackOptimisticStore; | ||
| GlobalQueue.mn = trackOptimisticStore; | ||
| } | ||
| export { installOptimisticEngine }; |
+23
-23
@@ -1,2 +0,2 @@ | ||
| import { defaultContext, CONFIG_TRANSPARENT, REACTIVE_DISPOSED, REACTIVE_ZOMBIE, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT } from "./constants.js"; | ||
| import { REACTIVE_DISPOSED, REACTIVE_ZOMBIE, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, CONFIG_TRANSPARENT, defaultContext } from "./constants.js"; | ||
@@ -9,3 +9,3 @@ import { context, runWithOwner, pendingCheckActive, latestReadActive, tracking } from "./core.js"; | ||
| import { globalQueue, GlobalQueue, zombieQueue, dirtyQueue } from "./scheduler.js"; | ||
| import { GlobalQueue, zombieQueue, dirtyQueue, globalQueue } from "./scheduler.js"; | ||
@@ -16,3 +16,3 @@ const PENDING_OWNER = {}; | ||
| function markDisposal(e) { | ||
| let n = e.Fe; | ||
| let n = e.He; | ||
| while (n) { | ||
@@ -59,3 +59,3 @@ const e = n.u; | ||
| if (n && e.xe) e.Ae = null; | ||
| let l = t ? e.Ze : e.Fe; | ||
| let l = t ? e.Ze : e.He; | ||
| while (l) { | ||
@@ -79,3 +79,3 @@ const e = l.Ve; | ||
| } else { | ||
| e.Fe = null; | ||
| e.He = null; | ||
| e.je = 0; | ||
@@ -87,8 +87,8 @@ } | ||
| // 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; | ||
| if (n && !t && !(i & REACTIVE_ZOMBIE) && e.Fe !== null && !(e.Fe.u & REACTIVE_DISPOSED)) { | ||
| const n = e.Nt; | ||
| const t = e.Ve; | ||
| if (n !== null) n.Ve = t; else e.He.Fe = t; | ||
| if (t !== null) t.Tt = n; | ||
| e.Tt = null; | ||
| if (n !== null) n.Ve = t; else e.Fe.He = t; | ||
| if (t !== null) t.Nt = n; | ||
| e.Nt = null; | ||
| } | ||
@@ -98,5 +98,5 @@ runDisposal(e, t); | ||
| // to mirror rerun ordering (compute-phase teardown first, cleanup last). | ||
| if (n && e.At) { | ||
| const n = e.At; | ||
| e.At = undefined; | ||
| if (n && e.dt) { | ||
| const n = e.dt; | ||
| e.dt = undefined; | ||
| n(); | ||
@@ -122,3 +122,3 @@ } | ||
| let t = e; | ||
| while (t.U & CONFIG_TRANSPARENT && t.He) t = t.He; | ||
| while (t.U & CONFIG_TRANSPARENT && t.Fe) t = t.Fe; | ||
| if (t.id != null) return formatId(t.id, n ? t.je++ : t.je); | ||
@@ -245,7 +245,7 @@ throw new Error(""); | ||
| U: t ? CONFIG_TRANSPARENT : 0, | ||
| dt: true, | ||
| Ct: n?.dt ? n.Ct : n, | ||
| Fe: null, | ||
| At: true, | ||
| Ct: n?.At ? n.Ct : n, | ||
| He: null, | ||
| Ve: null, | ||
| Tt: null, | ||
| Nt: null, | ||
| Me: null, | ||
@@ -257,13 +257,13 @@ v: n?.v ?? globalQueue, | ||
| Ze: null, | ||
| He: n, | ||
| Fe: n, | ||
| dispose: disposeRootSelf | ||
| }; | ||
| if (n) { | ||
| const e = n.Fe; | ||
| const e = n.He; | ||
| if (e === null) { | ||
| n.Fe = i; | ||
| n.He = i; | ||
| } else { | ||
| i.Ve = e; | ||
| e.Tt = i; | ||
| n.Fe = i; | ||
| e.Nt = i; | ||
| n.He = i; | ||
| } | ||
@@ -270,0 +270,0 @@ } |
+180
-180
@@ -1,2 +0,2 @@ | ||
| 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 { EFFECT_RENDER, EFFECT_USER, STATUS_PENDING, REACTIVE_DISPOSED, REACTIVE_ZOMBIE, NOT_PENDING, EFFECT_TRACKED, CONFIG_IN_SNAPSHOT_SCOPE, REACTIVE_SNAPSHOT_STALE, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_REASK, REACTIVE_MANUAL_WRITE, STATUS_UNINITIALIZED } from "./constants.js"; | ||
@@ -58,3 +58,4 @@ import { currentOptimisticLane } from "./core.js"; | ||
| 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; | ||
| const t = e.N; | ||
| return transitions.size === 0 && activeLanes.size === 0 && e.Qt.length === 0 && t.Be.length === 0 && t._.length === 0 && t.dn.size === 0 && transientStoreNodes.size === 0; | ||
| } | ||
@@ -74,3 +75,3 @@ | ||
| // unmarked node for the same property). | ||
| if (e.P) continue; | ||
| if (e.M) continue; | ||
| transientStoreNodes.delete(e); | ||
@@ -94,9 +95,35 @@ e.ct?.(); | ||
| /** | ||
| * Ambient work IS a transaction: the global queue always carries one | ||
| * current-transaction-shaped batch (`globalQueue._batch`). With no transition | ||
| * active, registrations (pending commits, optimistic nodes, affects marks, | ||
| * optimistic stores) land in a plain ambient batch that the plain flush | ||
| * finalizes; when a transition initializes it adopts the ambient batch's | ||
| * contents and `_batch` becomes the transition itself, so later registrations | ||
| * land there directly — no per-field aliasing. | ||
| */ function createBatch() { | ||
| return { | ||
| Ie: clock, | ||
| yt: [], | ||
| Lt: new Map, | ||
| Be: [], | ||
| _: [], | ||
| dn: new Set, | ||
| Te: [], | ||
| Bt: { | ||
| Mt: [ [], [] ], | ||
| Qt: [] | ||
| }, | ||
| cn: false, | ||
| un: new Set | ||
| }; | ||
| } | ||
| function mergeTransitionState(e, t) { | ||
| t.Ht = e; | ||
| t.cn = 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 | ||
| // Move (don't copy): the global queue's batch may still be the outgoing | ||
| // transition, and the adoption pass in initTransition would re-push its | ||
| // contents into the target — duplicating every entry. | ||
@@ -106,16 +133,16 @@ e.Be.push(...t.Be); | ||
| } | ||
| 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 | ||
| if (t._.length) { | ||
| // Move (don't copy): the global queue's batch may still be the outgoing | ||
| // transition, 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; | ||
| e._.push(...t._); | ||
| t._.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 i of t.dn) e.dn.add(i); | ||
| for (const [i, n] of t.Lt) { | ||
| let t = e.Lt.get(i); | ||
| if (!t) e.Lt.set(i, t = new Set); | ||
| for (const e of n) t.add(e); | ||
| } | ||
| for (const i of t.Qt) e.Qt.add(i); | ||
| for (const i of t.un) e.un.add(i); | ||
| } | ||
@@ -130,3 +157,3 @@ | ||
| scheduled = true; | ||
| if (!syncDepth && !globalQueue.Bt && !projectionWriteActive) queueMicrotask(flush); | ||
| if (!syncDepth && !globalQueue.Ut && !projectionWriteActive) queueMicrotask(flush); | ||
| } | ||
@@ -161,28 +188,28 @@ | ||
| class Queue { | ||
| He=null; | ||
| zt=[ [], [] ]; | ||
| wt=[]; | ||
| Fe=null; | ||
| Mt=[ [], [] ]; | ||
| Qt=[]; | ||
| created=clock; | ||
| addChild(e) { | ||
| this.wt.push(e); | ||
| e.He = this; | ||
| this.Qt.push(e); | ||
| e.Fe = this; | ||
| } | ||
| removeChild(e) { | ||
| const t = this.wt.indexOf(e); | ||
| const t = this.Qt.indexOf(e); | ||
| if (t >= 0) { | ||
| this.wt.splice(t, 1); | ||
| e.He = null; | ||
| this.Qt.splice(t, 1); | ||
| e.Fe = null; | ||
| } | ||
| } | ||
| notify(e, t, i, n) { | ||
| if (this.He) return this.He.notify(e, t, i, n); | ||
| if (this.Fe) return this.Fe.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] = []; | ||
| if (this.Mt[e - 1].length) { | ||
| const t = this.Mt[e - 1]; | ||
| this.Mt[e - 1] = []; | ||
| runQueue(t, e); | ||
| } | ||
| for (let t = 0; t < this.wt.length; t++) this.wt[t].run?.(e); | ||
| for (let t = 0; t < this.Qt.length; t++) this.Qt[t].run?.(e); | ||
| } | ||
@@ -196,3 +223,3 @@ enqueue(e, t) { | ||
| } else { | ||
| this.zt[e - 1].push(t); | ||
| this.Mt[e - 1].push(t); | ||
| } | ||
@@ -203,14 +230,14 @@ } | ||
| 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]; | ||
| e.Mt[0].push(...this.Mt[0]); | ||
| e.Mt[1].push(...this.Mt[1]); | ||
| this.Mt = [ [], [] ]; | ||
| for (let t = 0; t < this.Qt.length; t++) { | ||
| let i = this.Qt[t]; | ||
| let n = e.Qt[t]; | ||
| if (!n) { | ||
| n = { | ||
| zt: [ [], [] ], | ||
| wt: [] | ||
| Mt: [ [], [] ], | ||
| Qt: [] | ||
| }; | ||
| e.wt[t] = n; | ||
| e.Qt[t] = n; | ||
| } | ||
@@ -221,7 +248,7 @@ 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]; | ||
| this.Mt[0].push(...e.Mt[0]); | ||
| this.Mt[1].push(...e.Mt[1]); | ||
| for (let t = 0; t < e.Qt.length; t++) { | ||
| const i = e.Qt[t]; | ||
| let n = this.Qt[t]; | ||
| if (n) n.restoreQueues(i); | ||
@@ -233,12 +260,10 @@ } | ||
| class GlobalQueue extends Queue { | ||
| Bt=false; | ||
| xt=null; | ||
| Yt=[]; | ||
| Be=[]; | ||
| N=[]; | ||
| Lt=new Set; | ||
| Ut=false; | ||
| // The current transaction-shaped batch: a plain ambient batch while no | ||
| // transition is active, the active transition itself after initTransition. | ||
| N=createBatch(); | ||
| static Ce; | ||
| static Oe; | ||
| static ut; | ||
| static Kt=null; | ||
| static Vt=null; | ||
| // Store-side hook: drops a keyless affects() mark's identity scope when the | ||
@@ -269,3 +294,3 @@ // carrier node's last registration releases (wired by store.ts, mirroring | ||
| static _e=null; | ||
| static _=null; | ||
| static P=null; | ||
| static me=null; | ||
@@ -275,6 +300,6 @@ static R=null; | ||
| static Pt=null; | ||
| static vt=null; | ||
| static kt=null; | ||
| static tt=null; | ||
| static nt=null; | ||
| static Zt=null; | ||
| static wt=null; | ||
| // Optimistic-engine hooks (wired by core/optimistic.ts via | ||
@@ -288,18 +313,18 @@ // installOptimisticEngine(), called from verdict.ts / createOptimistic / | ||
| static bt=null; | ||
| static Ut=null; | ||
| static yt=null; | ||
| static Wt=null; | ||
| static jt=null; | ||
| static Mt=null; | ||
| static En=null; | ||
| static Tn=null; | ||
| static In=null; | ||
| static Nn=null; | ||
| static On=null; | ||
| static Dt=null; | ||
| static gt=null; | ||
| static Dt=null; | ||
| static ht=null; | ||
| static kt=null; | ||
| static vt=null; | ||
| static $e=null; | ||
| static et=null; | ||
| static Xe=null; | ||
| static Vt=null; | ||
| static mn=null; | ||
| flush() { | ||
| if (this.Bt) return; | ||
| this.Bt = true; | ||
| if (this.Ut) return; | ||
| this.Ut = true; | ||
| try { | ||
@@ -313,16 +338,14 @@ if (false) ; | ||
| runHeap(zombieQueue, GlobalQueue.Ce); | ||
| this.xt = null; | ||
| this.Yt = []; | ||
| this.Be = []; | ||
| this.N = []; | ||
| this.Lt = new Set; | ||
| // Detach: the stashed transition keeps its batch; ambient work that | ||
| // follows lands in a fresh one. | ||
| currentBatch = this.N = createBatch(); | ||
| // Run lane effects immediately (before stashing) - lanes with no pending async | ||
| if (activeLanes.size) { | ||
| GlobalQueue.Mt(EFFECT_RENDER); | ||
| GlobalQueue.Mt(EFFECT_USER); | ||
| GlobalQueue.On(EFFECT_RENDER); | ||
| GlobalQueue.On(EFFECT_USER); | ||
| } | ||
| this.stashQueues(e.Jt); | ||
| this.stashQueues(e.Bt); | ||
| clock++; | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.Le; | ||
| reassignPendingTransition(e.Yt); | ||
| reassignPendingTransition(e.yt); | ||
| activeTransition = null; | ||
@@ -332,4 +355,4 @@ // The stash pass (committed-view rerun of plain optimistic signals) | ||
| // means _optimisticWrite ran, which installed the hook. | ||
| if (!e.Te.length && !e.qt.size && e.Be.length) { | ||
| GlobalQueue.yt(e); | ||
| if (!e.Te.length && !e.Lt.size && e.Be.length) { | ||
| GlobalQueue.Tn(e); | ||
| } else { | ||
@@ -340,9 +363,21 @@ finalizePureQueue(null, true); | ||
| } | ||
| this.Yt !== activeTransition.Yt && this.Yt.push(...activeTransition.Yt); | ||
| this.restoreQueues(activeTransition.Jt); | ||
| transitions.delete(activeTransition); | ||
| const t = activeTransition; | ||
| const i = this.N; | ||
| i !== t && i.yt.push(...t.yt); | ||
| this.restoreQueues(t.Bt); | ||
| transitions.delete(t); | ||
| activeTransition = null; | ||
| reassignPendingTransition(this.Yt); | ||
| reassignPendingTransition(i.yt); | ||
| finalizePureQueue(t); | ||
| if (i === t) { | ||
| // Drop the dead Transition wrapper but keep its (drained) containers | ||
| // as the ambient batch — late registrations during finalization live | ||
| // there and must survive to the next flush. | ||
| const e = createBatch(); | ||
| e.yt = i.yt; | ||
| e.Be = i.Be; | ||
| e._ = i._; | ||
| e.dn = i.dn; | ||
| currentBatch = this.N = e; | ||
| } | ||
| } else { | ||
@@ -364,5 +399,5 @@ if (canUseSimpleSyncFlush(this)) { | ||
| // Run lane effects first (for ready lanes), then regular effects | ||
| activeLanes.size && GlobalQueue.Mt(EFFECT_RENDER); | ||
| activeLanes.size && GlobalQueue.On(EFFECT_RENDER); | ||
| this.run(EFFECT_RENDER); | ||
| activeLanes.size && GlobalQueue.Mt(EFFECT_USER); | ||
| activeLanes.size && GlobalQueue.On(EFFECT_USER); | ||
| this.run(EFFECT_USER); | ||
@@ -373,3 +408,3 @@ if (false) ; | ||
| } finally { | ||
| this.Bt = false; | ||
| this.Ut = false; | ||
| } | ||
@@ -384,4 +419,4 @@ } | ||
| const i = t.source; | ||
| let n = activeTransition.qt.get(i); | ||
| if (!n) activeTransition.qt.set(i, n = new Set); | ||
| let n = activeTransition.Lt.get(i); | ||
| if (!n) activeTransition.Lt.set(i, n = new Set); | ||
| const s = n.size; | ||
@@ -401,17 +436,3 @@ n.add(e); | ||
| if (!activeTransition) { | ||
| activeTransition = e ?? { | ||
| Ie: clock, | ||
| Yt: [], | ||
| qt: new Map, | ||
| Be: [], | ||
| N: [], | ||
| Lt: new Set, | ||
| Te: [], | ||
| Jt: { | ||
| zt: [ [], [] ], | ||
| wt: [] | ||
| }, | ||
| Ht: false, | ||
| Qt: new Set | ||
| }; | ||
| activeTransition = e ?? createBatch(); | ||
| } else if (e) { | ||
@@ -425,40 +446,28 @@ const t = 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); | ||
| const t = this.N; | ||
| if (t !== activeTransition) { | ||
| // Adopt the ambient batch into the transaction, then make the | ||
| // transaction the batch so later registrations land there directly. | ||
| // Pending and optimistic nodes are re-stamped as the transaction's; | ||
| // 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. | ||
| for (let e = 0; e < t.yt.length; e++) { | ||
| const i = t.yt[e]; | ||
| i.ve = activeTransition; | ||
| activeTransition.yt.push(i); | ||
| } | ||
| 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); | ||
| for (let e = 0; e < t.Be.length; e++) { | ||
| const i = t.Be[e]; | ||
| i.ve = activeTransition; | ||
| activeTransition.Be.push(i); | ||
| } | ||
| this.Be = activeTransition.Be; | ||
| if (t._.length) activeTransition._.push(...t._); | ||
| for (const e of t.dn) activeTransition.dn.add(e); | ||
| currentBatch = this.N = activeTransition; | ||
| } | ||
| 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; | ||
| } | ||
| } | ||
@@ -468,15 +477,3 @@ } | ||
| 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); | ||
| currentBatch.yt.push(e); | ||
| } | ||
@@ -499,19 +496,19 @@ | ||
| const s = reaskArmed; | ||
| for (let a = e.p; a !== null; a = a.de) { | ||
| for (let r = e.p; r !== null; r = r.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; | ||
| if (s) r.Ee.u &= ~REACTIVE_REASK; | ||
| if (n && r.Ee.U & CONFIG_IN_SNAPSHOT_SCOPE) { | ||
| r.Ee.u |= REACTIVE_SNAPSHOT_STALE; | ||
| continue; | ||
| } | ||
| if (t && i) { | ||
| a.Ee.u |= REACTIVE_OPTIMISTIC_DIRTY; | ||
| assignOrMergeLane(a.Ee, i); | ||
| r.Ee.u |= REACTIVE_OPTIMISTIC_DIRTY; | ||
| assignOrMergeLane(r.Ee, i); | ||
| } else if (t) { | ||
| a.Ee.u |= REACTIVE_OPTIMISTIC_DIRTY; | ||
| r.Ee.u |= REACTIVE_OPTIMISTIC_DIRTY; | ||
| // No source lane means reversion - clear subscriber's lane so effects go to regular queue | ||
| a.Ee.Je = undefined; | ||
| r.Ee.Je = undefined; | ||
| } | ||
| enqueueSub(a.Ee); | ||
| enqueueSub(r.Ee); | ||
| } | ||
@@ -543,7 +540,3 @@ } | ||
| function commitPendingNodes() { | ||
| if (globalQueue.xt !== null) { | ||
| commitPendingNode(globalQueue.xt); | ||
| globalQueue.xt = null; | ||
| } | ||
| const e = globalQueue.Yt; | ||
| const e = currentBatch.yt; | ||
| for (let t = 0; t < e.length; t++) { | ||
@@ -560,3 +553,3 @@ commitPendingNode(e[t]); | ||
| if (i) commitPendingNodes(); | ||
| if (!t && globalQueue.wt.length) checkBoundaryChildren(globalQueue); | ||
| if (!t && globalQueue.Qt.length) checkBoundaryChildren(globalQueue); | ||
| const n = dirtyQueue.EE >= dirtyQueue.Le; | ||
@@ -566,14 +559,15 @@ if (n) runHeap(dirtyQueue, GlobalQueue.Ce); | ||
| if (n) commitPendingNodes(); | ||
| // The settling batch: the completing transaction's, or the ambient one. | ||
| const t = e ?? globalQueue.N; | ||
| // 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); | ||
| if (t.Be.length) GlobalQueue.En(t.Be); | ||
| // 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 (e && e.un.size) { | ||
| for (const t of e.un) { | ||
| if (t.u & REACTIVE_DISPOSED) continue; | ||
| enqueueSub(t); | ||
| } | ||
| e.Qt.clear(); | ||
| e.un.clear(); | ||
| } | ||
@@ -583,12 +577,11 @@ // Declared motion ends with the transaction: settle (or plain flush end | ||
| // 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; | ||
| if (t._.length) GlobalQueue.h(t._); | ||
| // 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); | ||
| // core lets esbuild shake it — rollup already folds the null guard). The | ||
| // completing transition scopes the clear to its own layer keys (#2899). | ||
| if (t.dn.size) GlobalQueue.Vt(t.dn, e); | ||
| sweepTransientStoreNodes(); | ||
| // Lanes only enter activeLanes through the engine's getOrCreateLane. | ||
| if (activeLanes.size) GlobalQueue.jt(e); | ||
| if (activeLanes.size) GlobalQueue.Nn(e); | ||
| } | ||
@@ -598,3 +591,3 @@ } | ||
| function checkBoundaryChildren(e) { | ||
| for (const t of e.wt) { | ||
| for (const t of e.Qt) { | ||
| t.Se?.(); | ||
@@ -629,2 +622,9 @@ checkBoundaryChildren(t); | ||
| // Hot-path mirror of `globalQueue._batch`: `queuePendingNode` runs once per | ||
| // staged write and `commitPendingNodes` once per flush, and the extra | ||
| // property hop through `_batch` was a measured instruction-count regression | ||
| // (CodSpeed update1to1, PR #2905). The field stays authoritative for | ||
| // cross-module readers; every `_batch` assignment updates both. | ||
| let currentBatch = globalQueue.N; | ||
| function flush(e) { | ||
@@ -645,3 +645,3 @@ if (e) { | ||
| } | ||
| if (globalQueue.Bt) { | ||
| if (globalQueue.Ut) { | ||
| return; | ||
@@ -663,3 +663,3 @@ } | ||
| if (e.u & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false; | ||
| if (e.m === t || e.M?.has(t)) return true; | ||
| if (e.m?.has(t)) return true; | ||
| for (let i = e.S; i; i = i.st) { | ||
@@ -676,6 +676,6 @@ let e = i.ot; | ||
| function transitionComplete(e) { | ||
| if (e.Ht) return true; | ||
| if (e.cn) return true; | ||
| if (e.Te.length) return false; | ||
| let t = true; | ||
| for (const [i, n] of e.qt) { | ||
| for (const [i, n] of e.Lt) { | ||
| let s = false; | ||
@@ -689,3 +689,3 @@ for (const e of n) { | ||
| } | ||
| if (!s) e.qt.delete(i); else if (i.i & STATUS_PENDING && i.k?.source === i) { | ||
| if (!s) e.Lt.delete(i); else if (i.i & STATUS_PENDING && i.k?.source === i) { | ||
| t = false; | ||
@@ -698,4 +698,4 @@ break; | ||
| // _optimisticNodes, so without the engine the loop was vacuous anyway. | ||
| if (t && e.Be.length && GlobalQueue.Wt(e)) t = false; | ||
| t && (e.Ht = true); | ||
| if (t && e.Be.length && GlobalQueue.In(e)) t = false; | ||
| t && (e.cn = true); | ||
| return t; | ||
@@ -705,3 +705,3 @@ } | ||
| function currentTransition(e) { | ||
| while (e.Ht && typeof e.Ht === "object") e = e.Ht; | ||
| while (e.cn && typeof e.cn === "object") e = e.cn; | ||
| return e; | ||
@@ -708,0 +708,0 @@ } |
@@ -1,4 +0,4 @@ | ||
| import { STATUS_UNINITIALIZED, REACTIVE_DISPOSED, NOT_PENDING, REACTIVE_MANUAL_WRITE, STATUS_PENDING, REACTIVE_DIRTY, REACTIVE_CHECK } from "./constants.js"; | ||
| import { NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_DIRTY, REACTIVE_CHECK, unwrapOverride, STATUS_UNINITIALIZED, STATUS_PENDING, REACTIVE_MANUAL_WRITE } from "./constants.js"; | ||
| import { context, setPendingCheckActive, read, optimisticSignal, setSignal, setLatestReadActive, pendingCheckActive, latestReadActive, stale, currentOptimisticLane, prepareComputed, tracking, setContextInternal, optimisticComputed } from "./core.js"; | ||
| import { setSignal, read, context, stale, currentOptimisticLane, prepareComputed, tracking, setLatestReadActive, setContextInternal, optimisticComputed, setPendingCheckActive, latestReadActive, optimisticSignal, pendingCheckActive } from "./core.js"; | ||
@@ -13,3 +13,3 @@ import { NotReadyError } from "./error.js"; | ||
| import { hasActiveOverride, findLane } from "./lanes.js"; | ||
| import { findLane, hasActiveOverride } from "./lanes.js"; | ||
@@ -65,7 +65,6 @@ import { installOptimisticEngine } from "./optimistic.js"; | ||
| function quietPending(e) { | ||
| if (e.M) { | ||
| for (const t of e.M) if (!t.A) return false; | ||
| 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; | ||
@@ -81,7 +80,7 @@ } | ||
| if (t.u & REACTIVE_DISPOSED) return false; | ||
| if (e.P) return true; | ||
| if (e.M) return true; | ||
| const n = e.rt; | ||
| if (e.en) { | ||
| const t = e.en; | ||
| if (t.P) return true; | ||
| if (t.M) return true; | ||
| const n = t.rt || t; | ||
@@ -94,3 +93,3 @@ return newQuestionInFlight(n); | ||
| if (e.ge !== NOT_PENDING && !(t.i & STATUS_UNINITIALIZED)) { | ||
| if (hasActiveOverride(e)) return !e.Ge || !e.Ge(e.ge, e.be); | ||
| if (hasActiveOverride(e)) return !e.Ge || !e.Ge(e.ge, unwrapOverride(e.be)); | ||
| return true; | ||
@@ -180,3 +179,3 @@ } | ||
| setLatestReadActive(false); | ||
| const i = e.be !== undefined && e.be !== NOT_PENDING ? e.be : e.Ue; | ||
| const i = e.be !== undefined && e.be !== NOT_PENDING ? unwrapOverride(e.be) : e.Ue; | ||
| let r; | ||
@@ -280,3 +279,3 @@ try { | ||
| GlobalQueue._ = updatePendingSignal; | ||
| GlobalQueue.P = updatePendingSignal; | ||
@@ -291,3 +290,3 @@ GlobalQueue.me = updateChildCompanions; | ||
| GlobalQueue.vt = recordFreshRead; | ||
| GlobalQueue.kt = recordFreshRead; | ||
@@ -298,4 +297,4 @@ GlobalQueue.tt = applyReask; | ||
| GlobalQueue.Zt = witnessAffects; | ||
| GlobalQueue.wt = witnessAffects; | ||
| export { isPending, latest }; |
+196
-167
@@ -21,22 +21,22 @@ import { computed, runWithOwner, signal, setSignal } from "./core/core.js"; | ||
| 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 r = s.length > 1; | ||
| const n = s; | ||
| const h = { | ||
| jt: createOwner(), | ||
| Wt: 0, | ||
| Kt: t, | ||
| xt: [], | ||
| $t: n, | ||
| qt: [], | ||
| zt: [], | ||
| Ht: e, | ||
| Jt: e || i?.keyed === false ? [] : undefined, | ||
| Xt: r && i?.keyed !== false ? [] : undefined, | ||
| Yt: i?.keyed === false, | ||
| Zt: i?.fallback | ||
| }; | ||
| const o = computed(updateKeyedMap.bind(n)); | ||
| const o = computed(updateKeyedMap.bind(h)); | ||
| // 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; | ||
| h.jt.Ct = o; | ||
| o.U &= ~CONFIG_AUTO_DISPOSE; | ||
@@ -50,110 +50,145 @@ return accessor(o); | ||
| // Exception safety (#2903): a map callback can throw NotReadyError mid-pass | ||
| // (async read), and the computed re-runs the whole pass after settle. Every | ||
| // pass therefore STAGES its work — new rows are created into temp arrays and | ||
| // removals are deferred — and commits to `this` only after every mapper | ||
| // succeeded. An aborted pass disposes just the owners it created and leaves | ||
| // `_items`/`_mappings`/`_nodes`/`_rows`/`_indexes`/`_len` exactly as they | ||
| // were, so the retry diffs against uncorrupted state. Consequence of the | ||
| // strong-abort ordering: removed rows now dispose AFTER the pass's new rows | ||
| // are created (you cannot destroy state before knowing the pass will land). | ||
| function updateKeyedMap() { | ||
| const t = this.ts() || [], s = t.length; | ||
| const t = this.Kt() || [], 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); | ||
| runWithOwner(this.jt, () => { | ||
| let i, e, r, n, | ||
| // Mappers write freshly-created row/index signals into the STAGE | ||
| // arrays (`rows`/`indexes`), never into `this._rows`/`this._indexes`. | ||
| h = this.Jt ? this.Yt ? () => { | ||
| r[e] = signal(t[e], pureOptions); | ||
| return this.$t(accessor(r[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 ? () => { | ||
| r[e] = signal(t[e], pureOptions); | ||
| n && (n[e] = signal(e, pureOptions)); | ||
| return this.$t(accessor(r[e]), n ? accessor(n[e]) : undefined); | ||
| } : this.Xt ? () => { | ||
| const s = t[e]; | ||
| this.fs[e] = signal(e, pureOptions); | ||
| return this.es(s, accessor(this.fs[e])); | ||
| n[e] = signal(e, pureOptions); | ||
| return this.$t(s, accessor(n[e])); | ||
| } : () => { | ||
| const s = t[e]; | ||
| return this.es(s); | ||
| return this.$t(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.Wt !== 0) { | ||
| this.jt.dispose(false); | ||
| this.zt = []; | ||
| this.xt = []; | ||
| this.qt = []; | ||
| this.Wt = 0; | ||
| this.Jt && (this.Jt = []); | ||
| this.Xt && (this.Xt = []); | ||
| } | ||
| if (this.ps && !this.hs[0]) { | ||
| // create fallback | ||
| this.hs[0] = runWithOwner(this.rs[0] = createOwner(), this.ps); | ||
| if (this.Zt && !this.qt[0]) { | ||
| // an aborted fallback attempt leaves an owner without a mapping; | ||
| // dispose it before re-creating | ||
| this.zt[0]?.dispose(); | ||
| this.qt[0] = runWithOwner(this.zt[0] = createOwner(), this.Zt); | ||
| } | ||
| } | ||
| // 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); | ||
| else if (this.Wt === 0) { | ||
| const o = new Array(s); | ||
| const c = new Array(s); | ||
| r = this.Jt && new Array(s); | ||
| n = this.Xt && new Array(s); | ||
| try { | ||
| for (e = 0; e < s; e++) o[e] = runWithOwner(c[e] = createOwner(), h); | ||
| } catch (t) { | ||
| for (i = 0; i <= e; i++) c[i]?.dispose(); | ||
| throw t; | ||
| } | ||
| this.Xt = s; | ||
| // commit | ||
| if (this.zt[0]) this.zt[0].dispose(); | ||
| // previous fallback | ||
| this.qt = o; | ||
| this.zt = c; | ||
| r && (this.Jt = r); | ||
| n && (this.Xt = n); | ||
| this.xt = t.slice(0); | ||
| this.Wt = 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; | ||
| let o, c, a, f, u, p, w, l, d, O = new Array(s), m = new Array(s); | ||
| r = this.Jt ? new Array(s) : undefined; | ||
| n = this.Xt ? 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]); | ||
| for (o = 0, c = Math.min(this.Wt, s); o < c && (this.xt[o] === t[o] || this.Jt && compare(this.Ht, this.xt[o], t[o])); o++) { | ||
| if (this.Jt) setSignal(this.Jt[o], t[o]); | ||
| } | ||
| // 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]); | ||
| for (c = this.Wt - 1, a = s - 1; c >= o && a >= o && (this.xt[c] === t[a] || this.Jt && compare(this.Ht, this.xt[c], t[a])); c--, | ||
| a--) { | ||
| O[a] = this.qt[c]; | ||
| m[a] = this.zt[c]; | ||
| r && (r[a] = this.Jt[c]); | ||
| n && (n[a] = this.Xt[c]); | ||
| } | ||
| // 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); | ||
| p = new Map; | ||
| w = new Array(a + 1); | ||
| for (e = a; e >= o; e--) { | ||
| f = t[e]; | ||
| u = this.Ht ? this.Ht(f) : f; | ||
| i = p.get(u); | ||
| w[e] = i === undefined ? -1 : i; | ||
| p.set(u, 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); | ||
| // 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, queue them for disposal at commit | ||
| for (i = o; i <= c; i++) { | ||
| f = this.xt[i]; | ||
| u = this.Ht ? this.Ht(f) : f; | ||
| e = p.get(u); | ||
| 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(); | ||
| O[e] = this.qt[i]; | ||
| m[e] = this.zt[i]; | ||
| r && (r[e] = this.Jt[i]); | ||
| n && (n[e] = this.Xt[i]); | ||
| e = w[e]; | ||
| p.set(u, e); | ||
| } else (l ??= []).push(this.zt[i]); | ||
| } | ||
| // 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); | ||
| // 2) create new rows into the temp arrays; an abort disposes only these | ||
| try { | ||
| for (e = o; e < s; e++) { | ||
| if (e in O) continue; | ||
| (d ??= []).push(m[e] = createOwner()); | ||
| O[e] = runWithOwner(m[e], h); | ||
| } | ||
| } catch (t) { | ||
| if (d) for (i = 0; i < d.length; i++) d[i].dispose(); | ||
| throw t; | ||
| } | ||
| // 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); | ||
| // 3) commit: land positions, then dispose exited rows | ||
| for (e = o; e < s; e++) { | ||
| this.qt[e] = O[e]; | ||
| this.zt[e] = m[e]; | ||
| if (r) { | ||
| this.Jt[e] = r[e]; | ||
| setSignal(this.Jt[e], t[e]); | ||
| } | ||
| if (n) { | ||
| this.Xt[e] = n[e]; | ||
| setSignal(this.Xt[e], e); | ||
| } | ||
| } | ||
| if (l) for (i = 0; i < l.length; i++) l[i].dispose(); | ||
| // 4) in case the new set is shorter than the old, set the length of the mapped array | ||
| this.qt = this.qt.slice(0, this.Wt = s); | ||
| // 5) save a copy of the mapped items for the next update | ||
| this.xt = t.slice(0); | ||
| } | ||
| }); | ||
| return this.hs; | ||
| return this.qt; | ||
| } | ||
@@ -177,36 +212,47 @@ | ||
| 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); | ||
| const r = { | ||
| jt: createOwner(), | ||
| Wt: 0, | ||
| ts: 0, | ||
| ss: t, | ||
| $t: e, | ||
| zt: [], | ||
| qt: [], | ||
| es: i?.from, | ||
| Zt: i?.fallback | ||
| }; | ||
| const n = computed(updateRepeat.bind(r)); | ||
| // Same as mapArray: untracked reads inside the internal owner resolve via | ||
| // _parentComputed, so async reads in row callbacks register with the node | ||
| // (pending tracking + post-settle retry) instead of vanishing. | ||
| r.jt.Ct = n; | ||
| n.U &= ~CONFIG_AUTO_DISPOSE; | ||
| return accessor(n); | ||
| } | ||
| // Same staged-commit discipline as `updateKeyedMap` (#2903): the retained | ||
| // window overlap is copied into fresh arrays, missing indexes are created | ||
| // into them, and `this` is only touched — including disposal of rows leaving | ||
| // the window — after every `_map` call succeeded. A NotReadyError mid-pass | ||
| // disposes only the owners this pass created and leaves prior state intact | ||
| // for the post-settle retry. The overlap math also subsumes the previous | ||
| // disjoint-window/front-clear/end-clear/shift special cases. | ||
| function updateRepeat() { | ||
| const t = this.ws(); | ||
| const s = this.Os?.() || 0; | ||
| runWithOwner(this.$t, () => { | ||
| const t = this.ss(); | ||
| const s = this.es?.() || 0; | ||
| runWithOwner(this.jt, () => { | ||
| 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.Wt !== 0) { | ||
| this.jt.dispose(false); | ||
| this.zt = []; | ||
| this.qt = []; | ||
| this.Wt = 0; | ||
| // Reset offset to match the cleared data (#2767, repro 2). | ||
| this.ts = 0; | ||
| } | ||
| if (this.ps && !this.hs[0]) { | ||
| // create fallback | ||
| this.hs[0] = runWithOwner(this.rs[0] = createOwner(), this.ps); | ||
| if (this.Zt && !this.qt[0]) { | ||
| // an aborted fallback attempt leaves an owner without a mapping; | ||
| // dispose it before re-creating | ||
| this.zt[0]?.dispose(); | ||
| this.qt[0] = runWithOwner(this.zt[0] = createOwner(), this.Zt); | ||
| } | ||
@@ -216,47 +262,30 @@ 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; | ||
| const e = this.ts + this.Wt; | ||
| // Retained overlap [keepStart, keepEnd) in global indexes; empty when the | ||
| // windows are disjoint or when coming from empty/fallback. | ||
| const r = Math.max(s, this.ts); | ||
| const n = Math.min(i, e); | ||
| const h = new Array(t); | ||
| const o = new Array(t); | ||
| for (let t = r; t < n; t++) { | ||
| o[t - s] = this.zt[t - this.ts]; | ||
| h[t - s] = this.qt[t - this.ts]; | ||
| } | ||
| // 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--; | ||
| try { | ||
| for (let t = s; t < i; t++) { | ||
| if (t >= r && t < n) continue; | ||
| h[t - s] = runWithOwner(o[t - s] = createOwner(), () => this.$t(t)); | ||
| } | ||
| for (let t = 0; t < h; t++) { | ||
| this.hs[t] = runWithOwner(this.rs[t] = createOwner(), () => this.es(t + s)); | ||
| } | ||
| } catch (t) { | ||
| for (let t = s; t < i; t++) if ((t < r || t >= n) && o[t - s]) o[t - s].dispose(); | ||
| throw t; | ||
| } | ||
| 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; | ||
| // commit: dispose the previous fallback or the rows leaving the window | ||
| if (this.Wt === 0) this.zt[0]?.dispose(); else for (let t = this.ts; t < e; t++) if (t < s || t >= i) this.zt[t - this.ts].dispose(); | ||
| this.qt = h; | ||
| this.zt = o; | ||
| this.ts = s; | ||
| this.Wt = t; | ||
| }); | ||
| return this.hs; | ||
| return this.qt; | ||
| } | ||
@@ -263,0 +292,0 @@ |
@@ -7,3 +7,3 @@ import { computed, optimisticComputed, setSignal, optimisticSignal, runWithOwner, setMemo, signal, read, untrack } from "./core/core.js"; | ||
| import { CONFIG_CHILDREN_FORBIDDEN, EFFECT_USER, $REFRESH, CONFIG_AUTO_DISPOSE } from "./core/constants.js"; | ||
| import { CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, EFFECT_USER, $REFRESH } from "./core/constants.js"; | ||
@@ -10,0 +10,0 @@ import "./core/invariants.js"; |
@@ -5,3 +5,3 @@ import { computed } from "../core/core.js"; | ||
| import { GlobalQueue, schedule, setProjectionWriteActive, insertSubs, projectionWriteActive } from "../core/scheduler.js"; | ||
| import { GlobalQueue, schedule, currentTransition, insertSubs, setProjectionWriteActive, projectionWriteActive } from "../core/scheduler.js"; | ||
@@ -18,5 +18,5 @@ import "../core/invariants.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"; | ||
| import { storeSetter, $TARGET, STORE_OPTIMISTIC_OVERRIDE, STORE_NODE, STORE_OPTIMISTIC_OWNERS, 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) { | ||
| function createOptimisticStore(e, t, r) { | ||
| // Register clear function with scheduler; store nodes marked | ||
@@ -26,9 +26,9 @@ // STORE_OPTIMISTIC take the engine's write path, so install it before any | ||
| installOptimisticEngine(); | ||
| GlobalQueue.Kt ||= clearOptimisticStores; | ||
| const i = typeof t === "function"; | ||
| const o = i ? e : t; | ||
| const c = i ? t : undefined; | ||
| GlobalQueue.Vt ||= clearOptimisticStores; | ||
| const i = typeof e === "function"; | ||
| const o = i ? t : e; | ||
| const n = i ? e : undefined; | ||
| // Create optimistic projection store | ||
| const {store: n} = createOptimisticProjectionInternal(c, o, r); | ||
| return [ n, t => storeSetter(n, t) ]; | ||
| const {store: c} = createOptimisticProjectionInternal(n, o, r); | ||
| return [ c, e => storeSetter(c, e) ]; | ||
| } | ||
@@ -38,65 +38,100 @@ | ||
| // 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); | ||
| // scheduler's flush tail carries only a size-guarded hook call. The | ||
| // completing transition scopes each clear to its own layer keys (#2899). | ||
| function clearOptimisticStores(e, t) { | ||
| for (const r of e) { | ||
| const e = r[$TARGET]; | ||
| if (e?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(e, t); | ||
| } | ||
| t.clear(); | ||
| e.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 | ||
| /** | ||
| * Consume optimistic layer entries and reset their backing nodes to base. | ||
| * With `completing` (settle path, #2899) only entries the settling | ||
| * transaction owns are consumed — the layer is store-wide but concurrent | ||
| * actions revert independently, so keys stamped by a still-in-flight | ||
| * transition survive (node-level overrides already have this granularity via | ||
| * _optimisticNodes; this is the layer's half). `null` consumes ambient | ||
| * (transaction-less) entries at plain flush end. Omitted (projection landing: | ||
| * fresh authoritative data) consumes everything — the correction supersedes | ||
| * every tentative layer. | ||
| */ function clearOptimisticOverride(e, t) { | ||
| const r = e[STORE_OPTIMISTIC_OVERRIDE]; | ||
| if (!r) return; | ||
| const i = e[STORE_NODE]; | ||
| const o = e[STORE_OPTIMISTIC_OWNERS]; | ||
| const n = t !== undefined; | ||
| let c = false; | ||
| let s = false; | ||
| // Use projectionWriteActive to bypass optimistic signal behavior (no lane creation) | ||
| // This ensures reversion effects go to regular queues, not lane queues | ||
| const i = projectionWriteActive; | ||
| const O = 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(); | ||
| for (const O of Reflect.ownKeys(r)) { | ||
| if (n) { | ||
| let e = o?.[O] ?? null; | ||
| // Resolve merge chains (entangled actions settle as one); path-compress | ||
| // so later keys skip the walk. A dead owner (`_done === true`) settled | ||
| // through some other path — never strand its entry. A null owner is an | ||
| // ambient write: its batch belongs to whichever transaction adopted it | ||
| // (initTransition mid-batch) or to the plain flush, so it clears on | ||
| // whichever clear call reaches this store first. | ||
| if (e) { | ||
| if (typeof e.cn === "object") e = o[O] = currentTransition(e); | ||
| if (e !== t && e.cn !== true) { | ||
| s = true; | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
| // Notify $TRACK | ||
| if (r[$TRACK]) { | ||
| r[$TRACK].Je = undefined; | ||
| notifySelf(t); | ||
| delete r[O]; | ||
| if (o) delete o[O]; | ||
| c = true; | ||
| const T = i?.[O]; | ||
| if (T) { | ||
| // Clear lane association so effects go to regular queue | ||
| T.Je = undefined; | ||
| // Re-read from base — this key left the optimistic layer above, so the | ||
| // overlay resolves to STORE_OVERRIDE or STORE_VALUE. | ||
| const t = getOverlayLayer(e, O); | ||
| const r = t ? t[O] : e[STORE_VALUE][O]; | ||
| const i = r === $DELETED ? undefined : r; | ||
| const o = isWrappable(i) ? wrap(i, e) : i; | ||
| const n = visibleNodeValue(T); | ||
| T.be = NOT_PENDING; | ||
| T.fn = null; | ||
| T.ge = NOT_PENDING; | ||
| T.Ue = o; | ||
| if (!T.Ge || !T.Ge(n, o)) { | ||
| insertSubs(T, true); | ||
| schedule(); | ||
| } | ||
| } | ||
| } | ||
| if (!s) { | ||
| delete e[STORE_OPTIMISTIC_OVERRIDE]; | ||
| delete e[STORE_OPTIMISTIC_OWNERS]; | ||
| } | ||
| // Notify $TRACK | ||
| if (c && i?.[$TRACK]) { | ||
| i[$TRACK].Je = undefined; | ||
| notifySelf(e); | ||
| } | ||
| } finally { | ||
| setProjectionWriteActive(i); | ||
| setProjectionWriteActive(O); | ||
| } | ||
| } | ||
| function createOptimisticProjectionInternal(t, e, r) { | ||
| function createOptimisticProjectionInternal(e, t, r) { | ||
| let i; | ||
| const o = new WeakMap; | ||
| const wrapper = t => { | ||
| t[STORE_WRAP] = wrapProjection; | ||
| t[STORE_LOOKUP] = o; | ||
| t[STORE_OPTIMISTIC] = true; | ||
| const wrapper = e => { | ||
| e[STORE_WRAP] = wrapProjection; | ||
| e[STORE_LOOKUP] = o; | ||
| e[STORE_OPTIMISTIC] = true; | ||
| // Mark as optimistic store | ||
| Object.defineProperty(t, STORE_FIREWALL, { | ||
| Object.defineProperty(e, STORE_FIREWALL, { | ||
| get() { | ||
@@ -108,12 +143,12 @@ return i; | ||
| }; | ||
| 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 wrapProjection = e => { | ||
| if (o.has(e)) return o.get(e); | ||
| if (e[$TARGET]?.[STORE_WRAP] === wrapProjection) return e; | ||
| const t = createStoreProxy(e, storeTraps, wrapper); | ||
| o.set(e, t); | ||
| return t; | ||
| }; | ||
| const c = wrapProjection(e); | ||
| const n = wrapProjection(t); | ||
| // If there's a projection function, create a computed to drive it | ||
| if (t) { | ||
| if (e) { | ||
| // All writes inside firewall recompute must go to STORE_OVERRIDE (base), not | ||
@@ -125,13 +160,13 @@ // STORE_OPTIMISTIC_OVERRIDE. The outer wrap covers the sync body (including | ||
| const clearProjectionOverride = () => { | ||
| const t = c[$TARGET]; | ||
| if (t?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(t); | ||
| const e = n[$TARGET]; | ||
| if (e?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(e); | ||
| }; | ||
| const wrapCommit = t => { | ||
| const e = projectionWriteActive; | ||
| const wrapCommit = e => { | ||
| const t = projectionWriteActive; | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| t(); | ||
| e(); | ||
| clearProjectionOverride(); | ||
| } finally { | ||
| setProjectionWriteActive(e); | ||
| setProjectionWriteActive(t); | ||
| } | ||
@@ -142,3 +177,3 @@ }; | ||
| try { | ||
| runProjectionComputed(c, t, r?.key || "id", wrapCommit, clearProjectionOverride); | ||
| runProjectionComputed(n, e, r?.key || "id", wrapCommit, clearProjectionOverride); | ||
| } finally { | ||
@@ -151,3 +186,3 @@ setProjectionWriteActive(false); | ||
| return { | ||
| store: c, | ||
| store: n, | ||
| node: i | ||
@@ -154,0 +189,0 @@ }; |
@@ -11,3 +11,3 @@ import { setSignal, untrack } from "../core/core.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"; | ||
| import { $TARGET, STORE_OVERRIDE, STORE_OPTIMISTIC_OVERRIDE, STORE_VALUE, STORE_NODE, storeLookup, STORE_LOOKUP, $PROXY, isWrappable, wrap, notifySelf, $TRACK, STORE_DESC, STORE_HAS, getKeys, $DELETED, symbolKeyedRecords, getStoreSymbols } from "./store.js"; | ||
@@ -49,2 +49,4 @@ // Enumerate a node record's keys. Keeps the common string-key path on the | ||
| function getAllKeys(e, t, r) { | ||
| // Symbols are merged explicitly below; keep the common string-key path on | ||
| // Object.keys() and avoid reflecting the base symbols twice. | ||
| const n = getKeys(e, t); | ||
@@ -136,2 +138,30 @@ const a = Object.keys(r); | ||
| /** | ||
| * The captured-proxy half of the object diff (#2902): descend into keyed- | ||
| * matching children that have NO node at this level but shelter subscribers | ||
| * somewhere below (their target's sticky `STORE_DESC` flag, bubbled up by | ||
| * `getNode`). Without this, a proxy captured through untracked reads — a | ||
| * `<For>` row handed to a child component — detaches from the diff the | ||
| * moment no intermediate level happens to be tracked, and its live | ||
| * subscribers go permanently stale. Never-subscribed branches have no flag | ||
| * and stay pruned exactly as before; keys the main loop already visited | ||
| * (node present) are skipped. Callers gate on the parent's own flag and on | ||
| * `$TRACK` absence (an enumeration-tracked record already diffs every key). | ||
| */ function applyDescendants(e, t, r, n, a, i, o) { | ||
| const l = r[STORE_LOOKUP] || storeLookup; | ||
| const s = (i ? getKeys(e, i) : Object.keys(e)).concat(getStoreSymbols(e, i)); | ||
| for (let p = 0, f = s.length; p < f; p++) { | ||
| const f = s[p]; | ||
| if (n?.[f]) continue; | ||
| // main loop already diffed this slot | ||
| const c = unwrap(i ? getOverrideValue(e, i, f, o) : e[f]); | ||
| if (!isWrappable(c)) continue; | ||
| const u = (l.get(c) ?? storeLookup.get(c))?.[$TARGET]; | ||
| if (!u?.[STORE_DESC]) continue; | ||
| const S = unwrap(t[f]); | ||
| if (c === S || !isWrappable(S) || Array.isArray(c) !== Array.isArray(S) || a(c) != null && a(c) !== a(S)) continue; | ||
| applyState(S, wrap(c, r), a); | ||
| } | ||
| } | ||
| // Dispatcher: every applyState call (including recursion) checks for the | ||
@@ -167,3 +197,3 @@ // presence of override / optimistic-override slots once and routes to the | ||
| if (e.length && o && isWrappable(e[0]) && r(e[0]) != null) { | ||
| let l, s, p, f, c, u, y, S; | ||
| let l, s, p, f, c, u, S, y; | ||
| for (p = 0, f = Math.min(o, e.length); p < f && keyedMatch(u = n[p], e[p], r); p++) { | ||
@@ -191,18 +221,18 @@ isWrappable(u) && isWrappable(e[p]) && applyState(e[p], wrap(u, t), r); | ||
| } | ||
| y = new Array(c + 1); | ||
| S = 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); | ||
| y = itemKey(u, r); | ||
| l = O.get(y); | ||
| S[s] = l === undefined ? -1 : l; | ||
| O.set(y, s); | ||
| } | ||
| for (l = p; l <= f; l++) { | ||
| u = n[l]; | ||
| S = itemKey(u, r); | ||
| s = O.get(S); | ||
| y = itemKey(u, r); | ||
| s = O.get(y); | ||
| if (s !== undefined && s !== -1) { | ||
| E[s] = u; | ||
| s = y[s]; | ||
| O.set(S, s); | ||
| s = S[s]; | ||
| O.set(y, s); | ||
| } | ||
@@ -235,7 +265,8 @@ } | ||
| let i = t[STORE_NODE]; | ||
| let o; | ||
| 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]; | ||
| o = i[$TRACK]; | ||
| const a = o ? getAllKeys(n, undefined, e) : nodeKeys(i); | ||
| for (let l = 0, s = a.length; l < s; l++) { | ||
| const s = a[l]; | ||
| const p = i[s]; | ||
@@ -246,3 +277,3 @@ const f = unwrap(n[s]); | ||
| if (!f || !isWrappable(f) || !isWrappable(c) || Array.isArray(f) !== Array.isArray(c) || r(f) != null && r(f) !== r(c)) { | ||
| a && setSignal(a, void 0); | ||
| o && setSignal(o, void 0); | ||
| p && setSignal(p, isWrappable(c) ? wrap(c, t) : c); | ||
@@ -252,2 +283,3 @@ } else applyState(c, wrap(f, t), r); | ||
| } | ||
| if (!o && t[STORE_DESC]) applyDescendants(n, e, t, i, r); | ||
| // has | ||
@@ -277,13 +309,13 @@ if (i = t[STORE_HAS]) { | ||
| 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); | ||
| let p, f, c, u, S, y, E, O; | ||
| for (c = 0, u = Math.min(s, e.length); c < u && keyedMatch(y = getOverrideValue(n, a, c, i), e[c], r); c++) { | ||
| isWrappable(y) && isWrappable(e[c]) && applyState(e[c], wrap(y, 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; | ||
| const R = new Array(e.length), d = new Map; | ||
| for (u = s - 1, S = e.length - 1; u >= c && S >= c && keyedMatch(y = getOverrideValue(n, a, u, i), e[S], r); u--, | ||
| S--) { | ||
| R[S] = y; | ||
| } | ||
| if (c > y || c > u) { | ||
| for (f = c; f <= y; f++) { | ||
| if (c > S || c > u) { | ||
| for (f = c; f <= S; f++) { | ||
| l = true; | ||
@@ -294,3 +326,3 @@ o?.[f] && setSignal(o[f], wrapValue(e[f], t)); | ||
| l = true; | ||
| applyArrayItem(e[f], d[f], t, o?.[f], r); | ||
| applyArrayItem(e[f], R[f], t, o?.[f], r); | ||
| } | ||
@@ -303,23 +335,23 @@ const n = e.length; | ||
| } | ||
| E = new Array(y + 1); | ||
| for (f = y; f >= c; f--) { | ||
| S = e[f]; | ||
| O = itemKey(S, r); | ||
| p = R.get(O); | ||
| E = new Array(S + 1); | ||
| for (f = S; f >= c; f--) { | ||
| y = e[f]; | ||
| O = itemKey(y, r); | ||
| p = d.get(O); | ||
| E[f] = p === undefined ? -1 : p; | ||
| R.set(O, f); | ||
| d.set(O, f); | ||
| } | ||
| for (p = c; p <= u; p++) { | ||
| S = getOverrideValue(n, a, p, i); | ||
| O = itemKey(S, r); | ||
| f = R.get(O); | ||
| y = getOverrideValue(n, a, p, i); | ||
| O = itemKey(y, r); | ||
| f = d.get(O); | ||
| if (f !== undefined && f !== -1) { | ||
| d[f] = S; | ||
| R[f] = y; | ||
| f = E[f]; | ||
| R.set(O, f); | ||
| d.set(O, f); | ||
| } | ||
| } | ||
| for (f = c; f < e.length; f++) { | ||
| if (f in d) { | ||
| applyArrayItem(e[f], d[f], t, o?.[f], r); | ||
| if (f in R) { | ||
| applyArrayItem(e[f], R[f], t, o?.[f], r); | ||
| } else o?.[f] && setSignal(o[f], wrapValue(e[f], t)); | ||
@@ -347,4 +379,5 @@ } | ||
| // values | ||
| if (o) { | ||
| const l = o[$TRACK]; | ||
| let l; | ||
| if (o) { | ||
| l = o[$TRACK]; | ||
| const s = l ? getAllKeys(n, a, e) : nodeKeys(o); | ||
@@ -355,10 +388,11 @@ for (let p = 0, f = s.length; p < f; p++) { | ||
| 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)) { | ||
| let S = unwrap(e[f]); | ||
| if (u === S) continue; | ||
| if (!u || !isWrappable(u) || !isWrappable(S) || Array.isArray(u) !== Array.isArray(S) || r(u) != null && r(u) !== r(S)) { | ||
| l && setSignal(l, void 0); | ||
| c && setSignal(c, isWrappable(y) ? wrap(y, t) : y); | ||
| } else applyState(y, wrap(u, t), r); | ||
| c && setSignal(c, isWrappable(S) ? wrap(S, t) : S); | ||
| } else applyState(S, wrap(u, t), r); | ||
| } | ||
| } | ||
| if (!l && t[STORE_DESC]) applyDescendants(n, e, t, o, r, a, i); | ||
| // has | ||
@@ -365,0 +399,0 @@ if (o = t[STORE_HAS]) { |
+204
-73
@@ -1,2 +0,2 @@ | ||
| import { $REFRESH, STORE_SNAPSHOT_PROPS, NOT_PENDING, STATUS_PENDING, NO_SNAPSHOT } from "../core/constants.js"; | ||
| import { $REFRESH, STORE_SNAPSHOT_PROPS, NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, STATUS_PENDING, NO_SNAPSHOT } from "../core/constants.js"; | ||
@@ -7,5 +7,7 @@ import { suppressComputedRecompute, isEqual, signal, pendingCheckActive, untrack, setSignal, read, snapshotCaptureActive, snapshotSources } from "../core/core.js"; | ||
| import { NotReadyError } from "../core/error.js"; | ||
| import { getObserver } from "../core/owner.js"; | ||
| import { GlobalQueue, registerTransientStoreNode, projectionWriteActive, globalQueue } from "../core/scheduler.js"; | ||
| import { GlobalQueue, registerTransientStoreNode, projectionWriteActive, activeTransition, globalQueue } from "../core/scheduler.js"; | ||
@@ -30,3 +32,3 @@ import "../core/invariants.js"; | ||
| 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_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", STORE_OPTIMISTIC_OWNERS = "t", STORE_PARENT = "u", STORE_DESC = "d"; | ||
@@ -61,5 +63,13 @@ const STORE_SELF_PENDING = Symbol(0); | ||
| function wrap(e, t) { | ||
| if (t?.[STORE_WRAP]) return t[STORE_WRAP](e, t); | ||
| if (t?.[STORE_WRAP]) { | ||
| const r = t[STORE_WRAP](e, t); | ||
| const n = r[$TARGET]; | ||
| if (n && !n[STORE_PARENT] && n !== t) n[STORE_PARENT] = t; | ||
| return r; | ||
| } | ||
| let r = e[$PROXY] || storeLookup.get(e); | ||
| if (!r) storeLookup.set(e, r = createStoreProxy(e)); | ||
| if (!r) { | ||
| storeLookup.set(e, r = createStoreProxy(e)); | ||
| if (t) r[$TARGET][STORE_PARENT] = t; | ||
| } | ||
| return r; | ||
@@ -97,3 +107,3 @@ } | ||
| r = n[STORE_LOOKUP] ?? storeLookup; | ||
| for (const e of getKeys(i, o)) { | ||
| for (const e of getStoreKeys(i, o)) { | ||
| if (s && e === "length") continue; | ||
@@ -116,2 +126,19 @@ const n = e in o ? o[e] : i[e]; | ||
| function ownEnumerableSymbols(e) { | ||
| const t = Object.getOwnPropertySymbols(e); | ||
| const r = []; | ||
| for (let n = 0, o = t.length; n < o; n++) { | ||
| const o = t[n]; | ||
| if (Object.prototype.propertyIsEnumerable.call(e, o)) r.push(o); | ||
| } | ||
| return r; | ||
| } | ||
| // Plain-object variant that keeps Object.keys() as the fast path and only pays | ||
| // descriptor checks for symbols. Do not use this on store proxies: splitting | ||
| // strings/symbols would invoke their ownKeys trap twice. | ||
| function ownEnumerableKeysPlain(e) { | ||
| return Object.keys(e).concat(ownEnumerableSymbols(e)); | ||
| } | ||
| /** | ||
@@ -135,3 +162,3 @@ * Single chokepoint for the store's layered value resolution: returns the | ||
| */ function visibleNodeValue(e) { | ||
| return e.be !== undefined && e.be !== NOT_PENDING ? e.be : e.ge !== NOT_PENDING ? e.ge : e.Ue; | ||
| return e.be !== undefined && e.be !== NOT_PENDING ? unwrapOverride(e.be) : e.ge !== NOT_PENDING ? e.ge : e.Ue; | ||
| } | ||
@@ -195,6 +222,15 @@ | ||
| 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 | ||
| // A node born inside a live 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]); | ||
| if (r !== $AFFECTS && affectsScopes.size) inheritAffectsMarks(s, e[STORE_VALUE], r); | ||
| // Node presence bubbles up the wrap chain (sticky), so reconcile can see | ||
| // "subscribers live somewhere below" through node-less intermediate | ||
| // records — the captured-proxy diff gate (#2902). Amortized O(1): stops at | ||
| // the first already-flagged ancestor. | ||
| let E = e; | ||
| while (E && !E[STORE_DESC]) { | ||
| E[STORE_DESC] = true; | ||
| E = E[STORE_PARENT]; | ||
| } | ||
| return t[r] = s; | ||
@@ -204,12 +240,13 @@ } | ||
| /** | ||
| * 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) { | ||
| * Scope inheritance for late-created nodes: every live mark whose identity | ||
| * scope contains the owning record's raw — and, for keyed marks, whose key | ||
| * is this property — 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, r) { | ||
| // 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)) { | ||
| for (const [n, o] of affectsScopes) { | ||
| if (n.M && o.scope.has(t) && (o.key === undefined || o.key === r)) { | ||
| GlobalQueue.D(e); | ||
| n.inherited.push(e); | ||
| o.inherited.push(e); | ||
| } | ||
@@ -246,3 +283,6 @@ } | ||
| E = mergedOverlay(i); | ||
| n = i[STORE_LOOKUP] ?? n; | ||
| // Carry the effective lookup into untouched descendants. Default stores | ||
| // use the global lookup just like snapshotImpl; without it, nested raw | ||
| // objects fall back to string-only enumeration and symbol branches vanish. | ||
| n = i[STORE_LOOKUP] ?? n ?? storeLookup; | ||
| } | ||
@@ -255,4 +295,14 @@ if (Array.isArray(s)) { | ||
| } | ||
| // Arrays can also carry symbol metadata. Enumerate symbols separately to | ||
| // avoid scanning large index lists twice. Outside a store tree, retain the | ||
| // existing index-only walk. | ||
| const O = i || n ? getStoreSymbols(s, E) : []; | ||
| for (let e = 0, i = O.length; e < i; e++) { | ||
| const i = O[e]; | ||
| const c = getPropertyDescriptor(s, E, i); | ||
| if (!c || c.get) continue; | ||
| walkAffectsScope(c.value, t, r, n, o); | ||
| } | ||
| } else { | ||
| const e = getKeys(s, E); | ||
| const e = i || n ? getStoreKeys(s, E) : getKeys(s, E); | ||
| for (let i = 0, O = e.length; i < O; i++) { | ||
@@ -286,11 +336,11 @@ const O = getPropertyDescriptor(s, E, e[i]); | ||
| * @internal | ||
| */ function witnessAffectsMark(e) { | ||
| */ function witnessAffectsMark(e, t) { | ||
| // 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); | ||
| const r = e[STORE_NODE]?.[$AFFECTS]; | ||
| if (r?.M) GlobalQueue.wt(r); | ||
| 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); | ||
| const n = e[STORE_VALUE]; | ||
| for (const [e, o] of affectsScopes) { | ||
| if (e !== r && e.M && o.scope.has(n) && (o.key === undefined || o.key === t)) GlobalQueue.wt(e); | ||
| } | ||
@@ -312,10 +362,10 @@ } | ||
| const r = getNodes(e, STORE_NODE); | ||
| 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]); | ||
| }; | ||
| 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); | ||
@@ -330,7 +380,21 @@ if (!n) affectsScopes.set(t, n = { | ||
| } | ||
| 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]) ]; | ||
| let n = r[t]; | ||
| if (!n) { | ||
| const o = getOverlayLayer(e, t); | ||
| const i = o ? o[t] : e[STORE_VALUE][t]; | ||
| n = upsertStoreNode(e, r, t, i === $DELETED ? undefined : i, e[STORE_SNAPSHOT_PROPS]); | ||
| } | ||
| // Keyed marks resolve by identity too (#2904): another store family's | ||
| // proxy can share this record's raw (a derived store swaps its backing to | ||
| // the source's raw when its projection lands), and reads through it never | ||
| // touch this target's node map. Scope is exactly the owning record's raw, | ||
| // narrowed to this key for witness and birth inheritance. | ||
| let o = affectsScopes.get(n); | ||
| if (!o) affectsScopes.set(n, o = { | ||
| scope: new Set, | ||
| inherited: [], | ||
| key: t | ||
| }); | ||
| o.scope.add(e[STORE_VALUE]); | ||
| return [ n ]; | ||
| } | ||
@@ -370,13 +434,32 @@ | ||
| function getKeys(e, t, r = true) { | ||
| function getKeysImpl(e, t, r, n) { | ||
| // 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); | ||
| const o = e[$TARGET] ? untrack(() => r ? n ? ownEnumerableKeys(e) : Object.keys(e) : Reflect.ownKeys(e)) : r ? n ? ownEnumerableKeysPlain(e) : Object.keys(e) : Reflect.ownKeys(e); | ||
| return t ? mergeOverrideKeys(o, t) : o; | ||
| } | ||
| function getKeys(e, t, r = true) { | ||
| return getKeysImpl(e, t, r, false); | ||
| } | ||
| function getStoreKeys(e, t) { | ||
| return getKeysImpl(e, t, true, true); | ||
| } | ||
| function getStoreSymbols(e, t) { | ||
| const r = e[$TARGET] ? untrack(() => ownEnumerableSymbols(e)) : ownEnumerableSymbols(e); | ||
| return t ? mergeOverrideKeys(r, t, true) : r; | ||
| } | ||
| // Shared override-layer merge for key enumeration: adds live override keys, | ||
| // drops $DELETED ones. `symbolsOnly` scopes the override scan for the | ||
| // array-metadata passes. | ||
| function mergeOverrideKeys(e, t, r) { | ||
| const n = new Set(e); | ||
| const o = r ? Object.getOwnPropertySymbols(t) : Reflect.ownKeys(t); | ||
| for (const e of o) { | ||
| if (t[e] !== $DELETED) n.add(e); else n.delete(e); | ||
| } | ||
| return Array.from(o); | ||
| return Array.from(n); | ||
| } | ||
@@ -441,6 +524,18 @@ | ||
| if (e[STORE_OPTIMISTIC] && !projectionWriteActive) { | ||
| GlobalQueue.Vt(t); | ||
| GlobalQueue.mn(t); | ||
| } | ||
| } | ||
| /** | ||
| * Records which transition owns an optimistic layer entry (#2899), so a | ||
| * settling action only consumes its own keys — the layer is store-wide, but | ||
| * concurrent actions writing disjoint keys must revert independently, exactly | ||
| * like optimistic signal nodes do via the transition's _optimisticNodes. | ||
| * `activeTransition` is the write's transaction (action() opens it before the | ||
| * body runs); null marks an ambient write that clears at plain flush end. | ||
| * Same-key writes across actions keep last-write-wins layer semantics. | ||
| */ function stampOptimisticOwner(e, t, r) { | ||
| if (t === STORE_OPTIMISTIC_OVERRIDE) (e[STORE_OPTIMISTIC_OWNERS] ??= Object.create(null))[r] = activeTransition; | ||
| } | ||
| function upsertStoreNode(e, t, r, n, o) { | ||
@@ -493,2 +588,15 @@ if (t[r]) return t[r]; | ||
| /** | ||
| * A derived store's seed is a draft for the derive function, never an | ||
| * observable value (#2897): until the firewall first resolves there is | ||
| * nothing to read, so every consumer path throws NotReady — tracked reads | ||
| * through their node (core read()), and the untracked fall-throughs in the | ||
| * traps through this guard. Returning the seed leaked it; returning | ||
| * `undefined` would break non-nullable types. Callers exempt the firewall | ||
| * itself (the derive function works its own draft while uninitialized). | ||
| */ function throwIfUninitialized(e) { | ||
| const t = e[STORE_FIREWALL]; | ||
| if (t && t.i & STATUS_UNINITIALIZED) throw t.k ?? new NotReadyError(t); | ||
| } | ||
| const storeTraps = { | ||
@@ -499,3 +607,3 @@ get(e, t, r) { | ||
| if (t === $REFRESH) return e[STORE_FIREWALL]; | ||
| if (pendingCheckActive) witnessAffectsMark(e); | ||
| if (pendingCheckActive) witnessAffectsMark(e, t); | ||
| if (t === $TRACK) { | ||
@@ -515,10 +623,10 @@ trackSelf(e); | ||
| const c = !!e[STORE_VALUE][$TARGET]; | ||
| const f = E ?? e[STORE_VALUE]; | ||
| const S = E ?? e[STORE_VALUE]; | ||
| if (!i) { | ||
| const n = Object.getOwnPropertyDescriptor(f, t); | ||
| const n = Object.getOwnPropertyDescriptor(S, t); | ||
| if (n && n.get) return n.get.call(r); | ||
| if (!n && !O && e[STORE_CUSTOM_PROTO]) { | ||
| const e = unwrapStoreValue(f); | ||
| const e = unwrapStoreValue(S); | ||
| if (hasInheritedAccessor(e, t)) { | ||
| return Reflect.get(f, t, r); | ||
| return Reflect.get(S, t, r); | ||
| } | ||
@@ -529,3 +637,3 @@ } | ||
| if (isPrototypePollutionKey(t) && !hasOwnStoreProperty(e, t)) return undefined; | ||
| let r = i && (O || !c) ? visibleNodeValue(i) : f[t]; | ||
| let r = i && (O || !c) ? visibleNodeValue(i) : S[t]; | ||
| r === $DELETED && (r = undefined); | ||
@@ -537,17 +645,20 @@ if (!isWrappable(r)) return r; | ||
| } | ||
| let S = i ? O || !c ? read(o[t]) : (read(o[t]), f[t]) : f[t]; | ||
| S === $DELETED && (S = undefined); | ||
| let f = i ? O || !c ? read(o[t]) : (read(o[t]), S[t]) : S[t]; | ||
| f === $DELETED && (f = undefined); | ||
| if (!i) { | ||
| if (!O && typeof S === "function" && !Object.prototype.hasOwnProperty.call(f, t)) { | ||
| if (!O && typeof f === "function" && !Object.prototype.hasOwnProperty.call(S, t)) { | ||
| let t; | ||
| return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? S.bind(f) : S; | ||
| return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? f.bind(S) : f; | ||
| } else if (getObserver() && !n) { | ||
| return read(getNode(e, o, t, isWrappable(S) ? wrap(S, e) : S, isEqual, e[STORE_SNAPSHOT_PROPS])); | ||
| return read(getNode(e, o, t, isWrappable(f) ? wrap(f, e) : f, isEqual, e[STORE_SNAPSHOT_PROPS])); | ||
| } | ||
| } | ||
| return isWrappable(S) ? wrap(S, e) : S; | ||
| // Untracked fall-through (tracked reads already threw via their node in | ||
| // read(); the dev strictRead error above wins first for memo parity). | ||
| if (!n) throwIfUninitialized(e); | ||
| return isWrappable(f) ? wrap(f, e) : f; | ||
| }, | ||
| has(e, t) { | ||
| if (t === $PROXY || t === $TRACK || t === "__proto__") return true; | ||
| if (pendingCheckActive) witnessAffectsMark(e); | ||
| if (pendingCheckActive) witnessAffectsMark(e, t); | ||
| const r = getOverlayLayer(e, t); | ||
@@ -567,2 +678,3 @@ const n = r ? r[t] !== $DELETED : t in e[STORE_VALUE]; | ||
| } | ||
| throwIfUninitialized(e); | ||
| return n; | ||
@@ -579,25 +691,32 @@ }, | ||
| const c = E ? E[t] !== $DELETED : t in e[STORE_VALUE]; | ||
| const f = unwrapStoreValue(r); | ||
| const S = 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; | ||
| const f = typeof t === "string" ? Number(t) : -1; | ||
| const T = Array.isArray(s) && Number.isInteger(f) && f >= 0 && f < 4294967295 && String(f) === t; | ||
| const R = T ? f + 1 : 0; | ||
| const u = T && (getOverlayLayer(e, "length") ?? s).length; | ||
| const l = T && R > u ? R : undefined; | ||
| if (O === S && l === undefined) return true; | ||
| armOptimisticStoreWrite(e, n); | ||
| if (f !== undefined && f === o && l === undefined) delete e[i]?.[t]; else { | ||
| if (S !== undefined && S === o && l === undefined) { | ||
| delete e[i]?.[t]; | ||
| if (i === STORE_OPTIMISTIC_OVERRIDE) delete e[STORE_OPTIMISTIC_OWNERS]?.[t]; | ||
| } else { | ||
| const r = e[i] || (e[i] = Object.create(null)); | ||
| r[t] = f; | ||
| if (l !== undefined) r.length = l; | ||
| r[t] = S; | ||
| stampOptimisticOwner(e, i, t); | ||
| if (l !== undefined) { | ||
| r.length = l; | ||
| stampOptimisticOwner(e, i, "length"); | ||
| } | ||
| } | ||
| notifyStoreProperty(e, t, "set", f, O, c); | ||
| notifyStoreProperty(e, t, "set", S, 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) { | ||
| if (Array.isArray(s) && t === "length" && typeof S === "number" && typeof O === "number" && S < O) { | ||
| const t = e[i] || (e[i] = Object.create(null)); | ||
| for (let r = f; r < O; r++) { | ||
| for (let r = S; r < O; r++) { | ||
| if (t[r] === $DELETED) continue; | ||
@@ -607,2 +726,3 @@ const n = r in t ? t[r] : s[r]; | ||
| t[r] = $DELETED; | ||
| stampOptimisticOwner(e, i, r); | ||
| notifyStoreProperty(e, r, "delete", undefined, n, true); | ||
@@ -638,2 +758,3 @@ } | ||
| Object.defineProperty(e[i] || (e[i] = Object.create(null)), t, s); | ||
| stampOptimisticOwner(e, i, t); | ||
| notifyStoreProperty(e, t, "invalidate"); | ||
@@ -659,5 +780,7 @@ if (false) ; | ||
| (e[n] || (e[n] = Object.create(null)))[t] = $DELETED; | ||
| stampOptimisticOwner(e, n, t); | ||
| } else if (e[n] && t in e[n]) { | ||
| armOptimisticStoreWrite(e, e[$PROXY]); | ||
| delete e[n][t]; | ||
| if (n === STORE_OPTIMISTIC_OVERRIDE) delete e[STORE_OPTIMISTIC_OWNERS]?.[t]; | ||
| } else return true; | ||
@@ -671,3 +794,11 @@ notifyStoreProperty(e, t, "delete", undefined, i, true); | ||
| if (pendingCheckActive) witnessAffectsMark(e); | ||
| if (getObserver() !== e[STORE_FIREWALL]) trackSelf(e); | ||
| if (getObserver() !== e[STORE_FIREWALL]) { | ||
| trackSelf(e); | ||
| // trackSelf no-ops untracked, so enumeration of an unresolved derived | ||
| // store would otherwise leak the seed's structure (#2897). The write | ||
| // path is exempt (like the get/has traps' writeOnly early returns): | ||
| // the first landing's reconcile enumerates the store while | ||
| // STATUS_UNINITIALIZED is still set — it IS the initialization. | ||
| if (!getObserver() && !writeOnly(e[$PROXY])) throwIfUninitialized(e); | ||
| } | ||
| // Merge optimistic override with regular override for key enumeration | ||
@@ -765,2 +896,2 @@ let t = getKeys(e[STORE_VALUE], e[STORE_OVERRIDE], false); | ||
| 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 }; | ||
| export { $AFFECTS, $DELETED, $PROXY, $TARGET, $TRACK, STORE_CUSTOM_PROTO, STORE_DESC, STORE_FIREWALL, STORE_HAS, STORE_LOOKUP, STORE_NODE, STORE_OPTIMISTIC, STORE_OPTIMISTIC_OVERRIDE, STORE_OPTIMISTIC_OWNERS, STORE_OVERRIDE, STORE_PARENT, STORE_VALUE, STORE_WRAP, createStore, createStoreProxy, getKeys, getOverlayLayer, getPropertyDescriptor, getStoreAffectsNodes, getStoreKeys, getStoreSymbols, isWrappable, mergedOverlay, notifySelf, ownEnumerableKeys, setWriteOverride, storeLookup, storeSetter, storeTraps, symbolKeyedRecords, trackSelf, visibleNodeValue, witnessAffectsMark, wrap }; |
+61
-42
@@ -15,6 +15,6 @@ import { pendingCheckActive } from "../core/core.js"; | ||
| import { ownEnumerableKeys, $PROXY, isWrappable, $TARGET, trackSelf, witnessAffectsMark, mergedOverlay, STORE_VALUE, storeLookup, STORE_LOOKUP, $DELETED, wrap, getKeys, getPropertyDescriptor, $TRACK } from "./store.js"; | ||
| import { ownEnumerableKeys, $PROXY, isWrappable, $TARGET, trackSelf, witnessAffectsMark, mergedOverlay, STORE_VALUE, storeLookup, STORE_LOOKUP, $DELETED, wrap, getStoreSymbols, getPropertyDescriptor, getStoreKeys, getKeys, $TRACK } from "./store.js"; | ||
| function snapshotImpl(e, t, r, n) { | ||
| let o, s, c, i, f, u; | ||
| let o, s, i, c, f, u; | ||
| if (!isWrappable(e)) return e; | ||
@@ -30,5 +30,5 @@ if (r && r.has(e)) return r.get(e); | ||
| } | ||
| c = mergedOverlay(o); | ||
| i = mergedOverlay(o); | ||
| s = Array.isArray(o[STORE_VALUE]); | ||
| r.set(e, c ? i = s ? [] : Object.create(Object.getPrototypeOf(o[STORE_VALUE])) : o[STORE_VALUE]); | ||
| r.set(e, i ? c = s ? [] : Object.create(Object.getPrototypeOf(o[STORE_VALUE])) : o[STORE_VALUE]); | ||
| e = o[STORE_VALUE]; | ||
@@ -41,22 +41,39 @@ n = o[STORE_LOOKUP] ?? storeLookup; | ||
| if (s) { | ||
| const s = c?.length ?? e.length; | ||
| const s = i?.length ?? e.length; | ||
| for (let p = 0; p < s; p++) { | ||
| u = c && p in c ? c[p] : e[p]; | ||
| u = i && p in i ? i[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; | ||
| if ((f = snapshotImpl(u, t, r, n)) !== u || c) { | ||
| if (!c) r.set(e, c = [ ...e ]); | ||
| c[p] = f; | ||
| } | ||
| } | ||
| // Enumerate array symbols separately to avoid scanning indices twice. | ||
| // Spread copies omit symbols, so assign them after the numeric walk. | ||
| const p = n ? getStoreSymbols(e, i) : []; | ||
| for (let s = 0, l = p.length; s < l; s++) { | ||
| const l = p[s]; | ||
| const a = getPropertyDescriptor(e, i, l); | ||
| if (!a || a.get) continue; | ||
| u = i && l in i ? i[l] : e[l]; | ||
| if (t && isWrappable(u)) wrap(u, o); | ||
| f = snapshotImpl(u, t, r, n); | ||
| if (f !== u || c) { | ||
| if (!c) r.set(e, c = Object.assign([ ...e ], e)); | ||
| c[l] = 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) { | ||
| if (c) c.length = s; | ||
| } else if (!i) { | ||
| // 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]; | ||
| // A lookup means this object belongs to an immutable store backing tree, | ||
| // even if that nested value has not needed its own proxy yet. | ||
| const s = n ? getStoreKeys(e, undefined) : getKeys(e, undefined); | ||
| for (let i = 0, p = s.length; i < p; i++) { | ||
| const p = s[i]; | ||
| const l = Object.getOwnPropertyDescriptor(e, p); | ||
@@ -66,28 +83,30 @@ if (l.get) continue; | ||
| 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); | ||
| if ((f = snapshotImpl(u, t, r, n)) !== u || c) { | ||
| if (!c) { | ||
| c = Object.create(Object.getPrototypeOf(e)); | ||
| Object.assign(c, e); | ||
| } | ||
| i[p] = f; | ||
| c[p] = f; | ||
| } | ||
| } | ||
| } else { | ||
| const s = getKeys(e, c); | ||
| // An override only exists on a store record, and the target branch above | ||
| // always set `lookup` alongside it — so this branch is always store-keyed. | ||
| const s = getStoreKeys(e, i); | ||
| for (let p = 0, l = s.length; p < l; p++) { | ||
| let l = s[p]; | ||
| const a = getPropertyDescriptor(e, c, l); | ||
| const a = getPropertyDescriptor(e, i, l); | ||
| if (a.get) continue; | ||
| u = l in c ? c[l] : e[l]; | ||
| u = l in i ? i[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); | ||
| if ((f = snapshotImpl(u, t, r, n)) !== e[l] || c) { | ||
| if (!c) { | ||
| c = Object.create(Object.getPrototypeOf(e)); | ||
| Object.assign(c, e); | ||
| } | ||
| i[l] = f; | ||
| c[l] = f; | ||
| } | ||
| } | ||
| } | ||
| return i || e; | ||
| return c || e; | ||
| } | ||
@@ -225,10 +244,10 @@ | ||
| } | ||
| 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]) { | ||
| const i = Object.getOwnPropertyNames(t); | ||
| for (let r = i.length - 1; r >= 0; r--) { | ||
| const c = i[r]; | ||
| if (c === "__proto__" || c === "constructor") continue; | ||
| if (!n[c]) { | ||
| o = o || e !== s; | ||
| const r = Object.getOwnPropertyDescriptor(t, i); | ||
| n[i] = r.get ? { | ||
| const r = Object.getOwnPropertyDescriptor(t, c); | ||
| n[c] = r.get ? { | ||
| enumerable: true, | ||
@@ -242,10 +261,10 @@ configurable: true, | ||
| 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; | ||
| const i = {}; | ||
| const c = Object.keys(n); | ||
| for (let e = c.length - 1; e >= 0; e--) { | ||
| const t = c[e], r = n[t]; | ||
| if (r.get) Object.defineProperty(i, t, r); else i[t] = r.value; | ||
| } | ||
| c[$SOURCES] = r; | ||
| return c; | ||
| i[$SOURCES] = r; | ||
| return i; | ||
| } | ||
@@ -252,0 +271,0 @@ |
@@ -7,7 +7,19 @@ /** | ||
| * | ||
| * 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. | ||
| * `yield` is the transaction-safe suspension point: the action waits for a | ||
| * yielded promise and re-enters the transaction before running the code after | ||
| * it. A plain `await` does NOT — the runtime has no hook into an async | ||
| * generator's internal await continuations, so writes to fresh signals | ||
| * between an `await` and the next `yield` escape the transaction and commit | ||
| * immediately. `await` is still the ergonomic choice for typed results; just | ||
| * put a bare `yield` before any writes that follow it: | ||
| * | ||
| * ```ts | ||
| * const saved = await api.createTodo(text); // typed result | ||
| * yield; // re-enter the transaction before writing | ||
| * setTodos(t => { ... }); | ||
| * ``` | ||
| * | ||
| * (For the same reason, don't call `flush()` inside an action body — it | ||
| * drains the transaction mid-step.) | ||
| * | ||
| * Each call returns a `Promise` that resolves with the generator's return | ||
@@ -22,6 +34,7 @@ * value, or rejects if it throws. Pair with `createOptimistic` / | ||
| * | ||
| * const addTodo = action(function* (text: string) { | ||
| * const addTodo = action(async 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 | ||
| * const saved = await api.createTodo(text); // network round-trip, typed | ||
| * yield; // re-enter the transaction | ||
| * setTodos(t => { | ||
@@ -28,0 +41,0 @@ * const i = t.findIndex(x => x.id === tempId); |
@@ -38,2 +38,14 @@ export declare const REACTIVE_NONE = 0; | ||
| export declare const NO_SNAPSHOT: {}; | ||
| /** | ||
| * Stand-in stored in `_overrideValue` for an optimistic write of literal | ||
| * `undefined` (#2898). The slot doubles as the optimistic-node brand | ||
| * (`undefined` = not optimistic, `NOT_PENDING` = at rest), so the raw value | ||
| * would erase the node's optimistic identity: the write turns invisible and | ||
| * follow-up writes route off the optimistic path and commit permanently. | ||
| * Same shape as NO_SNAPSHOT. Sites that surface the override VALUE unwrap | ||
| * via `visibleOverrideValue`; slot identity tests stay raw. | ||
| */ | ||
| export declare const OVERRIDE_UNDEFINED: {}; | ||
| /** Unwrap an active override's stored value for surfacing to readers (#2898). */ | ||
| export declare function unwrapOverride<T = any>(v: unknown): T; | ||
| export declare const STORE_SNAPSHOT_PROPS = "sp"; | ||
@@ -40,0 +52,0 @@ export declare const SUPPORTS_PROXY: boolean; |
@@ -51,2 +51,9 @@ import type { Computed, Owner, Signal } from "./types.cjs"; | ||
| export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence">): DiagnosticEvent; | ||
| /** | ||
| * Shared strict-read diagnostics for core read() and the store proxy traps. | ||
| * Single source for the message text — the #2897 safeguard parity between | ||
| * memos and stores is exactly these firing identically from both paths. | ||
| */ | ||
| export declare function throwPendingUntrackedRead(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "data">>): never; | ||
| export declare function warnStrictReadUntracked(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence">>): void; | ||
| export declare function registerGraph(value: any, owner: Owner | null): void; | ||
@@ -53,0 +60,0 @@ export declare function clearSignals(node: Owner): void; |
@@ -40,2 +40,4 @@ import { type QueueCallback, type Transition } from "./scheduler.cjs"; | ||
| _transition?: Transition | null; | ||
| _overrideValue?: any; | ||
| _overrideOwner?: Transition | null; | ||
| }): Transition | null | undefined; | ||
@@ -42,0 +44,0 @@ /** |
@@ -78,11 +78,7 @@ import { type Heap } from "./heap.cjs"; | ||
| _running: boolean; | ||
| _pendingNode: Signal<any> | null; | ||
| _pendingNodes: Signal<any>[]; | ||
| _optimisticNodes: OptimisticNode[]; | ||
| _affectsNodes: OptimisticNode[]; | ||
| _optimisticStores: Set<any>; | ||
| _batch: Transition; | ||
| static _update: (el: Computed<unknown>) => void; | ||
| static _dispose: (el: Computed<unknown>, self: boolean, zombie: boolean) => void; | ||
| static _runEffect: (el: Computed<unknown>) => void; | ||
| static _clearOptimisticStores: ((stores: Set<any>) => void) | null; | ||
| static _clearOptimisticStores: ((stores: Set<any>, completing: Transition | null) => void) | null; | ||
| static _releaseAffectsScope: ((node: OptimisticNode) => void) | null; | ||
@@ -89,0 +85,0 @@ static _applyAffectsReads: ((el: Computed<any>, sources: (Signal<any> | Computed<any>)[]) => void) | null; |
@@ -47,2 +47,11 @@ import type { NOT_PENDING } from "./constants.cjs"; | ||
| _overrideValue?: T | typeof NOT_PENDING; | ||
| /** | ||
| * The transaction that owns the active override (stamped at optimistic | ||
| * write, cleared at settle). Ownership must live on the node: a lane's | ||
| * _transition is a scheduling affinity that a shared subscriber can merge | ||
| * across transactions (#2912) — following it would let one action's settle | ||
| * revert another action's live override. Node-level sibling of the store | ||
| * layer's STORE_OPTIMISTIC_OWNERS stamps (#2899). `null` = ambient write. | ||
| */ | ||
| _overrideOwner?: Transition | null; | ||
| _optimisticLane?: OptimisticLane; | ||
@@ -97,3 +106,2 @@ _pendingSignal?: Signal<boolean>; | ||
| _blocked?: boolean; | ||
| _pendingSource?: Computed<any>; | ||
| _pendingSources?: Set<Computed<any>>; | ||
@@ -100,0 +108,0 @@ _error?: unknown; |
| import { STORE_SNAPSHOT_PROPS, type Computed, type Refreshable, type Signal } from "../core/index.cjs"; | ||
| import { type Transition } from "../core/scheduler.cjs"; | ||
| /** A read-only view of a store's value as seen by consumers. Mutate it via the paired `StoreSetter`. */ | ||
@@ -44,3 +45,3 @@ export type Store<T> = Readonly<T>; | ||
| export declare const $TRACK: unique symbol, $TARGET: unique symbol, $PROXY: unique symbol, $DELETED: unique symbol, $AFFECTS: unique symbol; | ||
| export declare 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"; | ||
| export declare 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", STORE_OPTIMISTIC_OWNERS = "t", STORE_PARENT = "u", STORE_DESC = "d"; | ||
| export type StoreNode = { | ||
@@ -51,2 +52,3 @@ [$PROXY]: any; | ||
| [STORE_OPTIMISTIC_OVERRIDE]?: Record<PropertyKey, any>; | ||
| [STORE_OPTIMISTIC_OWNERS]?: Record<PropertyKey, Transition | null>; | ||
| [STORE_NODE]?: DataNodes; | ||
@@ -60,2 +62,4 @@ [STORE_HAS]?: DataNodes; | ||
| [STORE_SNAPSHOT_PROPS]?: Record<PropertyKey, any>; | ||
| [STORE_PARENT]?: StoreNode; | ||
| [STORE_DESC]?: boolean; | ||
| }; | ||
@@ -97,3 +101,3 @@ export declare namespace SolidStore { | ||
| */ | ||
| export declare function witnessAffectsMark(target: StoreNode): void; | ||
| export declare function witnessAffectsMark(target: StoreNode, property?: PropertyKey): void; | ||
| /** | ||
@@ -121,2 +125,4 @@ * Resolves the store nodes an `affects()` declaration marks: with a `key`, | ||
| export declare function getKeys(source: Record<PropertyKey, any>, override: Record<PropertyKey, any> | undefined, enumerable?: boolean): PropertyKey[]; | ||
| export declare function getStoreKeys(source: Record<PropertyKey, any>, override: Record<PropertyKey, any> | undefined): PropertyKey[]; | ||
| export declare function getStoreSymbols(source: Record<PropertyKey, any>, override: Record<PropertyKey, any> | undefined): symbol[]; | ||
| export declare function getPropertyDescriptor(source: Record<PropertyKey, any>, override: Record<PropertyKey, any> | undefined, property: PropertyKey): PropertyDescriptor | undefined; | ||
@@ -123,0 +129,0 @@ export declare const storeTraps: ProxyHandler<StoreNode>; |
@@ -7,7 +7,19 @@ /** | ||
| * | ||
| * 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. | ||
| * `yield` is the transaction-safe suspension point: the action waits for a | ||
| * yielded promise and re-enters the transaction before running the code after | ||
| * it. A plain `await` does NOT — the runtime has no hook into an async | ||
| * generator's internal await continuations, so writes to fresh signals | ||
| * between an `await` and the next `yield` escape the transaction and commit | ||
| * immediately. `await` is still the ergonomic choice for typed results; just | ||
| * put a bare `yield` before any writes that follow it: | ||
| * | ||
| * ```ts | ||
| * const saved = await api.createTodo(text); // typed result | ||
| * yield; // re-enter the transaction before writing | ||
| * setTodos(t => { ... }); | ||
| * ``` | ||
| * | ||
| * (For the same reason, don't call `flush()` inside an action body — it | ||
| * drains the transaction mid-step.) | ||
| * | ||
| * Each call returns a `Promise` that resolves with the generator's return | ||
@@ -22,6 +34,7 @@ * value, or rejects if it throws. Pair with `createOptimistic` / | ||
| * | ||
| * const addTodo = action(function* (text: string) { | ||
| * const addTodo = action(async 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 | ||
| * const saved = await api.createTodo(text); // network round-trip, typed | ||
| * yield; // re-enter the transaction | ||
| * setTodos(t => { | ||
@@ -28,0 +41,0 @@ * const i = t.findIndex(x => x.id === tempId); |
@@ -38,2 +38,14 @@ export declare const REACTIVE_NONE = 0; | ||
| export declare const NO_SNAPSHOT: {}; | ||
| /** | ||
| * Stand-in stored in `_overrideValue` for an optimistic write of literal | ||
| * `undefined` (#2898). The slot doubles as the optimistic-node brand | ||
| * (`undefined` = not optimistic, `NOT_PENDING` = at rest), so the raw value | ||
| * would erase the node's optimistic identity: the write turns invisible and | ||
| * follow-up writes route off the optimistic path and commit permanently. | ||
| * Same shape as NO_SNAPSHOT. Sites that surface the override VALUE unwrap | ||
| * via `visibleOverrideValue`; slot identity tests stay raw. | ||
| */ | ||
| export declare const OVERRIDE_UNDEFINED: {}; | ||
| /** Unwrap an active override's stored value for surfacing to readers (#2898). */ | ||
| export declare function unwrapOverride<T = any>(v: unknown): T; | ||
| export declare const STORE_SNAPSHOT_PROPS = "sp"; | ||
@@ -40,0 +52,0 @@ export declare const SUPPORTS_PROXY: boolean; |
@@ -51,2 +51,9 @@ import type { Computed, Owner, Signal } from "./types.js"; | ||
| export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence">): DiagnosticEvent; | ||
| /** | ||
| * Shared strict-read diagnostics for core read() and the store proxy traps. | ||
| * Single source for the message text — the #2897 safeguard parity between | ||
| * memos and stores is exactly these firing identically from both paths. | ||
| */ | ||
| export declare function throwPendingUntrackedRead(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "data">>): never; | ||
| export declare function warnStrictReadUntracked(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence">>): void; | ||
| export declare function registerGraph(value: any, owner: Owner | null): void; | ||
@@ -53,0 +60,0 @@ export declare function clearSignals(node: Owner): void; |
@@ -40,2 +40,4 @@ import { type QueueCallback, type Transition } from "./scheduler.js"; | ||
| _transition?: Transition | null; | ||
| _overrideValue?: any; | ||
| _overrideOwner?: Transition | null; | ||
| }): Transition | null | undefined; | ||
@@ -42,0 +44,0 @@ /** |
@@ -78,11 +78,7 @@ import { type Heap } from "./heap.js"; | ||
| _running: boolean; | ||
| _pendingNode: Signal<any> | null; | ||
| _pendingNodes: Signal<any>[]; | ||
| _optimisticNodes: OptimisticNode[]; | ||
| _affectsNodes: OptimisticNode[]; | ||
| _optimisticStores: Set<any>; | ||
| _batch: Transition; | ||
| static _update: (el: Computed<unknown>) => void; | ||
| static _dispose: (el: Computed<unknown>, self: boolean, zombie: boolean) => void; | ||
| static _runEffect: (el: Computed<unknown>) => void; | ||
| static _clearOptimisticStores: ((stores: Set<any>) => void) | null; | ||
| static _clearOptimisticStores: ((stores: Set<any>, completing: Transition | null) => void) | null; | ||
| static _releaseAffectsScope: ((node: OptimisticNode) => void) | null; | ||
@@ -89,0 +85,0 @@ static _applyAffectsReads: ((el: Computed<any>, sources: (Signal<any> | Computed<any>)[]) => void) | null; |
@@ -47,2 +47,11 @@ import type { NOT_PENDING } from "./constants.js"; | ||
| _overrideValue?: T | typeof NOT_PENDING; | ||
| /** | ||
| * The transaction that owns the active override (stamped at optimistic | ||
| * write, cleared at settle). Ownership must live on the node: a lane's | ||
| * _transition is a scheduling affinity that a shared subscriber can merge | ||
| * across transactions (#2912) — following it would let one action's settle | ||
| * revert another action's live override. Node-level sibling of the store | ||
| * layer's STORE_OPTIMISTIC_OWNERS stamps (#2899). `null` = ambient write. | ||
| */ | ||
| _overrideOwner?: Transition | null; | ||
| _optimisticLane?: OptimisticLane; | ||
@@ -97,3 +106,2 @@ _pendingSignal?: Signal<boolean>; | ||
| _blocked?: boolean; | ||
| _pendingSource?: Computed<any>; | ||
| _pendingSources?: Set<Computed<any>>; | ||
@@ -100,0 +108,0 @@ _error?: unknown; |
| import { STORE_SNAPSHOT_PROPS, type Computed, type Refreshable, type Signal } from "../core/index.js"; | ||
| import { type Transition } from "../core/scheduler.js"; | ||
| /** A read-only view of a store's value as seen by consumers. Mutate it via the paired `StoreSetter`. */ | ||
@@ -44,3 +45,3 @@ export type Store<T> = Readonly<T>; | ||
| export declare const $TRACK: unique symbol, $TARGET: unique symbol, $PROXY: unique symbol, $DELETED: unique symbol, $AFFECTS: unique symbol; | ||
| export declare 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"; | ||
| export declare 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", STORE_OPTIMISTIC_OWNERS = "t", STORE_PARENT = "u", STORE_DESC = "d"; | ||
| export type StoreNode = { | ||
@@ -51,2 +52,3 @@ [$PROXY]: any; | ||
| [STORE_OPTIMISTIC_OVERRIDE]?: Record<PropertyKey, any>; | ||
| [STORE_OPTIMISTIC_OWNERS]?: Record<PropertyKey, Transition | null>; | ||
| [STORE_NODE]?: DataNodes; | ||
@@ -60,2 +62,4 @@ [STORE_HAS]?: DataNodes; | ||
| [STORE_SNAPSHOT_PROPS]?: Record<PropertyKey, any>; | ||
| [STORE_PARENT]?: StoreNode; | ||
| [STORE_DESC]?: boolean; | ||
| }; | ||
@@ -97,3 +101,3 @@ export declare namespace SolidStore { | ||
| */ | ||
| export declare function witnessAffectsMark(target: StoreNode): void; | ||
| export declare function witnessAffectsMark(target: StoreNode, property?: PropertyKey): void; | ||
| /** | ||
@@ -121,2 +125,4 @@ * Resolves the store nodes an `affects()` declaration marks: with a `key`, | ||
| export declare function getKeys(source: Record<PropertyKey, any>, override: Record<PropertyKey, any> | undefined, enumerable?: boolean): PropertyKey[]; | ||
| export declare function getStoreKeys(source: Record<PropertyKey, any>, override: Record<PropertyKey, any> | undefined): PropertyKey[]; | ||
| export declare function getStoreSymbols(source: Record<PropertyKey, any>, override: Record<PropertyKey, any> | undefined): symbol[]; | ||
| export declare function getPropertyDescriptor(source: Record<PropertyKey, any>, override: Record<PropertyKey, any> | undefined, property: PropertyKey): PropertyDescriptor | undefined; | ||
@@ -123,0 +129,0 @@ export declare const storeTraps: ProxyHandler<StoreNode>; |
+1
-1
| { | ||
| "name": "@solidjs/signals", | ||
| "version": "2.0.0-beta.19", | ||
| "version": "2.0.0-beta.20", | ||
| "description": "Solid's reactive primitives: signals, memos, effects, stores, and async-aware computations.", | ||
@@ -5,0 +5,0 @@ "author": "Ryan Carniato", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
1055329
5.18%23432
3.83%