@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 CHANGED
@@ -2138,7 +2138,10 @@ function clearSnapshots() {
2138
2138
  if (snapshotSources) {
2139
2139
  for (const source of snapshotSources) {
2140
2140
  delete source._snapshotValue;
2141
- delete source[STORE_SNAPSHOT_PROPS];
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(el, create);
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 (create || (isEffect && activeTransition !== el._transition) || isOptimisticDirty) {
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
- if (!hasOverride || isOptimisticDirty || el._overrideValue !== prevVisible)
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
- GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, v);
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
- GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, v);
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
- const keys = nodeKeys(nodes);
4595
- for (let i = 0, len = keys.length; i < len; i++) {
4596
- const key = keys[i];
4597
- key in next || setSignal(nodes[key], undefined);
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
- const keys = nodeKeys(nodes);
4602
- for (let i = 0, len = keys.length; i < len; i++) {
4603
- const key = keys[i];
4604
- setSignal(nodes[key], key in next);
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 (isWrappable(next) && isWrappable(previous)) {
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
- const keys = (override ? getKeys(previous, override) : Object.keys(previous)).concat(
4632
- getStoreSymbols(previous, override)
4633
- );
4634
- for (let i = 0, len = keys.length; i < len; i++) {
4635
- const key = keys[i];
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
- const childTarget = (lookup.get(previousValue) ?? storeLookup.get(previousValue))?.[$TARGET];
4642
- if (!childTarget?.[STORE_DESC]) continue;
4643
- const nextValue = unwrap(next[key]);
4644
- if (
4645
- previousValue === nextValue ||
4646
- !isWrappable(nextValue) ||
4647
- Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4648
- (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4649
- )
4650
- continue;
4651
- applyState(nextValue, wrap(previousValue, target), keyFn);
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[STORE_OVERRIDE] || target[STORE_OPTIMISTIC_OVERRIDE]) {
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
- (target[STORE_LOOKUP] || storeLookup).set(next, target[$PROXY]);
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
- isWrappable(item) &&
4690
- isWrappable(next[start]) &&
4691
- applyState(next[start], wrap(item, target), keyFn);
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 (isWrappable(item) && isWrappable(next[i]))
4746
- applyState(next[i], wrap(item, target), keyFn);
4747
- else {
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
- const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
4767
- for (let i = 0, len = keys.length; i < len; i++) {
4768
- const key = keys[i];
4769
- const node = nodes[key];
4770
- const previousValue = unwrap(previous[key]);
4771
- let nextValue = unwrap(next[key]);
4772
- if (previousValue === nextValue) continue;
4773
- if (
4774
- !previousValue ||
4775
- !isWrappable(previousValue) ||
4776
- !isWrappable(nextValue) ||
4777
- Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4778
- (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4779
- ) {
4780
- tracked && setSignal(tracked, void 0);
4781
- node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
4782
- } else applyState(nextValue, wrap(previousValue, target), keyFn);
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
- (target[STORE_LOOKUP] || storeLookup).set(next, target[$PROXY]);
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
- isWrappable(next[start]) &&
4822
- applyState(next[start], wrap(item, target), keyFn);
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 (isWrappable(item) && isWrappable(next[i]))
4882
- applyState(next[i], wrap(item, target), keyFn);
4883
- else {
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 the identity of items whose `key` field matches between old and
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
- * Items with the same key are updated in place (only changed properties
4939
- * trigger updates). Items added or removed update the corresponding signals.
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 identity
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, "id")); // diff-merge by `id`
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.v = value;
5493
+ newTarget[STORE_VALUE] = value;
5494
+ initStoreFields(newTarget);
5164
5495
  } else {
5165
- newTarget = { v: value };
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
- let p = value[$PROXY] || storeLookup.get(value);
5571
+ const t = storeLookup.get(value);
5572
+ if (t !== undefined) return t[$PROXY];
5573
+ let p = value[$PROXY];
5187
5574
  if (!p) {
5188
- storeLookup.set(value, (p = createStoreProxy(value)));
5189
- if (target) p[$TARGET][STORE_PARENT] = target;
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] || lookup?.get(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 ?? storeLookup).get(value)?.[$TARGET];
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 ? createProjectionInternal(first, second, options).store : wrap(first);
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 target[STORE_OPTIMISTIC_OVERRIDE];
6334
- delete target[STORE_OPTIMISTIC_OWNERS];
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] || lookup?.get(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
- temp[newEnd] = this._mappings[end];
6973
- tempNodes[newEnd] = this._nodes[end];
6974
- rows && (rows[newEnd] = this._rows[end]);
6975
- indexes && (indexes[newEnd] = this._indexes[end]);
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
- // 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order
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 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
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 < newLen; j++) {
7004
- if (j in temp) continue;
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 positions, then dispose exited rows
7013
- for (j = start; j < newLen; j++) {
7014
- this._mappings[j] = temp[j];
7015
- this._nodes[j] = tempNodes[j];
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
- this._rows[j] = rows[j];
7018
- setSignal(this._rows[j], newItems[j]);
7504
+ rows[j] = this._rows[j - dif];
7505
+ setSignal(rows[j], newItems[j]);
7019
7506
  }
7020
7507
  if (indexes) {
7021
- this._indexes[j] = indexes[j];
7022
- setSignal(this._indexes[j], j);
7508
+ indexes[j] = this._indexes[j - dif];
7509
+ if (dif !== 0) setSignal(indexes[j], j);
7023
7510
  }
7024
7511
  }
7025
- if (removed) for (i = 0; i < removed.length; i++) removed[i].dispose();
7026
- // 4) in case the new set is shorter than the old, set the length of the mapped array
7027
- this._mappings = this._mappings.slice(0, (this._len = newLen));
7028
- // 5) save a copy of the mapped items for the next update
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;