@solidjs/signals
Advanced tools
+14
-11
@@ -85,3 +85,3 @@ import { handleAsync, clearStatus, notifyStatus } from "./async.js"; | ||
| e.se |= REACTIVE_DIRTY; | ||
| if (dirtyQueue.Fe > e.He) dirtyQueue.Fe = e.He; | ||
| if (dirtyQueue.He > e.Fe) dirtyQueue.He = e.Fe; | ||
| insertIntoHeap(e, dirtyQueue); | ||
@@ -99,3 +99,6 @@ } | ||
| delete e.Ve; | ||
| delete e[STORE_SNAPSHOT_PROPS]; | ||
| // StoreNode targets share one pre-initialized hidden class (see | ||
| // createStoreProxy) — assign undefined instead of deleting, and only | ||
| // when present so signal-node sources don't grow the field. | ||
| if (e[STORE_SNAPSHOT_PROPS] !== undefined) e[STORE_SNAPSHOT_PROPS] = undefined; | ||
| } | ||
@@ -136,3 +139,3 @@ snapshotSources = null; | ||
| let a = e.Ne === NOT_PENDING ? e.Pe : e.Ne; | ||
| let r = e.He; | ||
| let r = e.Fe; | ||
| let c = tracking; | ||
@@ -258,3 +261,3 @@ let _ = currentOptimisticLane; | ||
| e.Ne = a; | ||
| } else if (e.He != r) { | ||
| } else if (e.Fe != r) { | ||
| for (let t = e.o; t !== null; t = t.ue) { | ||
@@ -308,3 +311,3 @@ insertIntoHeapHeight(t.le, queueFor(t.le)); | ||
| Pe: undefined, | ||
| He: 0, | ||
| Fe: 0, | ||
| u: null, | ||
@@ -354,3 +357,3 @@ lt: undefined, | ||
| Pe: undefined, | ||
| He: 0, | ||
| Fe: 0, | ||
| u: null, | ||
@@ -406,3 +409,3 @@ lt: undefined, | ||
| } | ||
| if (n) e.He = n.He + 1; | ||
| if (n) e.Fe = n.Fe + 1; | ||
| if (GlobalQueue.Tt !== null) GlobalQueue.Tt(e); | ||
@@ -530,3 +533,3 @@ !t?.lazy && recompute(e, true); | ||
| const n = queueFor(e); | ||
| if (l.He >= n.Fe) { | ||
| if (l.Fe >= n.He) { | ||
| markNode(t); | ||
@@ -536,6 +539,6 @@ markHeap(n); | ||
| } | ||
| const i = l.He; | ||
| const i = l.Fe; | ||
| // parent check is shallow, might need to be recursive | ||
| if (i >= t.He && e.Ge !== t) { | ||
| t.He = i + 1; | ||
| if (i >= t.Fe && e.Ge !== t) { | ||
| t.Fe = i + 1; | ||
| } | ||
@@ -542,0 +545,0 @@ } |
@@ -28,3 +28,12 @@ /** | ||
| constructor(r) { | ||
| // Control-flow throw: it happens on every read of a pending source, so in | ||
| // production skip V8's eager stack capture (proportional to stack depth — | ||
| // real cost under SSR) by zeroing the V8-specific stackTraceLimit around | ||
| // super(). Dev keeps the stack for debuggability; non-V8 engines (no | ||
| // stackTraceLimit) take the plain path. | ||
| const o = Error; | ||
| const t = o.stackTraceLimit; | ||
| if (t !== undefined) o.stackTraceLimit = 0; | ||
| super(); | ||
| if (t !== undefined) o.stackTraceLimit = t; | ||
| this.source = r; | ||
@@ -31,0 +40,0 @@ } |
+12
-12
@@ -23,3 +23,3 @@ 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"; | ||
| const E = queueFor(e); | ||
| if (E.Fe > e.He) E.Fe = e.He; | ||
| if (E.He > e.Fe) E.He = e.Fe; | ||
| insertIntoHeap(e, E); | ||
@@ -29,5 +29,5 @@ } | ||
| function actualInsertIntoHeap(e, E) { | ||
| const t = (e.Ge?.ft ? e.Ge.Nt?.He : e.Ge?.He) ?? -1; | ||
| if (t >= e.He) e.He = t + 1; | ||
| const n = e.He; | ||
| const t = (e.Ge?.ft ? e.Ge.Nt?.Fe : e.Ge?.Fe) ?? -1; | ||
| if (t >= e.Fe) e.Fe = t + 1; | ||
| const n = e.Fe; | ||
| const I = E.eE[n]; | ||
@@ -71,3 +71,3 @@ if (I === undefined) E.eE[n] = e; else { | ||
| e.se = t & -25; | ||
| const n = e.He; | ||
| const n = e.Fe; | ||
| if (e.ut === e) E.eE[n] = undefined; else { | ||
@@ -112,7 +112,7 @@ const t = e.lt; | ||
| e.tE = false; | ||
| for (e.Fe = 0; e.Fe <= e.EE; e.Fe++) { | ||
| let t = e.eE[e.Fe]; | ||
| for (e.He = 0; e.He <= e.EE; e.He++) { | ||
| let t = e.eE[e.He]; | ||
| while (t !== undefined) { | ||
| if (t.se & REACTIVE_IN_HEAP) E(t); else adjustHeight(t, e); | ||
| t = e.eE[e.Fe]; | ||
| t = e.eE[e.He]; | ||
| } | ||
@@ -125,10 +125,10 @@ } | ||
| deleteFromHeap(e, E); | ||
| let t = e.He; | ||
| let t = e.Fe; | ||
| for (let E = e.Xe; E; E = E.et) { | ||
| const e = E.tt; | ||
| const n = e.nt || e; | ||
| if (n.ke && n.He >= t) t = n.He + 1; | ||
| if (n.ke && n.Fe >= t) t = n.Fe + 1; | ||
| } | ||
| if (e.He !== t) { | ||
| e.He = t; | ||
| if (e.Fe !== t) { | ||
| e.Fe = t; | ||
| for (let E = e.o; E !== null; E = E.ue) { | ||
@@ -135,0 +135,0 @@ // Route each subscriber by its own zombie flag, mirroring the |
@@ -22,3 +22,3 @@ 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"; | ||
| tE: false, | ||
| Fe: 0, | ||
| He: 0, | ||
| EE: 0 | ||
@@ -30,3 +30,3 @@ }; | ||
| tE: false, | ||
| Fe: 0, | ||
| He: 0, | ||
| EE: 0 | ||
@@ -339,3 +339,3 @@ }; | ||
| // flush path instead of leaving them until the next natural flush. | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.Fe || this.m.Qt.length > 0; | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.He || this.m.Qt.length > 0; | ||
| reassignPendingTransition(e.Qt); | ||
@@ -368,3 +368,3 @@ activeTransition = null; | ||
| commitPendingNodes(); | ||
| if (dirtyQueue.EE >= dirtyQueue.Fe) { | ||
| if (dirtyQueue.EE >= dirtyQueue.He) { | ||
| runHeap(dirtyQueue, GlobalQueue.pe); | ||
@@ -380,3 +380,3 @@ commitPendingNodes(); | ||
| // Check if finalization added items to the heap (from optimistic reversion) | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.Fe; | ||
| scheduled = dirtyQueue.EE >= dirtyQueue.He; | ||
| // Run lane effects first (for ready lanes), then regular effects | ||
@@ -535,3 +535,3 @@ activeLanes.size && GlobalQueue.In(EFFECT_RENDER); | ||
| if (!t && globalQueue.vt.length) checkBoundaryChildren(globalQueue); | ||
| const n = dirtyQueue.EE >= dirtyQueue.Fe; | ||
| const n = dirtyQueue.EE >= dirtyQueue.He; | ||
| if (n) runHeap(dirtyQueue, GlobalQueue.pe); | ||
@@ -538,0 +538,0 @@ if (i) { |
@@ -235,3 +235,3 @@ import { NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_DIRTY, REACTIVE_CHECK, unwrapOverride, REACTIVE_ZOMBIE, STATUS_UNINITIALIZED, STATUS_PENDING, REACTIVE_MANUAL_WRITE, STATUS_ERROR, REACTIVE_RECOMPUTING_DEPS } from "./constants.js"; | ||
| const e = queueFor(t); | ||
| if (t.He >= e.Fe && !(t.se & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) { | ||
| if (t.Fe >= e.He && !(t.se & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) { | ||
| markHeap(e); | ||
@@ -238,0 +238,0 @@ prepareComputed(t, true); |
+53
-29
@@ -121,5 +121,3 @@ import { computed, runWithOwner, signal, setSignal } from "./core/core.js"; | ||
| } else { | ||
| let o, c, a, f, u, p, w, l, d, O = new Array(s), m = new Array(s); | ||
| r = this.Ut ? new Array(s) : undefined; | ||
| n = this.$t ? new Array(s) : undefined; | ||
| let o, c, a, f, u, p, w, l, d; | ||
| // skip common prefix | ||
@@ -129,11 +127,20 @@ for (o = 0, c = Math.min(this.jt, s); o < c && (this.Mt[o] === t[o] || this.Ut && compare(this.Gt, this.Mt[o], t[o])); o++) { | ||
| } | ||
| // common suffix | ||
| // skip common suffix — counted only; retained entries land in one pass | ||
| // at commit instead of being staged and copied twice | ||
| for (c = this.jt - 1, a = s - 1; c >= o && a >= o && (this.Mt[c] === t[a] || this.Ut && compare(this.Gt, this.Mt[c], t[a])); c--, | ||
| a--) { | ||
| O[a] = this.xt[c]; | ||
| m[a] = this.Ft[c]; | ||
| r && (r[a] = this.Ut[c]); | ||
| n && (n[a] = this.$t[c]); | ||
| a--) ; | ||
| // no structural change (every position matched in place at equal | ||
| // length — the common post-reconcile shape): keep the same mapped | ||
| // array identity so downstream consumers don't re-run at all | ||
| if (o === s && this.jt === s) { | ||
| this.Mt = t.slice(0); | ||
| return; | ||
| } | ||
| // 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order | ||
| const O = s - this.jt; | ||
| const m = new Array(s); | ||
| const _ = new Array(s); | ||
| r = this.Ut ? new Array(s) : undefined; | ||
| n = this.$t ? new Array(s) : undefined; | ||
| // 0) prepare a map of all indices in the changed window of newItems, | ||
| // scanning backwards so we encounter them in natural order | ||
| p = new Map; | ||
@@ -148,3 +155,5 @@ w = new Array(a + 1); | ||
| } | ||
| // 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 | ||
| // 1) step through the old changed window and see if items can be found | ||
| // in the new set; if so, stage them at their new positions; if not, | ||
| // queue them for disposal at commit | ||
| for (i = o; i <= c; i++) { | ||
@@ -155,4 +164,4 @@ f = this.Mt[i]; | ||
| if (e !== undefined && e !== -1) { | ||
| O[e] = this.xt[i]; | ||
| m[e] = this.Ft[i]; | ||
| m[e] = this.xt[i]; | ||
| _[e] = this.Ft[i]; | ||
| r && (r[e] = this.Ut[i]); | ||
@@ -166,6 +175,6 @@ n && (n[e] = this.$t[i]); | ||
| try { | ||
| for (e = o; e < s; e++) { | ||
| if (e in O) continue; | ||
| (d ??= []).push(m[e] = createOwner()); | ||
| O[e] = runWithOwner(m[e], h); | ||
| for (e = o; e <= a; e++) { | ||
| if (_[e] !== undefined) continue; | ||
| (d ??= []).push(_[e] = createOwner()); | ||
| m[e] = runWithOwner(_[e], h); | ||
| } | ||
@@ -176,20 +185,35 @@ } catch (t) { | ||
| } | ||
| // 3) commit: land positions, then dispose exited rows | ||
| for (e = o; e < s; e++) { | ||
| this.xt[e] = O[e]; | ||
| this.Ft[e] = m[e]; | ||
| // 3) commit: land the retained prefix and suffix plus the staged window | ||
| // into the fresh arrays, swap them in (new identity for downstream | ||
| // change propagation), then dispose exited rows | ||
| for (i = 0; i < o; i++) { | ||
| m[i] = this.xt[i]; | ||
| _[i] = this.Ft[i]; | ||
| r && (r[i] = this.Ut[i]); | ||
| n && (n[i] = this.$t[i]); | ||
| } | ||
| for (e = o; e <= a; e++) { | ||
| if (r) setSignal(r[e], t[e]); | ||
| if (n) setSignal(n[e], e); | ||
| } | ||
| for (e = a + 1; e < s; e++) { | ||
| m[e] = this.xt[e - O]; | ||
| _[e] = this.Ft[e - O]; | ||
| if (r) { | ||
| this.Ut[e] = r[e]; | ||
| setSignal(this.Ut[e], t[e]); | ||
| r[e] = this.Ut[e - O]; | ||
| setSignal(r[e], t[e]); | ||
| } | ||
| if (n) { | ||
| this.$t[e] = n[e]; | ||
| setSignal(this.$t[e], e); | ||
| n[e] = this.$t[e - O]; | ||
| if (O !== 0) setSignal(n[e], e); | ||
| } | ||
| } | ||
| this.xt = m; | ||
| this.Ft = _; | ||
| r && (this.Ut = r); | ||
| n && (this.$t = n); | ||
| this.jt = s; | ||
| // save a copy of the mapped items for the next update | ||
| this.Mt = t.slice(0); | ||
| 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.xt = this.xt.slice(0, this.jt = s); | ||
| // 5) save a copy of the mapped items for the next update | ||
| this.Mt = t.slice(0); | ||
| } | ||
@@ -196,0 +220,0 @@ }); |
@@ -19,3 +19,3 @@ import { computed } from "../core/core.js"; | ||
| function createOptimisticStore(e, t, r) { | ||
| function createOptimisticStore(e, t, i) { | ||
| // Register clear function with scheduler; store nodes marked | ||
@@ -26,7 +26,7 @@ // STORE_OPTIMISTIC take the engine's write path, so install it before any | ||
| GlobalQueue.Dt ||= clearOptimisticStores; | ||
| const i = typeof e === "function"; | ||
| const o = i ? t : e; | ||
| const n = i ? e : undefined; | ||
| const r = typeof e === "function"; | ||
| const o = r ? t : e; | ||
| const n = r ? e : undefined; | ||
| // Create optimistic projection store | ||
| const {store: c} = createOptimisticProjectionInternal(n, o, r); | ||
| const {store: c} = createOptimisticProjectionInternal(n, o, i); | ||
| return [ c, e => storeSetter(c, e) ]; | ||
@@ -40,4 +40,4 @@ } | ||
| function clearOptimisticStores(e, t) { | ||
| for (const r of e) { | ||
| const e = r[$TARGET]; | ||
| for (const i of e) { | ||
| const e = i[$TARGET]; | ||
| if (e?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(e, t); | ||
@@ -60,5 +60,5 @@ } | ||
| */ function clearOptimisticOverride(e, t) { | ||
| const r = e[STORE_OPTIMISTIC_OVERRIDE]; | ||
| if (!r) return; | ||
| const i = e[STORE_NODE]; | ||
| const i = e[STORE_OPTIMISTIC_OVERRIDE]; | ||
| if (!i) return; | ||
| const r = e[STORE_NODE]; | ||
| const o = e[STORE_OPTIMISTIC_OWNERS]; | ||
@@ -73,3 +73,3 @@ const n = t !== undefined; | ||
| try { | ||
| for (const O of Reflect.ownKeys(r)) { | ||
| for (const O of Reflect.ownKeys(i)) { | ||
| if (n) { | ||
@@ -91,6 +91,6 @@ let e = o?.[O] ?? null; | ||
| } | ||
| delete r[O]; | ||
| delete i[O]; | ||
| if (o) delete o[O]; | ||
| c = true; | ||
| const T = i?.[O]; | ||
| const T = r?.[O]; | ||
| if (T) { | ||
@@ -102,5 +102,5 @@ // Clear lane association so effects go to regular queue | ||
| 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 i = t ? t[O] : e[STORE_VALUE][O]; | ||
| const r = i === $DELETED ? undefined : i; | ||
| const o = isWrappable(r) ? wrap(r, e) : r; | ||
| const n = visibleNodeValue(T); | ||
@@ -118,8 +118,10 @@ T.Ee = NOT_PENDING; | ||
| if (!s) { | ||
| delete e[STORE_OPTIMISTIC_OVERRIDE]; | ||
| delete e[STORE_OPTIMISTIC_OWNERS]; | ||
| // Assignment, not delete: StoreNode targets share one pre-initialized | ||
| // hidden class (see createStoreProxy) and a delete would demote it. | ||
| e[STORE_OPTIMISTIC_OVERRIDE] = undefined; | ||
| e[STORE_OPTIMISTIC_OWNERS] = undefined; | ||
| } | ||
| // Notify $TRACK | ||
| if (c && i?.[$TRACK]) { | ||
| i[$TRACK].je = undefined; | ||
| if (c && r?.[$TRACK]) { | ||
| r[$TRACK].je = undefined; | ||
| notifySelf(e); | ||
@@ -132,4 +134,4 @@ } | ||
| function createOptimisticProjectionInternal(e, t, r) { | ||
| let i; | ||
| function createOptimisticProjectionInternal(e, t, i) { | ||
| let r; | ||
| const o = new WeakMap; | ||
@@ -143,3 +145,3 @@ const wrapper = e => { | ||
| get() { | ||
| return i; | ||
| return r; | ||
| }, | ||
@@ -178,6 +180,6 @@ configurable: true | ||
| }; | ||
| i = computed(() => { | ||
| r = computed(() => { | ||
| setProjectionWriteActive(true); | ||
| try { | ||
| runProjectionComputed(n, e, r?.key || "id", wrapCommit, clearProjectionOverride); | ||
| runProjectionComputed(n, e, i?.key || "id", wrapCommit, clearProjectionOverride); | ||
| } finally { | ||
@@ -187,7 +189,7 @@ setProjectionWriteActive(false); | ||
| }, undefined); | ||
| i.T &= ~CONFIG_AUTO_DISPOSE; | ||
| r.T &= ~CONFIG_AUTO_DISPOSE; | ||
| } | ||
| return { | ||
| store: n, | ||
| node: i | ||
| node: r | ||
| }; | ||
@@ -194,0 +196,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_DESC, STORE_HAS, getKeys, $DELETED, symbolKeyedRecords, getStoreSymbols } 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, symbolKeyedRecords, getKeys, $DELETED, getStoreSymbols } from "./store.js"; | ||
@@ -159,5 +159,5 @@ // Enumerate a node record's keys. Keeps the common string-key path on the | ||
| 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); | ||
| const y = unwrap(t[f]); | ||
| if (c === y || !isWrappable(y) || Array.isArray(c) !== Array.isArray(y) || a(c) != null && a(c) !== a(y)) continue; | ||
| applyState(y, wrap(c, r), a); | ||
| } | ||
@@ -196,3 +196,3 @@ } | ||
| if (e.length && o && isWrappable(e[0]) && r(e[0]) != null) { | ||
| let l, s, p, f, c, u, S, y; | ||
| let l, s, p, f, c, u, y, S; | ||
| for (p = 0, f = Math.min(o, e.length); p < f && keyedMatch(u = n[p], e[p], r); p++) { | ||
@@ -220,18 +220,18 @@ isWrappable(u) && isWrappable(e[p]) && applyState(e[p], wrap(u, t), r); | ||
| } | ||
| S = new Array(c + 1); | ||
| y = new Array(c + 1); | ||
| for (s = c; s >= p; s--) { | ||
| u = e[s]; | ||
| y = itemKey(u, r); | ||
| l = O.get(y); | ||
| S[s] = l === undefined ? -1 : l; | ||
| O.set(y, s); | ||
| S = itemKey(u, r); | ||
| l = O.get(S); | ||
| y[s] = l === undefined ? -1 : l; | ||
| O.set(S, s); | ||
| } | ||
| for (l = p; l <= f; l++) { | ||
| u = n[l]; | ||
| y = itemKey(u, r); | ||
| s = O.get(y); | ||
| S = itemKey(u, r); | ||
| s = O.get(S); | ||
| if (s !== undefined && s !== -1) { | ||
| E[s] = u; | ||
| s = S[s]; | ||
| O.set(y, s); | ||
| s = y[s]; | ||
| O.set(S, s); | ||
| } | ||
@@ -267,13 +267,14 @@ } | ||
| 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]; | ||
| const f = unwrap(n[s]); | ||
| let c = unwrap(e[s]); | ||
| if (f === c) continue; | ||
| if (!f || !isWrappable(f) || !isWrappable(c) || Array.isArray(f) !== Array.isArray(c) || r(f) != null && r(f) !== r(c)) { | ||
| o && setSignal(o, void 0); | ||
| p && setSignal(p, isWrappable(c) ? wrap(c, t) : c); | ||
| } else applyState(c, wrap(f, t), r); | ||
| if (o || symbolKeyedRecords.has(i)) { | ||
| const a = o ? getAllKeys(n, undefined, e) : nodeKeys(i); | ||
| for (let l = 0, s = a.length; l < s; l++) { | ||
| diffNodeKey(a[l], i, n, e, t, o, r); | ||
| } | ||
| } else { | ||
| // Untracked, string-only node records (the overwhelmingly common case) | ||
| // iterate in place — nodeKeys() allocated a fresh key array per object | ||
| // per pass, which dominates allocation on large-graph reconciles. | ||
| for (const a in i) { | ||
| diffNodeKey(a, i, n, e, t, o, r); | ||
| } | ||
| } | ||
@@ -292,2 +293,15 @@ } | ||
| // One node-key step of the fast object diff — shared by the array-iterating | ||
| // (tracked / symbol-keyed) and for-in (plain) loops in applyStateFast. | ||
| function diffNodeKey(e, t, r, n, a, i, o) { | ||
| const l = t[e]; | ||
| const s = unwrap(r[e]); | ||
| let p = unwrap(n[e]); | ||
| if (s === p) return; | ||
| if (!s || !isWrappable(s) || !isWrappable(p) || Array.isArray(s) !== Array.isArray(p) || o(s) != null && o(s) !== o(p)) { | ||
| i && setSignal(i, void 0); | ||
| l && setSignal(l, isWrappable(p) ? wrap(p, a) : p); | ||
| } else applyState(p, wrap(s, a), o); | ||
| } | ||
| function applyStateSlow(e, t, r) { | ||
@@ -307,13 +321,13 @@ const n = t[STORE_VALUE]; | ||
| if (e.length && s && isWrappable(e[0]) && r(e[0]) != null) { | ||
| 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); | ||
| let p, f, c, u, y, S, E, O; | ||
| for (c = 0, u = Math.min(s, e.length); c < u && keyedMatch(S = getOverrideValue(n, a, c, i), e[c], r); c++) { | ||
| isWrappable(S) && isWrappable(e[c]) && applyState(e[c], wrap(S, t), r); | ||
| } | ||
| const 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; | ||
| const d = new Array(e.length), R = new Map; | ||
| for (u = s - 1, y = e.length - 1; u >= c && y >= c && keyedMatch(S = getOverrideValue(n, a, u, i), e[y], r); u--, | ||
| y--) { | ||
| d[y] = S; | ||
| } | ||
| if (c > S || c > u) { | ||
| for (f = c; f <= S; f++) { | ||
| if (c > y || c > u) { | ||
| for (f = c; f <= y; f++) { | ||
| l = true; | ||
@@ -324,3 +338,3 @@ o?.[f] && setSignal(o[f], wrapValue(e[f], t)); | ||
| l = true; | ||
| applyArrayItem(e[f], R[f], t, o?.[f], r); | ||
| applyArrayItem(e[f], d[f], t, o?.[f], r); | ||
| } | ||
@@ -333,23 +347,23 @@ const n = e.length; | ||
| } | ||
| E = new Array(S + 1); | ||
| for (f = S; f >= c; f--) { | ||
| y = e[f]; | ||
| O = itemKey(y, r); | ||
| p = d.get(O); | ||
| E = new Array(y + 1); | ||
| for (f = y; f >= c; f--) { | ||
| S = e[f]; | ||
| O = itemKey(S, r); | ||
| p = R.get(O); | ||
| E[f] = p === undefined ? -1 : p; | ||
| d.set(O, f); | ||
| R.set(O, f); | ||
| } | ||
| for (p = c; p <= u; p++) { | ||
| y = getOverrideValue(n, a, p, i); | ||
| O = itemKey(y, r); | ||
| f = d.get(O); | ||
| S = getOverrideValue(n, a, p, i); | ||
| O = itemKey(S, r); | ||
| f = R.get(O); | ||
| if (f !== undefined && f !== -1) { | ||
| R[f] = y; | ||
| d[f] = S; | ||
| f = E[f]; | ||
| d.set(O, f); | ||
| R.set(O, f); | ||
| } | ||
| } | ||
| for (f = c; f < e.length; f++) { | ||
| if (f in R) { | ||
| applyArrayItem(e[f], R[f], t, o?.[f], r); | ||
| if (f in d) { | ||
| applyArrayItem(e[f], d[f], t, o?.[f], r); | ||
| } else o?.[f] && setSignal(o[f], wrapValue(e[f], t)); | ||
@@ -385,8 +399,8 @@ } | ||
| const u = unwrap(getOverrideValue(n, a, f, i)); | ||
| 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)) { | ||
| let y = unwrap(e[f]); | ||
| if (u === y) continue; | ||
| if (!u || !isWrappable(u) || !isWrappable(y) || Array.isArray(u) !== Array.isArray(y) || r(u) != null && r(u) !== r(y)) { | ||
| l && setSignal(l, void 0); | ||
| c && setSignal(c, isWrappable(S) ? wrap(S, t) : S); | ||
| } else applyState(S, wrap(u, t), r); | ||
| c && setSignal(c, isWrappable(y) ? wrap(y, t) : y); | ||
| } else applyState(y, wrap(u, t), r); | ||
| } | ||
@@ -405,13 +419,23 @@ } | ||
| // No-key reconcile: every item reports "no key", which routes array diffs to | ||
| // the positional branch and object descent to plain per-property merging. | ||
| const NOKEY = () => null; | ||
| /** | ||
| * Returns a draft-mutating function that smart-merges `value` into a store, | ||
| * preserving the identity of items whose `key` field matches between old and | ||
| * new states. Useful when applying server payloads or full-replacement data | ||
| * onto an existing store without losing fine-grained reactivity. | ||
| * preserving fine-grained reactivity: only changed leaves trigger updates. | ||
| * | ||
| * Items with the same key are updated in place (only changed properties | ||
| * trigger updates). Items added or removed update the corresponding signals. | ||
| * With a `key` (default `"id"`), array items whose key matches between old | ||
| * and new states keep their identity (updated in place, moves and removals | ||
| * update the corresponding signals) — the shape for keyed server payloads. | ||
| * Items without the key field fall back to positional matching. | ||
| * | ||
| * With `key: null`, matching is purely positional: index N of the new array | ||
| * merges into index N of the old, and object properties merge recursively — | ||
| * the classic pattern for fixed-shape data that churns in place (dashboards, | ||
| * monitors), where no keyed diff pass is needed or wanted. | ||
| * | ||
| * @param value the next state to merge in | ||
| * @param key property name (string) or extractor function for stable identity | ||
| * @param key property name (string) or extractor function for stable | ||
| * identity (default `"id"`); pass `null` for positional merging | ||
| * | ||
@@ -424,8 +448,15 @@ * @example | ||
| * const fresh = await api.getTodos(); | ||
| * setTodos(reconcile(fresh, "id")); // diff-merge by `id` | ||
| * setTodos(reconcile(fresh)); // diff-merge by `id` | ||
| * } | ||
| * | ||
| * // fixed-shape polling data — positional merge | ||
| * setStats(reconcile(nextStats, null)); | ||
| * ``` | ||
| */ function reconcile(e, t) { | ||
| */ function reconcile(e, t = "id") { | ||
| return r => { | ||
| if (r == null) throw new Error(""); | ||
| if (t === null) { | ||
| applyState(e, r, NOKEY); | ||
| return; | ||
| } | ||
| const n = typeof t === "string" ? e => e[t] : t; | ||
@@ -432,0 +463,0 @@ const a = n(r); |
+142
-102
@@ -35,2 +35,25 @@ import { $REFRESH, STORE_SNAPSHOT_PROPS, NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, STATUS_PENDING, NO_SNAPSHOT } from "../core/constants.js"; | ||
| // Every StoreNode field is initialized up front, in one fixed order, so all | ||
| // targets share a single hidden class. The traps and reconcile read these | ||
| // fields on their hottest paths; fields added lazily in varying orders made | ||
| // those loads megamorphic across a large store graph (dictionary-mode probes | ||
| // on every trap hit). Assignments elsewhere must never `delete` a field — | ||
| // write `undefined` instead, or the shape degrades again. | ||
| function initStoreFields(e) { | ||
| e[STORE_OVERRIDE] = undefined; | ||
| e[STORE_OPTIMISTIC_OVERRIDE] = undefined; | ||
| e[STORE_OPTIMISTIC_OWNERS] = undefined; | ||
| e[STORE_NODE] = undefined; | ||
| e[STORE_HAS] = undefined; | ||
| e[STORE_CUSTOM_PROTO] = undefined; | ||
| e[STORE_WRAP] = undefined; | ||
| e[STORE_LOOKUP] = undefined; | ||
| e[STORE_FIREWALL] = undefined; | ||
| e[STORE_OPTIMISTIC] = undefined; | ||
| e[STORE_SNAPSHOT_PROPS] = undefined; | ||
| e[STORE_PARENT] = undefined; | ||
| e[STORE_DESC] = undefined; | ||
| e[$PROXY] = null; | ||
| } | ||
| function createStoreProxy(e, t = storeTraps, r) { | ||
@@ -40,7 +63,9 @@ let n; | ||
| n = []; | ||
| n.v = e; | ||
| n[STORE_VALUE] = e; | ||
| initStoreFields(n); | ||
| } else { | ||
| n = { | ||
| v: e | ||
| [STORE_VALUE]: e | ||
| }; | ||
| initStoreFields(n); | ||
| const t = e?.[$TARGET]?.[STORE_VALUE] ?? e; | ||
@@ -102,13 +127,13 @@ const r = Object.getPrototypeOf(t); | ||
| const i = n[STORE_VALUE]; | ||
| const s = Array.isArray(i); | ||
| const E = s ? [] : Object.create(Object.getPrototypeOf(i)); | ||
| t.set(e, E); | ||
| const E = Array.isArray(i); | ||
| const O = E ? [] : Object.create(Object.getPrototypeOf(i)); | ||
| t.set(e, O); | ||
| r = n[STORE_LOOKUP] ?? storeLookup; | ||
| for (const e of getStoreKeys(i, o)) { | ||
| if (s && e === "length") continue; | ||
| if (E && e === "length") continue; | ||
| const n = e in o ? o[e] : i[e]; | ||
| if (n !== $DELETED) E[e] = unwrapStoreValue(n, t, r); | ||
| if (n !== $DELETED) O[e] = unwrapStoreValue(n, t, r); | ||
| } | ||
| if (s) E.length = o.length ?? i.length; | ||
| return E; | ||
| if (E) O.length = o.length ?? i.length; | ||
| return O; | ||
| } | ||
@@ -188,6 +213,6 @@ | ||
| if (t[r]) return t[r]; | ||
| const s = signal(n, { | ||
| const E = signal(n, { | ||
| equals: o, | ||
| unobserved() { | ||
| if (t[r] === s) { | ||
| if (t[r] === E) { | ||
| delete t[r]; | ||
@@ -212,8 +237,8 @@ // Drop the symbol-record mark once the last user symbol node is | ||
| if (e[STORE_OPTIMISTIC]) { | ||
| s.Ee = NOT_PENDING; | ||
| E.Ee = NOT_PENDING; | ||
| } | ||
| if (i && r in i) { | ||
| const e = i[r]; | ||
| s.Ve = e === undefined ? NO_SNAPSHOT : e; | ||
| snapshotSources?.add(s); | ||
| E.Ve = e === undefined ? NO_SNAPSHOT : e; | ||
| snapshotSources?.add(E); | ||
| } | ||
@@ -224,3 +249,3 @@ if (typeof r === "symbol" && r !== $TRACK && r !== $AFFECTS) symbolKeyedRecords.add(t); | ||
| // record's own $AFFECTS carrier is the mark's channel, never a member. | ||
| if (r !== $AFFECTS && affectsScopes.size) inheritAffectsMarks(s, e[STORE_VALUE], r); | ||
| if (r !== $AFFECTS && affectsScopes.size) inheritAffectsMarks(E, e[STORE_VALUE], r); | ||
| // Node presence bubbles up the wrap chain (sticky), so reconcile can see | ||
@@ -230,8 +255,8 @@ // "subscribers live somewhere below" through node-less intermediate | ||
| // the first already-flagged ancestor. | ||
| let E = e; | ||
| while (E && !E[STORE_DESC]) { | ||
| E[STORE_DESC] = true; | ||
| E = E[STORE_PARENT]; | ||
| let O = e; | ||
| while (O && !O[STORE_DESC]) { | ||
| O[STORE_DESC] = true; | ||
| O = O[STORE_PARENT]; | ||
| } | ||
| return t[r] = s; | ||
| return t[r] = E; | ||
| } | ||
@@ -273,11 +298,11 @@ | ||
| const i = e[$TARGET] || (n ?? storeLookup).get(e)?.[$TARGET]; | ||
| const s = i ? i[STORE_VALUE] : e; | ||
| if (o.has(s)) return; | ||
| o.add(s); | ||
| t.scope.add(s); | ||
| let E; | ||
| const E = i ? i[STORE_VALUE] : e; | ||
| if (o.has(E)) return; | ||
| o.add(E); | ||
| t.scope.add(E); | ||
| let O; | ||
| if (i) { | ||
| collectRecordNodes(i[STORE_NODE], r); | ||
| collectRecordNodes(i[STORE_HAS], r); | ||
| E = mergedOverlay(i); | ||
| O = mergedOverlay(i); | ||
| // Carry the effective lookup into untouched descendants. Default stores | ||
@@ -288,6 +313,6 @@ // use the global lookup just like snapshotImpl; without it, nested raw | ||
| } | ||
| if (Array.isArray(s)) { | ||
| const e = E?.length ?? s.length; | ||
| if (Array.isArray(E)) { | ||
| const e = O?.length ?? E.length; | ||
| for (let i = 0; i < e; i++) { | ||
| const e = E && i in E ? E[i] : s[i]; | ||
| const e = O && i in O ? O[i] : E[i]; | ||
| if (e !== $DELETED) walkAffectsScope(e, t, r, n, o); | ||
@@ -298,15 +323,15 @@ } | ||
| // 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); | ||
| const s = i || n ? getStoreSymbols(E, O) : []; | ||
| for (let e = 0, i = s.length; e < i; e++) { | ||
| const i = s[e]; | ||
| const S = getPropertyDescriptor(E, O, i); | ||
| if (!S || S.get) continue; | ||
| walkAffectsScope(S.value, t, r, n, o); | ||
| } | ||
| } else { | ||
| const e = i || n ? getStoreKeys(s, E) : getKeys(s, E); | ||
| for (let i = 0, O = e.length; i < O; i++) { | ||
| const O = getPropertyDescriptor(s, E, e[i]); | ||
| if (!O || O.get) continue; | ||
| walkAffectsScope(O.value, t, r, n, o); | ||
| const e = i || n ? getStoreKeys(E, O) : getKeys(E, O); | ||
| for (let i = 0, s = e.length; i < s; i++) { | ||
| const s = getPropertyDescriptor(E, O, e[i]); | ||
| if (!s || s.get) continue; | ||
| walkAffectsScope(s.value, t, r, n, o); | ||
| } | ||
@@ -502,6 +527,6 @@ } | ||
| const i = e[STORE_OPTIMISTIC] && !projectionWriteActive; | ||
| const s = i ? STORE_OPTIMISTIC_OVERRIDE : STORE_OVERRIDE; | ||
| const E = i ? STORE_OPTIMISTIC_OVERRIDE : STORE_OVERRIDE; | ||
| return { | ||
| base: o, | ||
| overrideKey: s, | ||
| overrideKey: E, | ||
| state: n | ||
@@ -540,5 +565,5 @@ }; | ||
| const i = isWrappable(n) ? wrap(n, e) : n; | ||
| const s = getNode(e, t, r, i, isEqual, o); | ||
| registerTransientStoreNode(s); | ||
| return s; | ||
| const E = getNode(e, t, r, i, isEqual, o); | ||
| registerTransientStoreNode(E); | ||
| return E; | ||
| } | ||
@@ -550,29 +575,29 @@ | ||
| // (whose whole purpose is immediate visibility via STORE_OPTIMISTIC_OVERRIDE). | ||
| const s = projectionWriteActive || e[STORE_OPTIMISTIC]; | ||
| const E = r !== "delete"; | ||
| const O = e[STORE_HAS]?.[t]; | ||
| if (O) { | ||
| setSignal(O, E); | ||
| } else if (!s && r !== "invalidate" && i !== E) { | ||
| const E = projectionWriteActive || e[STORE_OPTIMISTIC]; | ||
| const O = r !== "delete"; | ||
| const s = e[STORE_HAS]?.[t]; | ||
| if (s) { | ||
| setSignal(s, O); | ||
| } else if (!E && r !== "invalidate" && i !== O) { | ||
| const r = upsertStoreNode(e, getNodes(e, STORE_HAS), t, i); | ||
| setSignal(r, E); | ||
| setSignal(r, O); | ||
| } | ||
| const c = getNodes(e, STORE_NODE); | ||
| const S = getNodes(e, STORE_NODE); | ||
| if (r === "set") { | ||
| if (c[t]) { | ||
| setSignal(c[t], () => isWrappable(n) ? wrap(n, e) : n); | ||
| } else if (!s) { | ||
| const r = upsertStoreNode(e, c, t, o, e[STORE_SNAPSHOT_PROPS]); | ||
| if (S[t]) { | ||
| setSignal(S[t], () => isWrappable(n) ? wrap(n, e) : n); | ||
| } else if (!E) { | ||
| const r = upsertStoreNode(e, S, t, o, e[STORE_SNAPSHOT_PROPS]); | ||
| setSignal(r, () => isWrappable(n) ? wrap(n, e) : n); | ||
| } | ||
| } else if (r === "invalidate") { | ||
| if (c[t]) { | ||
| setSignal(c[t], {}); | ||
| delete c[t]; | ||
| if (S[t]) { | ||
| setSignal(S[t], {}); | ||
| delete S[t]; | ||
| } | ||
| } else { | ||
| if (c[t]) { | ||
| setSignal(c[t], undefined); | ||
| } else if (!s) { | ||
| const r = upsertStoreNode(e, c, t, o, e[STORE_SNAPSHOT_PROPS]); | ||
| if (S[t]) { | ||
| setSignal(S[t], undefined); | ||
| } else if (!E) { | ||
| const r = upsertStoreNode(e, S, t, o, e[STORE_SNAPSHOT_PROPS]); | ||
| setSignal(r, undefined); | ||
@@ -609,20 +634,35 @@ } | ||
| } | ||
| // Hot path: an existing node on a plain target — no firewall (so neither | ||
| // selfRead nor the uninitialized guard can apply), no override layers, | ||
| // no write scope, and a raw (non-proxy) source. This is the shape of | ||
| // every effect re-read of a settled store; it pays one node read and the | ||
| // wrap check, nothing else. Any exotic bit falls through to the full | ||
| // resolution below, and dev strictRead keeps its warning path. | ||
| if (e[STORE_FIREWALL] === undefined && e[STORE_OVERRIDE] === undefined && e[STORE_OPTIMISTIC_OVERRIDE] === undefined && !writeOverride && (Writing === null || !Writing.has(r))) { | ||
| const r = e[STORE_NODE]; | ||
| const n = r && r[t]; | ||
| if (n !== undefined && e[STORE_VALUE][$TARGET] === undefined) { | ||
| let t = read(n); | ||
| if (t === $DELETED) t = undefined; | ||
| return isWrappable(t) ? wrap(t, e) : t; | ||
| } | ||
| } | ||
| const n = getObserver() === e[STORE_FIREWALL]; | ||
| const o = getNodes(e, STORE_NODE); | ||
| const i = n ? undefined : o[t]; | ||
| const s = e[STORE_VALUE]; | ||
| if (!i && !e[STORE_OVERRIDE] && !e[STORE_OPTIMISTIC_OVERRIDE] && !e[STORE_CUSTOM_PROTO] && !e[STORE_OPTIMISTIC] && !e[STORE_SNAPSHOT_PROPS] && !s[$TARGET] && !(t in s) && getObserver() && !n && !writeOnly(r)) { | ||
| const E = e[STORE_VALUE]; | ||
| if (!i && !e[STORE_OVERRIDE] && !e[STORE_OPTIMISTIC_OVERRIDE] && !e[STORE_CUSTOM_PROTO] && !e[STORE_OPTIMISTIC] && !e[STORE_SNAPSHOT_PROPS] && !E[$TARGET] && !(t in E) && getObserver() && !n && !writeOnly(r)) { | ||
| return read(getNode(e, o, t, undefined)); | ||
| } | ||
| const E = getOverlayLayer(e, t); | ||
| const O = !!E; | ||
| const c = !!e[STORE_VALUE][$TARGET]; | ||
| const S = E ?? e[STORE_VALUE]; | ||
| const O = getOverlayLayer(e, t); | ||
| const s = !!O; | ||
| const S = !!e[STORE_VALUE][$TARGET]; | ||
| const c = O ?? e[STORE_VALUE]; | ||
| if (!i) { | ||
| const n = Object.getOwnPropertyDescriptor(S, t); | ||
| const n = Object.getOwnPropertyDescriptor(c, t); | ||
| if (n && n.get) return n.get.call(r); | ||
| if (!n && !O && e[STORE_CUSTOM_PROTO]) { | ||
| const e = unwrapStoreValue(S); | ||
| if (!n && !s && e[STORE_CUSTOM_PROTO]) { | ||
| const e = unwrapStoreValue(c); | ||
| if (hasInheritedAccessor(e, t)) { | ||
| return Reflect.get(S, t, r); | ||
| return Reflect.get(c, t, r); | ||
| } | ||
@@ -633,3 +673,3 @@ } | ||
| if (isPrototypePollutionKey(t) && !hasOwnStoreProperty(e, t)) return undefined; | ||
| let r = i && (O || !c) ? visibleNodeValue(i) : S[t]; | ||
| let r = i && (s || !S) ? visibleNodeValue(i) : c[t]; | ||
| r === $DELETED && (r = undefined); | ||
@@ -641,10 +681,10 @@ if (!isWrappable(r)) return r; | ||
| } | ||
| let f = i ? O || !c ? read(o[t]) : (read(o[t]), S[t]) : S[t]; | ||
| f === $DELETED && (f = undefined); | ||
| let T = i ? s || !S ? read(o[t]) : (read(o[t]), c[t]) : c[t]; | ||
| T === $DELETED && (T = undefined); | ||
| if (!i) { | ||
| if (!O && typeof f === "function" && !Object.prototype.hasOwnProperty.call(S, t)) { | ||
| if (!s && typeof T === "function" && !Object.prototype.hasOwnProperty.call(c, t)) { | ||
| let t; | ||
| return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? f.bind(S) : f; | ||
| return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? T.bind(c) : T; | ||
| } else if (getObserver() && !n) { | ||
| return read(getNode(e, o, t, isWrappable(f) ? wrap(f, e) : f, isEqual, e[STORE_SNAPSHOT_PROPS])); | ||
| return read(getNode(e, o, t, isWrappable(T) ? wrap(T, e) : T, isEqual, e[STORE_SNAPSHOT_PROPS])); | ||
| } | ||
@@ -655,3 +695,3 @@ } | ||
| if (!n) throwIfUninitialized(e); | ||
| return isWrappable(f) ? wrap(f, e) : f; | ||
| return isWrappable(T) ? wrap(T, e) : T; | ||
| }, | ||
@@ -683,18 +723,18 @@ has(e, t) { | ||
| untrack(() => { | ||
| const {base: o, overrideKey: i, state: s} = prepareStoreWrite(e, n, t); | ||
| const E = getOverlayLayer(e, t); | ||
| const O = E ? E[t] : o; | ||
| const c = E ? E[t] !== $DELETED : t in e[STORE_VALUE]; | ||
| const S = unwrapStoreValue(r); | ||
| const {base: o, overrideKey: i, state: E} = prepareStoreWrite(e, n, t); | ||
| const O = getOverlayLayer(e, t); | ||
| const s = O ? O[t] : o; | ||
| const S = O ? O[t] !== $DELETED : t in e[STORE_VALUE]; | ||
| const c = 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 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; | ||
| const T = typeof t === "string" ? Number(t) : -1; | ||
| const R = Array.isArray(E) && Number.isInteger(T) && T >= 0 && T < 4294967295 && String(T) === t; | ||
| const f = R ? T + 1 : 0; | ||
| const u = R && (getOverlayLayer(e, "length") ?? E).length; | ||
| const l = R && f > u ? f : undefined; | ||
| if (s === c && l === undefined) return true; | ||
| armOptimisticStoreWrite(e, n); | ||
| if (S !== undefined && S === o && l === undefined) { | ||
| if (c !== undefined && c === o && l === undefined) { | ||
| delete e[i]?.[t]; | ||
@@ -704,3 +744,3 @@ if (i === STORE_OPTIMISTIC_OVERRIDE) delete e[STORE_OPTIMISTIC_OWNERS]?.[t]; | ||
| const r = e[i] || (e[i] = Object.create(null)); | ||
| r[t] = S; | ||
| r[t] = c; | ||
| stampOptimisticOwner(e, i, t); | ||
@@ -712,12 +752,12 @@ if (l !== undefined) { | ||
| } | ||
| notifyStoreProperty(e, t, "set", S, O, c); | ||
| notifyStoreProperty(e, t, "set", c, s, S); | ||
| // 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 S === "number" && typeof O === "number" && S < O) { | ||
| if (Array.isArray(E) && t === "length" && typeof c === "number" && typeof s === "number" && c < s) { | ||
| const t = e[i] || (e[i] = Object.create(null)); | ||
| for (let r = S; r < O; r++) { | ||
| for (let r = c; r < s; r++) { | ||
| if (t[r] === $DELETED) continue; | ||
| const n = r in t ? t[r] : s[r]; | ||
| if (!(r in t) && !(r in s)) continue; | ||
| const n = r in t ? t[r] : E[r]; | ||
| if (!(r in t) && !(r in E)) continue; | ||
| t[r] = $DELETED; | ||
@@ -729,3 +769,3 @@ stampOptimisticOwner(e, i, r); | ||
| // notify length change | ||
| if (Array.isArray(s) && t !== "length" && l !== undefined) { | ||
| if (Array.isArray(E) && t !== "length" && l !== undefined) { | ||
| const t = getNodes(e, STORE_NODE); | ||
@@ -751,7 +791,7 @@ if (t.length) { | ||
| armOptimisticStoreWrite(e, n); | ||
| const s = "value" in r ? { | ||
| const E = "value" in r ? { | ||
| ...r, | ||
| value: unwrapStoreValue(r.value) | ||
| } : r; | ||
| Object.defineProperty(e[i] || (e[i] = Object.create(null)), t, s); | ||
| Object.defineProperty(e[i] || (e[i] = Object.create(null)), t, E); | ||
| stampOptimisticOwner(e, i, t); | ||
@@ -758,0 +798,0 @@ notifyStoreProperty(e, t, "invalidate"); |
@@ -30,2 +30,14 @@ import { pendingCheckActive } from "../core/core.js"; | ||
| i = mergedOverlay(o); | ||
| // A derived store's STORE_VALUE is the inner store's live proxy | ||
| // (store-in-store: createOptimisticStore/createProjection over a store). | ||
| // Without an overlay of its own, the fast path below would map to — and | ||
| // could return — that proxy verbatim. Recurse instead so the | ||
| // inner store's own target branch unwraps it (chains of any depth). | ||
| // With an overlay, a fresh `result` is always built, so the walk over the | ||
| // inner proxy already copies plain values. | ||
| if (!i && o[STORE_VALUE][$TARGET]) { | ||
| f = snapshotImpl(o[STORE_VALUE], t, r, n); | ||
| r.set(e, f); | ||
| return f; | ||
| } | ||
| s = Array.isArray(o[STORE_VALUE]); | ||
@@ -32,0 +44,0 @@ r.set(e, i ? c = s ? [] : Object.create(Object.getPrototypeOf(o[STORE_VALUE])) : o[STORE_VALUE]); |
@@ -26,3 +26,2 @@ /** | ||
| export declare class NotReadyError extends Error { | ||
| source: any; | ||
| /** | ||
@@ -35,2 +34,3 @@ * Tags a visibility-only notification on the affects() boundary channel: | ||
| _markVisual?: boolean; | ||
| source: any; | ||
| constructor(source: any); | ||
@@ -37,0 +37,0 @@ } |
| /** | ||
| * Returns a draft-mutating function that smart-merges `value` into a store, | ||
| * preserving the identity of items whose `key` field matches between old and | ||
| * new states. Useful when applying server payloads or full-replacement data | ||
| * onto an existing store without losing fine-grained reactivity. | ||
| * preserving fine-grained reactivity: only changed leaves trigger updates. | ||
| * | ||
| * Items with the same key are updated in place (only changed properties | ||
| * trigger updates). Items added or removed update the corresponding signals. | ||
| * With a `key` (default `"id"`), array items whose key matches between old | ||
| * and new states keep their identity (updated in place, moves and removals | ||
| * update the corresponding signals) — the shape for keyed server payloads. | ||
| * Items without the key field fall back to positional matching. | ||
| * | ||
| * With `key: null`, matching is purely positional: index N of the new array | ||
| * merges into index N of the old, and object properties merge recursively — | ||
| * the classic pattern for fixed-shape data that churns in place (dashboards, | ||
| * monitors), where no keyed diff pass is needed or wanted. | ||
| * | ||
| * @param value the next state to merge in | ||
| * @param key property name (string) or extractor function for stable identity | ||
| * @param key property name (string) or extractor function for stable | ||
| * identity (default `"id"`); pass `null` for positional merging | ||
| * | ||
@@ -19,6 +25,9 @@ * @example | ||
| * const fresh = await api.getTodos(); | ||
| * setTodos(reconcile(fresh, "id")); // diff-merge by `id` | ||
| * setTodos(reconcile(fresh)); // diff-merge by `id` | ||
| * } | ||
| * | ||
| * // fixed-shape polling data — positional merge | ||
| * setStats(reconcile(nextStats, null)); | ||
| * ``` | ||
| */ | ||
| export declare function reconcile<T extends U, U>(value: T, key: string | ((item: NonNullable<any>) => any)): (state: U) => void; | ||
| export declare function reconcile<T extends U, U>(value: T, key?: string | ((item: NonNullable<any>) => any) | null): (state: U) => void; |
@@ -26,3 +26,2 @@ /** | ||
| export declare class NotReadyError extends Error { | ||
| source: any; | ||
| /** | ||
@@ -35,2 +34,3 @@ * Tags a visibility-only notification on the affects() boundary channel: | ||
| _markVisual?: boolean; | ||
| source: any; | ||
| constructor(source: any); | ||
@@ -37,0 +37,0 @@ } |
| /** | ||
| * Returns a draft-mutating function that smart-merges `value` into a store, | ||
| * preserving the identity of items whose `key` field matches between old and | ||
| * new states. Useful when applying server payloads or full-replacement data | ||
| * onto an existing store without losing fine-grained reactivity. | ||
| * preserving fine-grained reactivity: only changed leaves trigger updates. | ||
| * | ||
| * Items with the same key are updated in place (only changed properties | ||
| * trigger updates). Items added or removed update the corresponding signals. | ||
| * With a `key` (default `"id"`), array items whose key matches between old | ||
| * and new states keep their identity (updated in place, moves and removals | ||
| * update the corresponding signals) — the shape for keyed server payloads. | ||
| * Items without the key field fall back to positional matching. | ||
| * | ||
| * With `key: null`, matching is purely positional: index N of the new array | ||
| * merges into index N of the old, and object properties merge recursively — | ||
| * the classic pattern for fixed-shape data that churns in place (dashboards, | ||
| * monitors), where no keyed diff pass is needed or wanted. | ||
| * | ||
| * @param value the next state to merge in | ||
| * @param key property name (string) or extractor function for stable identity | ||
| * @param key property name (string) or extractor function for stable | ||
| * identity (default `"id"`); pass `null` for positional merging | ||
| * | ||
@@ -19,6 +25,9 @@ * @example | ||
| * const fresh = await api.getTodos(); | ||
| * setTodos(reconcile(fresh, "id")); // diff-merge by `id` | ||
| * setTodos(reconcile(fresh)); // diff-merge by `id` | ||
| * } | ||
| * | ||
| * // fixed-shape polling data — positional merge | ||
| * setStats(reconcile(nextStats, null)); | ||
| * ``` | ||
| */ | ||
| export declare function reconcile<T extends U, U>(value: T, key: string | ((item: NonNullable<any>) => any)): (state: U) => void; | ||
| export declare function reconcile<T extends U, U>(value: T, key?: string | ((item: NonNullable<any>) => any) | null): (state: U) => void; |
+1
-1
| { | ||
| "name": "@solidjs/signals", | ||
| "version": "2.0.0-beta.23", | ||
| "version": "2.0.0-beta.24", | ||
| "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
1077913
1.73%23754
1.5%