@solidjs/signals 2.0.0-beta.23 → 2.0.0-beta.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev.js +615 -125
- package/dist/node.cjs +1551 -1092
- package/dist/prod/core/core.js +106 -69
- package/dist/prod/core/effect.js +9 -9
- package/dist/prod/core/error.js +9 -0
- package/dist/prod/core/graph.js +3 -3
- package/dist/prod/core/heap.js +12 -12
- package/dist/prod/core/optimistic.js +6 -4
- package/dist/prod/core/owner.js +10 -10
- package/dist/prod/core/scheduler.js +6 -6
- package/dist/prod/core/verdict.js +2 -2
- package/dist/prod/map.js +53 -29
- package/dist/prod/store/optimistic.js +40 -31
- package/dist/prod/store/projection.js +25 -18
- package/dist/prod/store/reconcile.js +433 -208
- package/dist/prod/store/store.js +250 -116
- package/dist/prod/store/utils.js +34 -22
- package/dist/types/core/core.d.ts +15 -0
- package/dist/types/core/error.d.ts +1 -1
- package/dist/types/store/optimistic.d.ts +1 -1
- package/dist/types/store/reconcile.d.ts +17 -8
- package/dist/types/store/store.d.ts +20 -2
- package/dist/types-cjs/core/core.d.cts +15 -0
- package/dist/types-cjs/core/error.d.cts +1 -1
- package/dist/types-cjs/store/optimistic.d.cts +1 -1
- package/dist/types-cjs/store/reconcile.d.cts +17 -8
- package/dist/types-cjs/store/store.d.cts +20 -2
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -2138,7 +2138,10 @@ function clearSnapshots() {
|
|
|
2138
2138
|
if (snapshotSources) {
|
|
2139
2139
|
for (const source of snapshotSources) {
|
|
2140
2140
|
delete source._snapshotValue;
|
|
2141
|
-
|
|
2141
|
+
// StoreNode targets share one pre-initialized hidden class (see
|
|
2142
|
+
// createStoreProxy) — assign undefined instead of deleting, and only
|
|
2143
|
+
// when present so signal-node sources don't grow the field.
|
|
2144
|
+
if (source[STORE_SNAPSHOT_PROPS] !== undefined) source[STORE_SNAPSHOT_PROPS] = undefined;
|
|
2142
2145
|
}
|
|
2143
2146
|
snapshotSources = null;
|
|
2144
2147
|
}
|
|
@@ -2227,7 +2230,21 @@ function recompute(el, create = false) {
|
|
|
2227
2230
|
value = inFlightChanged || !isAsyncResult ? fnResult : handleAsync(el, fnResult);
|
|
2228
2231
|
if (!inFlightChanged && !isAsyncResult) el._inFlight = null;
|
|
2229
2232
|
}
|
|
2230
|
-
clearStatus
|
|
2233
|
+
// On a status-free node clearStatus is a guaranteed no-op: every branch
|
|
2234
|
+
// in its body is gated on one of these fields, and with _statusFlags === 0
|
|
2235
|
+
// the flags write (create or not) stores the 0 already there.
|
|
2236
|
+
if (
|
|
2237
|
+
el._statusFlags !== 0 ||
|
|
2238
|
+
el._notifyStatus !== undefined ||
|
|
2239
|
+
el._error ||
|
|
2240
|
+
el._reask ||
|
|
2241
|
+
el._blocked ||
|
|
2242
|
+
el._pendingSources !== undefined ||
|
|
2243
|
+
el._pendingSignal !== undefined ||
|
|
2244
|
+
el._latestValueComputed !== undefined ||
|
|
2245
|
+
el._child !== null
|
|
2246
|
+
)
|
|
2247
|
+
clearStatus(el, create);
|
|
2231
2248
|
// _optimisticLane is only ever assigned by engine paths.
|
|
2232
2249
|
if (el._optimisticLane) GlobalQueue._laneAsyncSettled(el);
|
|
2233
2250
|
} catch (e) {
|
|
@@ -2288,7 +2305,15 @@ function recompute(el, create = false) {
|
|
|
2288
2305
|
if (el._error);
|
|
2289
2306
|
else if (valueChanged) {
|
|
2290
2307
|
const prevVisible = hasOverride ? el._overrideValue : undefined;
|
|
2291
|
-
if (
|
|
2308
|
+
if (
|
|
2309
|
+
create ||
|
|
2310
|
+
// Plain sync flush (no transition on either side) commits effect
|
|
2311
|
+
// values directly — the pending round-trip (queuePendingNode +
|
|
2312
|
+
// commitPendingNodes) exists to sequence transition reveals, and
|
|
2313
|
+
// paying it per effect on the plain path is pure overhead.
|
|
2314
|
+
(isEffect && (activeTransition !== el._transition || activeTransition === null)) ||
|
|
2315
|
+
isOptimisticDirty
|
|
2316
|
+
) {
|
|
2292
2317
|
el._value = value;
|
|
2293
2318
|
// Lane-propagated correction: upstream data is fresh, correct the
|
|
2294
2319
|
// override unconditionally. The direct _value commit is the lane's
|
|
@@ -2308,7 +2333,12 @@ function recompute(el, create = false) {
|
|
|
2308
2333
|
if ((activeTransition || el._transition) && GlobalQueue._syncCompanions !== null)
|
|
2309
2334
|
GlobalQueue._syncCompanions(el, value);
|
|
2310
2335
|
}
|
|
2311
|
-
|
|
2336
|
+
// insertSubs only walks _subs (no scheduling of its own), so a
|
|
2337
|
+
// subscriber-less node has nothing to notify.
|
|
2338
|
+
if (
|
|
2339
|
+
el._subs !== null &&
|
|
2340
|
+
(!hasOverride || isOptimisticDirty || el._overrideValue !== prevVisible)
|
|
2341
|
+
)
|
|
2312
2342
|
insertSubs(el, isOptimisticDirty || hasOverride);
|
|
2313
2343
|
} else if (hasOverride) {
|
|
2314
2344
|
// Unchanged value (equals the override) recomputed while the override
|
|
@@ -2607,6 +2637,39 @@ function prepareComputed(comp, refresh) {
|
|
|
2607
2637
|
updateIfNecessary(comp);
|
|
2608
2638
|
}
|
|
2609
2639
|
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Sentinel returned by readNodeFast when the plain-signal fast path does not
|
|
2642
|
+
* apply and the caller must fall back to the full read().
|
|
2643
|
+
*/
|
|
2644
|
+
const READ_SLOW = Symbol("read-slow");
|
|
2645
|
+
/**
|
|
2646
|
+
* read()'s plain-signal fast path as a standalone entry for hot callers
|
|
2647
|
+
* (store traps). Safe to substitute for read() only because the bail
|
|
2648
|
+
* conditions mirror read()'s prelude and fast-path guard exactly: the
|
|
2649
|
+
* latestRead and pendingCheck windows run side-effectful hooks before the
|
|
2650
|
+
* fast path, `_fn` nodes need prepareComputed, and firewall / override /
|
|
2651
|
+
* snapshot / transition / lane / dev-strictRead state all take the full
|
|
2652
|
+
* resolution. Anything slow returns READ_SLOW; the caller then calls read().
|
|
2653
|
+
*/
|
|
2654
|
+
function readNodeFast(el) {
|
|
2655
|
+
if (
|
|
2656
|
+
latestReadActive ||
|
|
2657
|
+
pendingCheckActive ||
|
|
2658
|
+
el._fn ||
|
|
2659
|
+
el._firewall ||
|
|
2660
|
+
el._overrideValue !== undefined ||
|
|
2661
|
+
el._snapshotValue !== undefined ||
|
|
2662
|
+
activeTransition !== null ||
|
|
2663
|
+
currentOptimisticLane !== null ||
|
|
2664
|
+
snapshotCaptureActive ||
|
|
2665
|
+
strictRead
|
|
2666
|
+
)
|
|
2667
|
+
return READ_SLOW;
|
|
2668
|
+
let c = context;
|
|
2669
|
+
if (c?._root) c = c._parentComputed;
|
|
2670
|
+
if (c && tracking) link(el, c);
|
|
2671
|
+
return !c || el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
|
|
2672
|
+
}
|
|
2610
2673
|
function read(el) {
|
|
2611
2674
|
// Handle latest() mode: read from _latestValueComputed
|
|
2612
2675
|
// Checked before isPending so that isPending(() => latest(x)) checks
|
|
@@ -2802,7 +2865,12 @@ function setSignal(el, v) {
|
|
|
2802
2865
|
if (!valueChanged) return v;
|
|
2803
2866
|
if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
|
|
2804
2867
|
el._pendingValue = v;
|
|
2805
|
-
|
|
2868
|
+
// syncCompanions only pokes _pendingSignal/_latestValueComputed — with
|
|
2869
|
+
// neither companion present the call is a guaranteed no-op (companions are
|
|
2870
|
+
// only ever created, never removed, and creating one installs the hook).
|
|
2871
|
+
(el._pendingSignal !== undefined || el._latestValueComputed !== undefined) &&
|
|
2872
|
+
GlobalQueue._syncCompanions !== null &&
|
|
2873
|
+
GlobalQueue._syncCompanions(el, v);
|
|
2806
2874
|
el._time = clock;
|
|
2807
2875
|
insertSubs(el);
|
|
2808
2876
|
schedule();
|
|
@@ -3144,7 +3212,11 @@ function optimisticWrite(el, v) {
|
|
|
3144
3212
|
// brand, and erasing it makes the write invisible and routes follow-up
|
|
3145
3213
|
// writes off the optimistic path into permanent commits (#2898).
|
|
3146
3214
|
el._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
|
|
3147
|
-
|
|
3215
|
+
// syncCompanions only pokes _pendingSignal/_latestValueComputed — with
|
|
3216
|
+
// neither companion present the call is a guaranteed no-op.
|
|
3217
|
+
(el._pendingSignal !== undefined || el._latestValueComputed !== undefined) &&
|
|
3218
|
+
GlobalQueue._syncCompanions !== null &&
|
|
3219
|
+
GlobalQueue._syncCompanions(el, v);
|
|
3148
3220
|
el._time = clock;
|
|
3149
3221
|
insertSubs(el, true);
|
|
3150
3222
|
schedule();
|
|
@@ -4588,27 +4660,70 @@ function keyedMatch(a, b, keyFn) {
|
|
|
4588
4660
|
// and `in` dependencies should follow the new value's membership. Use
|
|
4589
4661
|
// membership rather than length arithmetic so sparse arrays and named array
|
|
4590
4662
|
// props behave like normal property reads.
|
|
4663
|
+
// Inline key iteration on purpose: these loops run per array target per
|
|
4664
|
+
// reconcile pass, and a shared helper means a closure + per-key call in
|
|
4665
|
+
// instruction counts. String-only records (the common case) iterate in
|
|
4666
|
+
// place; symbol-bearing records take the nodeKeys array path.
|
|
4591
4667
|
function syncArrayNodeMembership(target, next) {
|
|
4592
4668
|
let nodes = target[STORE_NODE];
|
|
4593
4669
|
if (nodes) {
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4670
|
+
if (symbolKeyedRecords.has(nodes)) {
|
|
4671
|
+
const keys = nodeKeys(nodes);
|
|
4672
|
+
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4673
|
+
keys[i] in next || setSignal(nodes[keys[i]], undefined);
|
|
4674
|
+
}
|
|
4675
|
+
} else {
|
|
4676
|
+
for (const key in nodes) {
|
|
4677
|
+
key in next || setSignal(nodes[key], undefined);
|
|
4678
|
+
}
|
|
4598
4679
|
}
|
|
4599
4680
|
}
|
|
4600
4681
|
if ((nodes = target[STORE_HAS])) {
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4682
|
+
if (symbolKeyedRecords.has(nodes)) {
|
|
4683
|
+
const keys = nodeKeys(nodes);
|
|
4684
|
+
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4685
|
+
setSignal(nodes[keys[i]], keys[i] in next);
|
|
4686
|
+
}
|
|
4687
|
+
} else {
|
|
4688
|
+
for (const key in nodes) {
|
|
4689
|
+
setSignal(nodes[key], key in next);
|
|
4690
|
+
}
|
|
4605
4691
|
}
|
|
4606
4692
|
}
|
|
4607
4693
|
}
|
|
4694
|
+
// Recurse into a matched wrappable child pair without manufacturing a proxy:
|
|
4695
|
+
// resolve the child's target through the lookup and dispatch directly. A
|
|
4696
|
+
// lookup miss means the child was never observed — no proxy, no nodes, no
|
|
4697
|
+
// subscribers anywhere below — so the parent's swap making `next`
|
|
4698
|
+
// authoritative IS the whole update; a later read wraps next's child on
|
|
4699
|
+
// demand. This turns the diff from O(previous graph) into O(observed graph)
|
|
4700
|
+
// and drops the wrap()/$PROXY/$TARGET round-trip per visited child.
|
|
4701
|
+
// Wrap-family stores (projections/optimistic) own child proxy creation and
|
|
4702
|
+
// keep the proxy-based recursion.
|
|
4703
|
+
function applyStateChild(next, prevRaw, target, keyFn) {
|
|
4704
|
+
if (target[STORE_WRAP] !== undefined) {
|
|
4705
|
+
applyState(next, wrap(prevRaw, target), keyFn);
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
const childTarget = prevRaw[$TARGET] ?? storeLookup.get(prevRaw);
|
|
4709
|
+
if (childTarget === undefined) return;
|
|
4710
|
+
next = unwrap(next);
|
|
4711
|
+
if (childTarget[STORE_SHALLOW]) {
|
|
4712
|
+
applyStateShallow(next, childTarget);
|
|
4713
|
+
} else if (childTarget[STORE_OVERRIDE] || childTarget[STORE_OPTIMISTIC_OVERRIDE]) {
|
|
4714
|
+
applyStateSlow(next, childTarget, keyFn);
|
|
4715
|
+
} else {
|
|
4716
|
+
applyStateFast(next, childTarget, keyFn);
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4608
4719
|
// Reconcile a single array slot: recurse into a wrappable pair, otherwise replace
|
|
4609
4720
|
// the node's value outright (covers object→primitive and primitive→object).
|
|
4610
4721
|
function applyArrayItem(next, previous, target, node, keyFn) {
|
|
4611
|
-
if (
|
|
4722
|
+
if (
|
|
4723
|
+
isWrappable(next) &&
|
|
4724
|
+
isWrappable(previous) &&
|
|
4725
|
+
!(rawValuesUsed && (isRawValue(previous) || isRawValue(next)))
|
|
4726
|
+
) {
|
|
4612
4727
|
const wrapped = wrap(previous, target);
|
|
4613
4728
|
node && setSignal(node, wrapped);
|
|
4614
4729
|
applyState(next, wrapped, keyFn);
|
|
@@ -4628,27 +4743,54 @@ function applyArrayItem(next, previous, target, node, keyFn) {
|
|
|
4628
4743
|
*/
|
|
4629
4744
|
function applyDescendants(previous, next, target, nodes, keyFn, override, optOverride) {
|
|
4630
4745
|
const lookup = target[STORE_LOOKUP] || storeLookup;
|
|
4631
|
-
|
|
4632
|
-
getStoreSymbols(previous, override)
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4746
|
+
if (override) {
|
|
4747
|
+
const keys = getKeys(previous, override).concat(getStoreSymbols(previous, override));
|
|
4748
|
+
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4749
|
+
const key = keys[i];
|
|
4750
|
+
if (nodes?.[key]) continue; // main loop already diffed this slot
|
|
4751
|
+
const previousValue = unwrap(getOverrideValue(previous, override, key, optOverride));
|
|
4752
|
+
if (!isWrappable(previousValue)) continue;
|
|
4753
|
+
descendInto(previousValue, next[key], lookup, keyFn);
|
|
4754
|
+
}
|
|
4755
|
+
return;
|
|
4756
|
+
}
|
|
4757
|
+
// No-override path (every applyStateFast call): iterate in place instead of
|
|
4758
|
+
// building keys + symbols + concat arrays per object per pass. The cheap
|
|
4759
|
+
// bails (noded key, primitive value) stay inline — only genuine descent
|
|
4760
|
+
// candidates pay a call.
|
|
4761
|
+
for (const key in previous) {
|
|
4636
4762
|
if (nodes?.[key]) continue; // main loop already diffed this slot
|
|
4637
|
-
const previousValue = unwrap(
|
|
4638
|
-
override ? getOverrideValue(previous, override, key, optOverride) : previous[key]
|
|
4639
|
-
);
|
|
4763
|
+
const previousValue = unwrap(previous[key]);
|
|
4640
4764
|
if (!isWrappable(previousValue)) continue;
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
(
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4765
|
+
descendInto(previousValue, next[key], lookup, keyFn);
|
|
4766
|
+
}
|
|
4767
|
+
const syms = Object.getOwnPropertySymbols(previous);
|
|
4768
|
+
for (let i = 0, len = syms.length; i < len; i++) {
|
|
4769
|
+
if (Object.prototype.propertyIsEnumerable.call(previous, syms[i])) {
|
|
4770
|
+
if (nodes?.[syms[i]]) continue;
|
|
4771
|
+
const previousValue = unwrap(previous[syms[i]]);
|
|
4772
|
+
if (!isWrappable(previousValue)) continue;
|
|
4773
|
+
descendInto(previousValue, next[syms[i]], lookup, keyFn);
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
function descendInto(previousValue, rawNext, lookup, keyFn) {
|
|
4778
|
+
const childTarget = lookupTarget(previousValue, lookup);
|
|
4779
|
+
if (!childTarget?.[STORE_DESC]) return;
|
|
4780
|
+
const nextValue = unwrap(rawNext);
|
|
4781
|
+
if (
|
|
4782
|
+
previousValue === nextValue ||
|
|
4783
|
+
!isWrappable(nextValue) ||
|
|
4784
|
+
Array.isArray(previousValue) !== Array.isArray(nextValue) ||
|
|
4785
|
+
(keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
|
|
4786
|
+
)
|
|
4787
|
+
return;
|
|
4788
|
+
if (childTarget[STORE_SHALLOW]) {
|
|
4789
|
+
applyStateShallow(nextValue, childTarget);
|
|
4790
|
+
} else if (childTarget[STORE_OVERRIDE] || childTarget[STORE_OPTIMISTIC_OVERRIDE]) {
|
|
4791
|
+
applyStateSlow(nextValue, childTarget, keyFn);
|
|
4792
|
+
} else {
|
|
4793
|
+
applyStateFast(nextValue, childTarget, keyFn);
|
|
4652
4794
|
}
|
|
4653
4795
|
}
|
|
4654
4796
|
// Dispatcher: every applyState call (including recursion) checks for the
|
|
@@ -4662,18 +4804,101 @@ function applyState(next, state, keyFn) {
|
|
|
4662
4804
|
next = unwrap(next);
|
|
4663
4805
|
const target = state?.[$TARGET];
|
|
4664
4806
|
if (!target) return;
|
|
4665
|
-
if (target[
|
|
4807
|
+
if (target[STORE_SHALLOW]) {
|
|
4808
|
+
applyStateShallow(next, target);
|
|
4809
|
+
} else if (target[STORE_OVERRIDE] || target[STORE_OPTIMISTIC_OVERRIDE]) {
|
|
4666
4810
|
applyStateSlow(next, target, keyFn);
|
|
4667
4811
|
} else {
|
|
4668
4812
|
applyStateFast(next, target, keyFn);
|
|
4669
4813
|
}
|
|
4670
4814
|
}
|
|
4815
|
+
// Shallow boundary diff: the target's own keys are reactive, its values are
|
|
4816
|
+
// raw records replaced by reference — no recursion, no wrapping. Arrays merge
|
|
4817
|
+
// positionally (below a shallow boundary the VALUE is the identity; keyed
|
|
4818
|
+
// row identity belongs to the consumer, e.g. <For keyed>). Incoming
|
|
4819
|
+
// wrappables are sticky-marked raw so they present raw through every store.
|
|
4820
|
+
// One pass over a shallow target's node record: replace changed slots with
|
|
4821
|
+
// raw next values, null out slots absent from next. Returns whether anything
|
|
4822
|
+
// differed.
|
|
4823
|
+
function shallowDiffNodes(nodes, next, prevAt, skipLength) {
|
|
4824
|
+
let changed = false;
|
|
4825
|
+
for (const key in nodes) {
|
|
4826
|
+
if (skipLength && key === "length") continue;
|
|
4827
|
+
if (key in next) {
|
|
4828
|
+
const v = next[key];
|
|
4829
|
+
if (v !== prevAt(key)) {
|
|
4830
|
+
changed = true;
|
|
4831
|
+
setSignal(nodes[key], v);
|
|
4832
|
+
}
|
|
4833
|
+
} else {
|
|
4834
|
+
changed = true;
|
|
4835
|
+
setSignal(nodes[key], undefined);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
return changed;
|
|
4839
|
+
}
|
|
4840
|
+
function applyStateShallow(next, target, keyFn) {
|
|
4841
|
+
const previous = target[STORE_VALUE];
|
|
4842
|
+
const override = target[STORE_OVERRIDE];
|
|
4843
|
+
const optOverride = target[STORE_OPTIMISTIC_OVERRIDE];
|
|
4844
|
+
if (next === previous && !override && !optOverride) return;
|
|
4845
|
+
// Setter-staged writes fold into the diff: previous values resolve through
|
|
4846
|
+
// the override layers (so a replaced slot compares against what readers
|
|
4847
|
+
// saw), and the regular override clears with the swap — reconcile makes
|
|
4848
|
+
// `next` the authoritative base, same as the deep slow path.
|
|
4849
|
+
const prevAt = key => {
|
|
4850
|
+
const v = getOverrideValue(previous, override, key, optOverride);
|
|
4851
|
+
return v === $DELETED ? undefined : v;
|
|
4852
|
+
};
|
|
4853
|
+
target[STORE_OVERRIDE] = undefined;
|
|
4854
|
+
const fam = target[STORE_LOOKUP];
|
|
4855
|
+
fam !== undefined ? fam.set(next, target[$PROXY]) : storeLookup.set(next, target);
|
|
4856
|
+
target[STORE_VALUE] = next;
|
|
4857
|
+
markRawIngest(next);
|
|
4858
|
+
const nodes = target[STORE_NODE];
|
|
4859
|
+
const tracked = nodes && nodes[$TRACK];
|
|
4860
|
+
let changed = false;
|
|
4861
|
+
if (Array.isArray(previous)) {
|
|
4862
|
+
const prevLength = override?.length ?? optOverride?.length ?? previous.length;
|
|
4863
|
+
if (nodes) {
|
|
4864
|
+
changed = shallowDiffNodes(nodes, next, prevAt, true);
|
|
4865
|
+
if (nodes.length && prevLength !== next.length) setSignal(nodes.length, next.length);
|
|
4866
|
+
}
|
|
4867
|
+
if (!changed && (tracked || target[STORE_HAS])) {
|
|
4868
|
+
// Slots without nodes still feed $TRACK enumerators / `in` probes.
|
|
4869
|
+
if (prevLength !== next.length) changed = true;
|
|
4870
|
+
else {
|
|
4871
|
+
for (let i = 0, len = next.length; i < len; i++) {
|
|
4872
|
+
if (prevAt(i) !== next[i]) {
|
|
4873
|
+
changed = true;
|
|
4874
|
+
break;
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
} else {
|
|
4880
|
+
if (nodes) {
|
|
4881
|
+
changed = shallowDiffNodes(nodes, next, prevAt, false);
|
|
4882
|
+
}
|
|
4883
|
+
if (!changed && (tracked || target[STORE_HAS])) changed = true;
|
|
4884
|
+
}
|
|
4885
|
+
let has = target[STORE_HAS];
|
|
4886
|
+
if (has) {
|
|
4887
|
+
for (const key in has) {
|
|
4888
|
+
setSignal(has[key], key in next);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
changed && notifySelf(target);
|
|
4892
|
+
}
|
|
4671
4893
|
function applyStateFast(next, target, keyFn) {
|
|
4672
4894
|
const previous = target[STORE_VALUE];
|
|
4673
4895
|
if (next === previous) return;
|
|
4674
4896
|
const arrayNodes = target[STORE_NODE];
|
|
4675
4897
|
// swap
|
|
4676
|
-
|
|
4898
|
+
{
|
|
4899
|
+
const fam = target[STORE_LOOKUP];
|
|
4900
|
+
fam !== undefined ? fam.set(next, target[$PROXY]) : storeLookup.set(next, target);
|
|
4901
|
+
}
|
|
4677
4902
|
target[STORE_VALUE] = next;
|
|
4678
4903
|
// merge
|
|
4679
4904
|
if (Array.isArray(previous)) {
|
|
@@ -4686,10 +4911,22 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4686
4911
|
start < end && keyedMatch((item = previous[start]), next[start], keyFn);
|
|
4687
4912
|
start++
|
|
4688
4913
|
) {
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4914
|
+
// keyedMatch established both sides wrappable unless they're the
|
|
4915
|
+
// SAME reference — and an identical slot is a guaranteed no-op
|
|
4916
|
+
// (the child's STORE_VALUE tracks the previous graph), so skip
|
|
4917
|
+
// the recursion dispatch entirely. Raw-marked values are leaves:
|
|
4918
|
+
// replace the slot node instead of recursing.
|
|
4919
|
+
if (item !== next[start]) {
|
|
4920
|
+
if (rawValuesUsed && (isRawValue(item) || isRawValue(next[start]))) {
|
|
4921
|
+
arrayNodes?.[start] && setSignal(arrayNodes[start], wrapValue(next[start], target));
|
|
4922
|
+
} else applyStateChild(next[start], item, target, keyFn);
|
|
4923
|
+
}
|
|
4692
4924
|
}
|
|
4925
|
+
// Every position key-matched at equal length (the steady-state shape
|
|
4926
|
+
// of a polling tick): membership, length, and order are unchanged —
|
|
4927
|
+
// nothing below could do observable work, so skip the staging
|
|
4928
|
+
// allocations and membership sync outright.
|
|
4929
|
+
if (start === next.length && start === prevLength) return;
|
|
4693
4930
|
const temp = new Array(next.length),
|
|
4694
4931
|
newIndices = new Map();
|
|
4695
4932
|
for (
|
|
@@ -4742,9 +4979,13 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4742
4979
|
} else if (next.length) {
|
|
4743
4980
|
for (let i = 0, len = next.length; i < len; i++) {
|
|
4744
4981
|
const item = previous[i];
|
|
4745
|
-
if (
|
|
4746
|
-
|
|
4747
|
-
|
|
4982
|
+
if (
|
|
4983
|
+
isWrappable(item) &&
|
|
4984
|
+
isWrappable(next[i]) &&
|
|
4985
|
+
!(rawValuesUsed && (isRawValue(item) || isRawValue(next[i])))
|
|
4986
|
+
) {
|
|
4987
|
+
if (item !== next[i]) applyStateChild(next[i], item, target, keyFn);
|
|
4988
|
+
} else {
|
|
4748
4989
|
if (item !== next[i]) changed = true;
|
|
4749
4990
|
arrayNodes?.[i] && setSignal(arrayNodes[i], wrapValue(next[i], target));
|
|
4750
4991
|
}
|
|
@@ -4763,23 +5004,55 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4763
5004
|
let tracked;
|
|
4764
5005
|
if (nodes) {
|
|
4765
5006
|
tracked = nodes[$TRACK];
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
5007
|
+
// The per-key body is duplicated across both loops on purpose: this is
|
|
5008
|
+
// the hottest object-diff site and a shared helper costs a per-key call
|
|
5009
|
+
// in instruction counts (CodSpeed regressed ~7% on deep-tree reconciles
|
|
5010
|
+
// with the extracted form).
|
|
5011
|
+
if (tracked || symbolKeyedRecords.has(nodes)) {
|
|
5012
|
+
const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
|
|
5013
|
+
for (let i = 0, len = keys.length; i < len; i++) {
|
|
5014
|
+
const key = keys[i];
|
|
5015
|
+
const node = nodes[key];
|
|
5016
|
+
const previousValue = unwrap(previous[key]);
|
|
5017
|
+
const nextValue = unwrap(next[key]);
|
|
5018
|
+
if (previousValue === nextValue) continue;
|
|
5019
|
+
if (
|
|
5020
|
+
!previousValue ||
|
|
5021
|
+
!isWrappable(previousValue) ||
|
|
5022
|
+
!isWrappable(nextValue) ||
|
|
5023
|
+
// Raw-marked values are leaves replaced by reference — a "wrappable
|
|
5024
|
+
// pair" is only recursable when both sides are actual store children.
|
|
5025
|
+
(rawValuesUsed && (isRawValue(previousValue) || isRawValue(nextValue))) ||
|
|
5026
|
+
Array.isArray(previousValue) !== Array.isArray(nextValue) ||
|
|
5027
|
+
(keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
|
|
5028
|
+
) {
|
|
5029
|
+
tracked && setSignal(tracked, void 0);
|
|
5030
|
+
node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
|
|
5031
|
+
} else applyStateChild(nextValue, previousValue, target, keyFn);
|
|
5032
|
+
}
|
|
5033
|
+
} else {
|
|
5034
|
+
// Untracked, string-only node records (the overwhelmingly common case)
|
|
5035
|
+
// iterate in place — nodeKeys() allocated a fresh key array per object
|
|
5036
|
+
// per pass, which dominates allocation on large-graph reconciles.
|
|
5037
|
+
for (const key in nodes) {
|
|
5038
|
+
const node = nodes[key];
|
|
5039
|
+
const previousValue = unwrap(previous[key]);
|
|
5040
|
+
const nextValue = unwrap(next[key]);
|
|
5041
|
+
if (previousValue === nextValue) continue;
|
|
5042
|
+
if (
|
|
5043
|
+
!previousValue ||
|
|
5044
|
+
!isWrappable(previousValue) ||
|
|
5045
|
+
!isWrappable(nextValue) ||
|
|
5046
|
+
// Raw-marked values are leaves replaced by reference — a "wrappable
|
|
5047
|
+
// pair" is only recursable when both sides are actual store children.
|
|
5048
|
+
(rawValuesUsed && (isRawValue(previousValue) || isRawValue(nextValue))) ||
|
|
5049
|
+
Array.isArray(previousValue) !== Array.isArray(nextValue) ||
|
|
5050
|
+
(keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
|
|
5051
|
+
) {
|
|
5052
|
+
tracked && setSignal(tracked, void 0);
|
|
5053
|
+
node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
|
|
5054
|
+
} else applyStateChild(nextValue, previousValue, target, keyFn);
|
|
5055
|
+
}
|
|
4783
5056
|
}
|
|
4784
5057
|
}
|
|
4785
5058
|
if (!tracked && target[STORE_DESC]) applyDescendants(previous, next, target, nodes, keyFn);
|
|
@@ -4798,7 +5071,10 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4798
5071
|
const optOverride = target[STORE_OPTIMISTIC_OVERRIDE];
|
|
4799
5072
|
let nodes = target[STORE_NODE];
|
|
4800
5073
|
// swap
|
|
4801
|
-
|
|
5074
|
+
{
|
|
5075
|
+
const fam = target[STORE_LOOKUP];
|
|
5076
|
+
fam !== undefined ? fam.set(next, target[$PROXY]) : storeLookup.set(next, target);
|
|
5077
|
+
}
|
|
4802
5078
|
target[STORE_VALUE] = next;
|
|
4803
5079
|
target[STORE_OVERRIDE] = undefined;
|
|
4804
5080
|
// merge
|
|
@@ -4817,9 +5093,11 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4817
5093
|
);
|
|
4818
5094
|
start++
|
|
4819
5095
|
) {
|
|
4820
|
-
isWrappable(item) &&
|
|
4821
|
-
|
|
4822
|
-
|
|
5096
|
+
if (isWrappable(item) && isWrappable(next[start]) && item !== next[start]) {
|
|
5097
|
+
if (rawValuesUsed && (isRawValue(item) || isRawValue(next[start]))) {
|
|
5098
|
+
nodes?.[start] && setSignal(nodes[start], wrapValue(next[start], target));
|
|
5099
|
+
} else applyState(next[start], wrap(item, target), keyFn);
|
|
5100
|
+
}
|
|
4823
5101
|
}
|
|
4824
5102
|
const temp = new Array(next.length),
|
|
4825
5103
|
newIndices = new Map();
|
|
@@ -4878,9 +5156,13 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4878
5156
|
} else if (next.length) {
|
|
4879
5157
|
for (let i = 0, len = next.length; i < len; i++) {
|
|
4880
5158
|
const item = getOverrideValue(previous, override, i, optOverride);
|
|
4881
|
-
if (
|
|
4882
|
-
|
|
4883
|
-
|
|
5159
|
+
if (
|
|
5160
|
+
isWrappable(item) &&
|
|
5161
|
+
isWrappable(next[i]) &&
|
|
5162
|
+
!(rawValuesUsed && (isRawValue(item) || isRawValue(next[i])))
|
|
5163
|
+
) {
|
|
5164
|
+
if (item !== next[i]) applyState(next[i], wrap(item, target), keyFn);
|
|
5165
|
+
} else {
|
|
4884
5166
|
if (item !== next[i]) changed = true;
|
|
4885
5167
|
nodes?.[i] && setSignal(nodes[i], wrapValue(next[i], target));
|
|
4886
5168
|
}
|
|
@@ -4910,6 +5192,7 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4910
5192
|
!previousValue ||
|
|
4911
5193
|
!isWrappable(previousValue) ||
|
|
4912
5194
|
!isWrappable(nextValue) ||
|
|
5195
|
+
(rawValuesUsed && (isRawValue(previousValue) || isRawValue(nextValue))) ||
|
|
4913
5196
|
Array.isArray(previousValue) !== Array.isArray(nextValue) ||
|
|
4914
5197
|
(keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
|
|
4915
5198
|
) {
|
|
@@ -4929,17 +5212,26 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4929
5212
|
}
|
|
4930
5213
|
}
|
|
4931
5214
|
}
|
|
5215
|
+
// No-key reconcile: every item reports "no key", which routes array diffs to
|
|
5216
|
+
// the positional branch and object descent to plain per-property merging.
|
|
5217
|
+
const NOKEY = () => null;
|
|
4932
5218
|
/**
|
|
4933
5219
|
* Returns a draft-mutating function that smart-merges `value` into a store,
|
|
4934
|
-
* preserving
|
|
4935
|
-
* new states. Useful when applying server payloads or full-replacement data
|
|
4936
|
-
* onto an existing store without losing fine-grained reactivity.
|
|
5220
|
+
* preserving fine-grained reactivity: only changed leaves trigger updates.
|
|
4937
5221
|
*
|
|
4938
|
-
*
|
|
4939
|
-
*
|
|
5222
|
+
* With a `key` (default `"id"`), array items whose key matches between old
|
|
5223
|
+
* and new states keep their identity (updated in place, moves and removals
|
|
5224
|
+
* update the corresponding signals) — the shape for keyed server payloads.
|
|
5225
|
+
* Items without the key field fall back to positional matching.
|
|
5226
|
+
*
|
|
5227
|
+
* With `key: null`, matching is purely positional: index N of the new array
|
|
5228
|
+
* merges into index N of the old, and object properties merge recursively —
|
|
5229
|
+
* the classic pattern for fixed-shape data that churns in place (dashboards,
|
|
5230
|
+
* monitors), where no keyed diff pass is needed or wanted.
|
|
4940
5231
|
*
|
|
4941
5232
|
* @param value the next state to merge in
|
|
4942
|
-
* @param key property name (string) or extractor function for stable
|
|
5233
|
+
* @param key property name (string) or extractor function for stable
|
|
5234
|
+
* identity (default `"id"`); pass `null` for positional merging
|
|
4943
5235
|
*
|
|
4944
5236
|
* @example
|
|
4945
5237
|
* ```ts
|
|
@@ -4947,13 +5239,20 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4947
5239
|
*
|
|
4948
5240
|
* async function refresh() {
|
|
4949
5241
|
* const fresh = await api.getTodos();
|
|
4950
|
-
* setTodos(reconcile(fresh
|
|
5242
|
+
* setTodos(reconcile(fresh)); // diff-merge by `id`
|
|
4951
5243
|
* }
|
|
5244
|
+
*
|
|
5245
|
+
* // fixed-shape polling data — positional merge
|
|
5246
|
+
* setStats(reconcile(nextStats, null));
|
|
4952
5247
|
* ```
|
|
4953
5248
|
*/
|
|
4954
|
-
function reconcile(value, key) {
|
|
5249
|
+
function reconcile(value, key = "id") {
|
|
4955
5250
|
return state => {
|
|
4956
5251
|
if (state == null) throw new Error("Cannot reconcile null or undefined state");
|
|
5252
|
+
if (key === null) {
|
|
5253
|
+
applyState(value, state, NOKEY);
|
|
5254
|
+
return;
|
|
5255
|
+
}
|
|
4957
5256
|
const keyFn = typeof key === "string" ? item => item[key] : key;
|
|
4958
5257
|
const eq = keyFn(state);
|
|
4959
5258
|
if (eq !== undefined && keyFn(value) !== eq)
|
|
@@ -4965,9 +5264,16 @@ function reconcile(value, key) {
|
|
|
4965
5264
|
function createProjectionInternal(fn, seed, options) {
|
|
4966
5265
|
let node;
|
|
4967
5266
|
const wrappedMap = new WeakMap();
|
|
5267
|
+
// A shallow projection's children are raw and never wrapped, so the
|
|
5268
|
+
// wrapper only ever runs for the root — flagging unconditionally is safe.
|
|
5269
|
+
const shallow = !!options?.shallow;
|
|
4968
5270
|
const wrapper = s => {
|
|
4969
5271
|
s[STORE_WRAP] = wrapProjection;
|
|
4970
5272
|
s[STORE_LOOKUP] = wrappedMap;
|
|
5273
|
+
if (shallow) {
|
|
5274
|
+
s[STORE_SHALLOW] = true;
|
|
5275
|
+
markRawIngest(s[STORE_VALUE]);
|
|
5276
|
+
}
|
|
4971
5277
|
Object.defineProperty(s, STORE_FIREWALL, {
|
|
4972
5278
|
get() {
|
|
4973
5279
|
return node;
|
|
@@ -5154,15 +5460,41 @@ const STORE_VALUE = "v",
|
|
|
5154
5460
|
STORE_OPTIMISTIC = "p",
|
|
5155
5461
|
STORE_OPTIMISTIC_OWNERS = "t",
|
|
5156
5462
|
STORE_PARENT = "u",
|
|
5157
|
-
STORE_DESC = "d"
|
|
5463
|
+
STORE_DESC = "d",
|
|
5464
|
+
STORE_SHALLOW = "s";
|
|
5158
5465
|
const STORE_SELF_PENDING = Symbol("STORE_SELF_PENDING");
|
|
5466
|
+
// Every StoreNode field is initialized up front, in one fixed order, so all
|
|
5467
|
+
// targets share a single hidden class. The traps and reconcile read these
|
|
5468
|
+
// fields on their hottest paths; fields added lazily in varying orders made
|
|
5469
|
+
// those loads megamorphic across a large store graph (dictionary-mode probes
|
|
5470
|
+
// on every trap hit). Assignments elsewhere must never `delete` a field —
|
|
5471
|
+
// write `undefined` instead, or the shape degrades again.
|
|
5472
|
+
function initStoreFields(newTarget) {
|
|
5473
|
+
newTarget[STORE_OVERRIDE] = undefined;
|
|
5474
|
+
newTarget[STORE_OPTIMISTIC_OVERRIDE] = undefined;
|
|
5475
|
+
newTarget[STORE_OPTIMISTIC_OWNERS] = undefined;
|
|
5476
|
+
newTarget[STORE_NODE] = undefined;
|
|
5477
|
+
newTarget[STORE_HAS] = undefined;
|
|
5478
|
+
newTarget[STORE_CUSTOM_PROTO] = undefined;
|
|
5479
|
+
newTarget[STORE_WRAP] = undefined;
|
|
5480
|
+
newTarget[STORE_LOOKUP] = undefined;
|
|
5481
|
+
newTarget[STORE_FIREWALL] = undefined;
|
|
5482
|
+
newTarget[STORE_OPTIMISTIC] = undefined;
|
|
5483
|
+
newTarget[STORE_SNAPSHOT_PROPS] = undefined;
|
|
5484
|
+
newTarget[STORE_PARENT] = undefined;
|
|
5485
|
+
newTarget[STORE_DESC] = undefined;
|
|
5486
|
+
newTarget[STORE_SHALLOW] = undefined;
|
|
5487
|
+
newTarget[$PROXY] = null;
|
|
5488
|
+
}
|
|
5159
5489
|
function createStoreProxy(value, traps = storeTraps, extend) {
|
|
5160
5490
|
let newTarget;
|
|
5161
5491
|
if (Array.isArray(value)) {
|
|
5162
5492
|
newTarget = [];
|
|
5163
|
-
newTarget
|
|
5493
|
+
newTarget[STORE_VALUE] = value;
|
|
5494
|
+
initStoreFields(newTarget);
|
|
5164
5495
|
} else {
|
|
5165
|
-
newTarget = {
|
|
5496
|
+
newTarget = { [STORE_VALUE]: value };
|
|
5497
|
+
initStoreFields(newTarget);
|
|
5166
5498
|
const unwrapped = value?.[$TARGET]?.[STORE_VALUE] ?? value;
|
|
5167
5499
|
const proto = Object.getPrototypeOf(unwrapped);
|
|
5168
5500
|
if (proto !== null && proto !== Object.prototype) {
|
|
@@ -5172,24 +5504,98 @@ function createStoreProxy(value, traps = storeTraps, extend) {
|
|
|
5172
5504
|
extend && extend(newTarget);
|
|
5173
5505
|
return (newTarget[$PROXY] = new Proxy(newTarget, traps));
|
|
5174
5506
|
}
|
|
5507
|
+
// The global lookup maps raw value -> StoreNode TARGET (not proxy): reconcile
|
|
5508
|
+
// and the unwrap/snapshot walks resolve targets through it constantly, and a
|
|
5509
|
+
// target hit is a plain field read away from its proxy while a proxy hit
|
|
5510
|
+
// costs a trap to get back to the target. Per-family STORE_LOOKUP maps
|
|
5511
|
+
// (projections/optimistic) still map raw -> proxy — their wrap functions own
|
|
5512
|
+
// that contract — so mixed-lookup consumers resolve through lookupTarget().
|
|
5175
5513
|
const storeLookup = new WeakMap();
|
|
5176
5514
|
// Node records that hold at least one user (non-`$TRACK`) symbol-keyed node.
|
|
5177
5515
|
// Lets reconcile enumerate symbols only for records that need it (#2851).
|
|
5178
5516
|
const symbolKeyedRecords = new WeakSet();
|
|
5517
|
+
function lookupTarget(value, lookup) {
|
|
5518
|
+
if (lookup !== undefined && lookup !== storeLookup) {
|
|
5519
|
+
const p = lookup.get(value);
|
|
5520
|
+
if (p !== undefined) return p[$TARGET];
|
|
5521
|
+
}
|
|
5522
|
+
return storeLookup.get(value);
|
|
5523
|
+
}
|
|
5524
|
+
// Values marked raw never acquire a proxy identity: wrap() serves them as-is
|
|
5525
|
+
// everywhere — deep stores hold them as leaf values replaced by reference.
|
|
5526
|
+
// Once raw, always raw (identity stays single, just unwrapped). Consulted
|
|
5527
|
+
// only on wrap-creation and ingest paths; reads never touch it.
|
|
5528
|
+
const rawValues = new WeakSet();
|
|
5529
|
+
/**
|
|
5530
|
+
* Marks a value as raw: no store will ever wrap it — every store presents it
|
|
5531
|
+
* as-is, tracked by reference at whatever slot holds it and updated by
|
|
5532
|
+
* replacement. Useful for class instances and external objects (editors,
|
|
5533
|
+
* scene graphs, Maps) and for record-shaped data updated wholesale. Sticky
|
|
5534
|
+
* for the value's lifetime.
|
|
5535
|
+
*/
|
|
5536
|
+
// Flipped on the first mark and exported as a LIVE binding: reconcile
|
|
5537
|
+
// consults it on every recursable pair, and importing the boolean directly
|
|
5538
|
+
// lets those sites skip even the function call when no shallow store or raw
|
|
5539
|
+
// mark exists anywhere in the app.
|
|
5540
|
+
let rawValuesUsed = false;
|
|
5541
|
+
function isRawValue(value) {
|
|
5542
|
+
return rawValuesUsed && rawValues.has(value);
|
|
5543
|
+
}
|
|
5544
|
+
function markRawOne(v) {
|
|
5545
|
+
if (isWrappable(v)) {
|
|
5546
|
+
if (storeLookup.has(v))
|
|
5547
|
+
throw new Error(
|
|
5548
|
+
"shallow store: an ingested record is already tracked as a deep store — one value cannot present both wrapped and raw"
|
|
5549
|
+
);
|
|
5550
|
+
rawValuesUsed = true;
|
|
5551
|
+
rawValues.add(v);
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
function markRawIngest(container) {
|
|
5555
|
+
if (Array.isArray(container)) {
|
|
5556
|
+
for (let i = 0, len = container.length; i < len; i++) markRawOne(container[i]);
|
|
5557
|
+
} else {
|
|
5558
|
+
for (const k in container) markRawOne(container[k]);
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5179
5561
|
function wrap(value, target) {
|
|
5562
|
+
// Raw is raw in every family: the mark must preempt family wrapping too,
|
|
5563
|
+
// or a shallow projection/optimistic store would proxy its raw records.
|
|
5564
|
+
if (rawValuesUsed && rawValues.has(value)) return value;
|
|
5180
5565
|
if (target?.[STORE_WRAP]) {
|
|
5181
5566
|
const p = target[STORE_WRAP](value, target);
|
|
5182
5567
|
const t = p[$TARGET];
|
|
5183
5568
|
if (t && !t[STORE_PARENT] && t !== target) t[STORE_PARENT] = target;
|
|
5184
5569
|
return p;
|
|
5185
5570
|
}
|
|
5186
|
-
|
|
5571
|
+
const t = storeLookup.get(value);
|
|
5572
|
+
if (t !== undefined) return t[$PROXY];
|
|
5573
|
+
let p = value[$PROXY];
|
|
5187
5574
|
if (!p) {
|
|
5188
|
-
|
|
5189
|
-
|
|
5575
|
+
p = createStoreProxy(value);
|
|
5576
|
+
const newTarget = p[$TARGET];
|
|
5577
|
+
storeLookup.set(value, newTarget);
|
|
5578
|
+
if (target) newTarget[STORE_PARENT] = target;
|
|
5190
5579
|
}
|
|
5191
5580
|
return p;
|
|
5192
5581
|
}
|
|
5582
|
+
// Shallow store root: the target itself is fully reactive (per-key nodes,
|
|
5583
|
+
// membership, $TRACK); its values are served raw. Seed values are marked at
|
|
5584
|
+
// creation; reconcile and the set trap mark on ingest.
|
|
5585
|
+
function wrapShallow(value) {
|
|
5586
|
+
const existing = storeLookup.get(value);
|
|
5587
|
+
if (existing !== undefined) {
|
|
5588
|
+
if (existing[STORE_SHALLOW]) return existing[$PROXY];
|
|
5589
|
+
throw new Error("createStore({ shallow }): value is already tracked as a deep store");
|
|
5590
|
+
}
|
|
5591
|
+
if (value[$TARGET]) throw new Error("createStore({ shallow }): value is already a store proxy");
|
|
5592
|
+
const p = createStoreProxy(value);
|
|
5593
|
+
const newTarget = p[$TARGET];
|
|
5594
|
+
newTarget[STORE_SHALLOW] = true;
|
|
5595
|
+
storeLookup.set(value, newTarget);
|
|
5596
|
+
markRawIngest(value);
|
|
5597
|
+
return p;
|
|
5598
|
+
}
|
|
5193
5599
|
function isWrappable(obj) {
|
|
5194
5600
|
if (obj == null || typeof obj !== "object" || Object.isFrozen(obj)) return false;
|
|
5195
5601
|
// Dynamic Node check (kept dynamic so test/SSR overrides of `globalThis.Node`
|
|
@@ -5204,7 +5610,7 @@ function writeOnly(proxy) {
|
|
|
5204
5610
|
return writeOverride || !!Writing?.has(proxy);
|
|
5205
5611
|
}
|
|
5206
5612
|
function unwrapStoreValue(value, map, lookup) {
|
|
5207
|
-
const target = value?.[$TARGET] ||
|
|
5613
|
+
const target = value?.[$TARGET] || lookupTarget(value, lookup);
|
|
5208
5614
|
if (!target) return value;
|
|
5209
5615
|
const override = target[STORE_OVERRIDE];
|
|
5210
5616
|
if (!override) return target[STORE_VALUE];
|
|
@@ -5391,7 +5797,7 @@ function walkAffectsScope(
|
|
|
5391
5797
|
visited
|
|
5392
5798
|
) {
|
|
5393
5799
|
if (!isWrappable(value)) return;
|
|
5394
|
-
const target = value[$TARGET] || (lookup
|
|
5800
|
+
const target = value[$TARGET] || lookupTarget(value, lookup);
|
|
5395
5801
|
const raw = target ? target[STORE_VALUE] : value;
|
|
5396
5802
|
if (visited.has(raw)) return;
|
|
5397
5803
|
visited.add(raw);
|
|
@@ -5741,6 +6147,45 @@ const storeTraps = {
|
|
|
5741
6147
|
trackSelf(target);
|
|
5742
6148
|
return receiver;
|
|
5743
6149
|
}
|
|
6150
|
+
// Hot path: an existing node on a plain target — no firewall (so neither
|
|
6151
|
+
// selfRead nor the uninitialized guard can apply), no override layers,
|
|
6152
|
+
// no write scope, and a raw (non-proxy) source. This is the shape of
|
|
6153
|
+
// every effect re-read of a settled store; it pays one node read and the
|
|
6154
|
+
// wrap check, nothing else. Any exotic bit falls through to the full
|
|
6155
|
+
// resolution below, and dev strictRead keeps its warning path.
|
|
6156
|
+
if (
|
|
6157
|
+
!strictRead &&
|
|
6158
|
+
target[STORE_FIREWALL] === undefined &&
|
|
6159
|
+
target[STORE_OVERRIDE] === undefined &&
|
|
6160
|
+
target[STORE_OPTIMISTIC_OVERRIDE] === undefined &&
|
|
6161
|
+
!writeOverride &&
|
|
6162
|
+
(Writing === null || !Writing.has(receiver))
|
|
6163
|
+
) {
|
|
6164
|
+
const nodes = target[STORE_NODE];
|
|
6165
|
+
const node = nodes && nodes[property];
|
|
6166
|
+
if (node !== undefined && target[STORE_VALUE][$TARGET] === undefined) {
|
|
6167
|
+
// readNodeFast is read()'s plain-signal fast path hoisted over the
|
|
6168
|
+
// call; READ_SLOW means a global read window (latest/pending-check/
|
|
6169
|
+
// transition/lane/snapshot capture) or a node layer is active, and
|
|
6170
|
+
// only then does the full read() resolution have anything to do.
|
|
6171
|
+
let value = readNodeFast(node);
|
|
6172
|
+
if (value === READ_SLOW) value = read(node);
|
|
6173
|
+
if (value === $DELETED) value = undefined;
|
|
6174
|
+
// Every node-writing site wraps wrappables before setSignal (see the
|
|
6175
|
+
// dev assertion below), so re-wrapping on read is redundant — except
|
|
6176
|
+
// during snapshot capture, where read() can surface a raw captured
|
|
6177
|
+
// value seeded from snapshot props.
|
|
6178
|
+
if (!snapshotCaptureActive) {
|
|
6179
|
+
if (isWrappable(value) && wrap(value, target) !== value) {
|
|
6180
|
+
throw new Error(
|
|
6181
|
+
"store node invariant violated: node held an unwrapped wrappable value"
|
|
6182
|
+
);
|
|
6183
|
+
}
|
|
6184
|
+
return value;
|
|
6185
|
+
}
|
|
6186
|
+
return isWrappable(value) ? wrap(value, target) : value;
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
5744
6189
|
const selfRead = getObserver() === target[STORE_FIREWALL];
|
|
5745
6190
|
const nodes = getNodes(target, STORE_NODE);
|
|
5746
6191
|
const tracked = selfRead ? undefined : nodes[property];
|
|
@@ -5781,6 +6226,11 @@ const storeTraps = {
|
|
|
5781
6226
|
tracked && (overridden || !proxySource) ? visibleNodeValue(tracked) : storeValue[property];
|
|
5782
6227
|
value === $DELETED && (value = undefined);
|
|
5783
6228
|
if (!isWrappable(value)) return value;
|
|
6229
|
+
// Shallow boundary: records are replaced, never edited in place. Reads
|
|
6230
|
+
// inside a setter serve the raw so read-then-replace, filter/pop and
|
|
6231
|
+
// projection derives all work; in-place mutation of a raw is inert by
|
|
6232
|
+
// construction — the same contract as a markRaw child in a deep store.
|
|
6233
|
+
if (target[STORE_SHALLOW]) return value;
|
|
5784
6234
|
const wrapped = wrap(value, target);
|
|
5785
6235
|
Writing?.add(wrapped);
|
|
5786
6236
|
return wrapped;
|
|
@@ -5866,6 +6316,7 @@ const storeTraps = {
|
|
|
5866
6316
|
? prevLayer[property] !== $DELETED
|
|
5867
6317
|
: property in target[STORE_VALUE];
|
|
5868
6318
|
const value = unwrapStoreValue(rawValue);
|
|
6319
|
+
if (target[STORE_SHALLOW] && isWrappable(value)) rawValues.add(value);
|
|
5869
6320
|
// Symbol-keyed writes on arrays are metadata, not index writes — never run
|
|
5870
6321
|
// them through the numeric index/length machinery (`parseInt` on a symbol
|
|
5871
6322
|
// throws). #2769
|
|
@@ -6082,7 +6533,11 @@ function storeSetter(store, fn) {
|
|
|
6082
6533
|
}
|
|
6083
6534
|
function createStore(first, second, options) {
|
|
6084
6535
|
const derived = typeof first === "function",
|
|
6085
|
-
wrappedStore = derived
|
|
6536
|
+
wrappedStore = derived
|
|
6537
|
+
? createProjectionInternal(first, second, options).store
|
|
6538
|
+
: second?.shallow
|
|
6539
|
+
? wrapShallow(first)
|
|
6540
|
+
: wrap(first);
|
|
6086
6541
|
registerGraph(wrappedStore, getOwner());
|
|
6087
6542
|
return [
|
|
6088
6543
|
wrappedStore,
|
|
@@ -6246,6 +6701,8 @@ function createOptimisticStore(first, second, options) {
|
|
|
6246
6701
|
installOptimisticEngine();
|
|
6247
6702
|
GlobalQueue._clearOptimisticStores ||= clearOptimisticStores;
|
|
6248
6703
|
const derived = typeof first === "function";
|
|
6704
|
+
// Plain form: the second slot carries options.
|
|
6705
|
+
if (!derived && options === undefined) options = second;
|
|
6249
6706
|
const initialValue = derived ? second : first;
|
|
6250
6707
|
const fn = derived ? first : undefined;
|
|
6251
6708
|
// Create optimistic projection store
|
|
@@ -6330,8 +6787,10 @@ function clearOptimisticOverride(target, completing) {
|
|
|
6330
6787
|
}
|
|
6331
6788
|
}
|
|
6332
6789
|
if (!remaining) {
|
|
6333
|
-
delete
|
|
6334
|
-
delete
|
|
6790
|
+
// Assignment, not delete: StoreNode targets share one pre-initialized
|
|
6791
|
+
// hidden class (see createStoreProxy) and a delete would demote it.
|
|
6792
|
+
target[STORE_OPTIMISTIC_OVERRIDE] = undefined;
|
|
6793
|
+
target[STORE_OPTIMISTIC_OWNERS] = undefined;
|
|
6335
6794
|
}
|
|
6336
6795
|
// Notify $TRACK
|
|
6337
6796
|
if (cleared && nodes?.[$TRACK]) {
|
|
@@ -6345,9 +6804,14 @@ function clearOptimisticOverride(target, completing) {
|
|
|
6345
6804
|
function createOptimisticProjectionInternal(fn, initialValue, options) {
|
|
6346
6805
|
let node;
|
|
6347
6806
|
const wrappedMap = new WeakMap();
|
|
6807
|
+
const shallow = !!options?.shallow;
|
|
6348
6808
|
const wrapper = s => {
|
|
6349
6809
|
s[STORE_WRAP] = wrapProjection;
|
|
6350
6810
|
s[STORE_LOOKUP] = wrappedMap;
|
|
6811
|
+
if (shallow) {
|
|
6812
|
+
s[STORE_SHALLOW] = true;
|
|
6813
|
+
markRawIngest(s[STORE_VALUE]);
|
|
6814
|
+
}
|
|
6351
6815
|
s[STORE_OPTIMISTIC] = true; // Mark as optimistic store
|
|
6352
6816
|
Object.defineProperty(s, STORE_FIREWALL, {
|
|
6353
6817
|
get() {
|
|
@@ -6516,7 +6980,7 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6516
6980
|
if (!isWrappable(item)) return item;
|
|
6517
6981
|
if (map && map.has(item)) return map.get(item);
|
|
6518
6982
|
if (!map) map = new Map();
|
|
6519
|
-
if ((target = item[$TARGET] ||
|
|
6983
|
+
if ((target = item[$TARGET] || lookupTarget(item, lookup))) {
|
|
6520
6984
|
if (track) {
|
|
6521
6985
|
trackSelf(target, $TRACK);
|
|
6522
6986
|
// A tracked walk reads THROUGH the record without touching the proxy
|
|
@@ -6524,6 +6988,18 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6524
6988
|
if (pendingCheckActive) witnessAffectsMark(target);
|
|
6525
6989
|
}
|
|
6526
6990
|
override = mergedOverlay(target);
|
|
6991
|
+
// A derived store's STORE_VALUE is the inner store's live proxy
|
|
6992
|
+
// (store-in-store: createOptimisticStore/createProjection over a store).
|
|
6993
|
+
// Without an overlay of its own, the fast path below would map to — and
|
|
6994
|
+
// could return — that proxy verbatim. Recurse instead so the
|
|
6995
|
+
// inner store's own target branch unwraps it (chains of any depth).
|
|
6996
|
+
// With an overlay, a fresh `result` is always built, so the walk over the
|
|
6997
|
+
// inner proxy already copies plain values.
|
|
6998
|
+
if (!override && target[STORE_VALUE][$TARGET]) {
|
|
6999
|
+
unwrapped = snapshotImpl(target[STORE_VALUE], track, map, lookup);
|
|
7000
|
+
map.set(item, unwrapped);
|
|
7001
|
+
return unwrapped;
|
|
7002
|
+
}
|
|
6527
7003
|
isArray = Array.isArray(target[STORE_VALUE]);
|
|
6528
7004
|
map.set(
|
|
6529
7005
|
item,
|
|
@@ -6542,7 +7018,7 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6542
7018
|
for (let i = 0; i < len; i++) {
|
|
6543
7019
|
v = override && i in override ? override[i] : item[i];
|
|
6544
7020
|
if (v === $DELETED) continue;
|
|
6545
|
-
if (track && isWrappable(v)) wrap(v, target);
|
|
7021
|
+
if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
|
|
6546
7022
|
if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== v || result) {
|
|
6547
7023
|
if (!result) map.set(item, (result = [...item]));
|
|
6548
7024
|
result[i] = unwrapped;
|
|
@@ -6556,7 +7032,7 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6556
7032
|
const desc = getPropertyDescriptor(item, override, prop);
|
|
6557
7033
|
if (!desc || desc.get) continue;
|
|
6558
7034
|
v = override && prop in override ? override[prop] : item[prop];
|
|
6559
|
-
if (track && isWrappable(v)) wrap(v, target);
|
|
7035
|
+
if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
|
|
6560
7036
|
unwrapped = snapshotImpl(v, track, map, lookup);
|
|
6561
7037
|
if (unwrapped !== v || result) {
|
|
6562
7038
|
if (!result) map.set(item, (result = Object.assign([...item], item)));
|
|
@@ -6578,7 +7054,7 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6578
7054
|
const desc = Object.getOwnPropertyDescriptor(item, prop);
|
|
6579
7055
|
if (desc.get) continue;
|
|
6580
7056
|
v = desc.value;
|
|
6581
|
-
if (track && isWrappable(v)) wrap(v, target);
|
|
7057
|
+
if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
|
|
6582
7058
|
if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== v || result) {
|
|
6583
7059
|
if (!result) {
|
|
6584
7060
|
result = Object.create(Object.getPrototypeOf(item));
|
|
@@ -6596,7 +7072,7 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6596
7072
|
const desc = getPropertyDescriptor(item, override, prop);
|
|
6597
7073
|
if (desc.get) continue;
|
|
6598
7074
|
v = prop in override ? override[prop] : item[prop];
|
|
6599
|
-
if (track && isWrappable(v)) wrap(v, target);
|
|
7075
|
+
if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
|
|
6600
7076
|
if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== item[prop] || result) {
|
|
6601
7077
|
if (!result) {
|
|
6602
7078
|
result = Object.create(Object.getPrototypeOf(item));
|
|
@@ -6937,19 +7413,7 @@ function updateKeyedMap() {
|
|
|
6937
7413
|
this._items = newItems.slice(0);
|
|
6938
7414
|
this._len = newLen;
|
|
6939
7415
|
} else {
|
|
6940
|
-
let start,
|
|
6941
|
-
end,
|
|
6942
|
-
newEnd,
|
|
6943
|
-
item,
|
|
6944
|
-
key,
|
|
6945
|
-
newIndices,
|
|
6946
|
-
newIndicesNext,
|
|
6947
|
-
removed,
|
|
6948
|
-
created,
|
|
6949
|
-
temp = new Array(newLen),
|
|
6950
|
-
tempNodes = new Array(newLen);
|
|
6951
|
-
rows = this._rows ? new Array(newLen) : undefined;
|
|
6952
|
-
indexes = this._indexes ? new Array(newLen) : undefined;
|
|
7416
|
+
let start, end, newEnd, item, key, newIndices, newIndicesNext, removed, created;
|
|
6953
7417
|
// skip common prefix
|
|
6954
7418
|
for (
|
|
6955
7419
|
start = 0, end = Math.min(this._len, newLen);
|
|
@@ -6960,7 +7424,8 @@ function updateKeyedMap() {
|
|
|
6960
7424
|
) {
|
|
6961
7425
|
if (this._rows) setSignal(this._rows[start], newItems[start]);
|
|
6962
7426
|
}
|
|
6963
|
-
// common suffix
|
|
7427
|
+
// skip common suffix — counted only; retained entries land in one pass
|
|
7428
|
+
// at commit instead of being staged and copied twice
|
|
6964
7429
|
for (
|
|
6965
7430
|
end = this._len - 1, newEnd = newLen - 1;
|
|
6966
7431
|
end >= start &&
|
|
@@ -6968,13 +7433,21 @@ function updateKeyedMap() {
|
|
|
6968
7433
|
(this._items[end] === newItems[newEnd] ||
|
|
6969
7434
|
(this._rows && compare(this._key, this._items[end], newItems[newEnd])));
|
|
6970
7435
|
end--, newEnd--
|
|
6971
|
-
)
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
7436
|
+
);
|
|
7437
|
+
// no structural change (every position matched in place at equal
|
|
7438
|
+
// length — the common post-reconcile shape): keep the same mapped
|
|
7439
|
+
// array identity so downstream consumers don't re-run at all
|
|
7440
|
+
if (start === newLen && this._len === newLen) {
|
|
7441
|
+
this._items = newItems.slice(0);
|
|
7442
|
+
return;
|
|
6976
7443
|
}
|
|
6977
|
-
|
|
7444
|
+
const dif = newLen - this._len;
|
|
7445
|
+
const temp = new Array(newLen);
|
|
7446
|
+
const tempNodes = new Array(newLen);
|
|
7447
|
+
rows = this._rows ? new Array(newLen) : undefined;
|
|
7448
|
+
indexes = this._indexes ? new Array(newLen) : undefined;
|
|
7449
|
+
// 0) prepare a map of all indices in the changed window of newItems,
|
|
7450
|
+
// scanning backwards so we encounter them in natural order
|
|
6978
7451
|
newIndices = new Map();
|
|
6979
7452
|
newIndicesNext = new Array(newEnd + 1);
|
|
6980
7453
|
for (j = newEnd; j >= start; j--) {
|
|
@@ -6984,7 +7457,9 @@ function updateKeyedMap() {
|
|
|
6984
7457
|
newIndicesNext[j] = i === undefined ? -1 : i;
|
|
6985
7458
|
newIndices.set(key, j);
|
|
6986
7459
|
}
|
|
6987
|
-
// 1) step through
|
|
7460
|
+
// 1) step through the old changed window and see if items can be found
|
|
7461
|
+
// in the new set; if so, stage them at their new positions; if not,
|
|
7462
|
+
// queue them for disposal at commit
|
|
6988
7463
|
for (i = start; i <= end; i++) {
|
|
6989
7464
|
item = this._items[i];
|
|
6990
7465
|
key = this._key ? this._key(item) : item;
|
|
@@ -7000,8 +7475,8 @@ function updateKeyedMap() {
|
|
|
7000
7475
|
}
|
|
7001
7476
|
// 2) create new rows into the temp arrays; an abort disposes only these
|
|
7002
7477
|
try {
|
|
7003
|
-
for (j = start; j
|
|
7004
|
-
if (j
|
|
7478
|
+
for (j = start; j <= newEnd; j++) {
|
|
7479
|
+
if (tempNodes[j] !== undefined) continue;
|
|
7005
7480
|
(created ??= []).push((tempNodes[j] = createOwner()));
|
|
7006
7481
|
temp[j] = runWithOwner(tempNodes[j], mapper);
|
|
7007
7482
|
}
|
|
@@ -7009,24 +7484,39 @@ function updateKeyedMap() {
|
|
|
7009
7484
|
if (created) for (i = 0; i < created.length; i++) created[i].dispose();
|
|
7010
7485
|
throw err;
|
|
7011
7486
|
}
|
|
7012
|
-
// 3) commit: land
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7487
|
+
// 3) commit: land the retained prefix and suffix plus the staged window
|
|
7488
|
+
// into the fresh arrays, swap them in (new identity for downstream
|
|
7489
|
+
// change propagation), then dispose exited rows
|
|
7490
|
+
for (i = 0; i < start; i++) {
|
|
7491
|
+
temp[i] = this._mappings[i];
|
|
7492
|
+
tempNodes[i] = this._nodes[i];
|
|
7493
|
+
rows && (rows[i] = this._rows[i]);
|
|
7494
|
+
indexes && (indexes[i] = this._indexes[i]);
|
|
7495
|
+
}
|
|
7496
|
+
for (j = start; j <= newEnd; j++) {
|
|
7497
|
+
if (rows) setSignal(rows[j], newItems[j]);
|
|
7498
|
+
if (indexes) setSignal(indexes[j], j);
|
|
7499
|
+
}
|
|
7500
|
+
for (j = newEnd + 1; j < newLen; j++) {
|
|
7501
|
+
temp[j] = this._mappings[j - dif];
|
|
7502
|
+
tempNodes[j] = this._nodes[j - dif];
|
|
7016
7503
|
if (rows) {
|
|
7017
|
-
|
|
7018
|
-
setSignal(
|
|
7504
|
+
rows[j] = this._rows[j - dif];
|
|
7505
|
+
setSignal(rows[j], newItems[j]);
|
|
7019
7506
|
}
|
|
7020
7507
|
if (indexes) {
|
|
7021
|
-
|
|
7022
|
-
setSignal(
|
|
7508
|
+
indexes[j] = this._indexes[j - dif];
|
|
7509
|
+
if (dif !== 0) setSignal(indexes[j], j);
|
|
7023
7510
|
}
|
|
7024
7511
|
}
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7512
|
+
this._mappings = temp;
|
|
7513
|
+
this._nodes = tempNodes;
|
|
7514
|
+
rows && (this._rows = rows);
|
|
7515
|
+
indexes && (this._indexes = indexes);
|
|
7516
|
+
this._len = newLen;
|
|
7517
|
+
// save a copy of the mapped items for the next update
|
|
7029
7518
|
this._items = newItems.slice(0);
|
|
7519
|
+
if (removed) for (i = 0; i < removed.length; i++) removed[i].dispose();
|
|
7030
7520
|
}
|
|
7031
7521
|
});
|
|
7032
7522
|
return this._mappings;
|