@solidjs/signals 2.0.0-beta.24 → 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
@@ -2230,7 +2230,21 @@ function recompute(el, create = false) {
2230
2230
  value = inFlightChanged || !isAsyncResult ? fnResult : handleAsync(el, fnResult);
2231
2231
  if (!inFlightChanged && !isAsyncResult) el._inFlight = null;
2232
2232
  }
2233
- 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);
2234
2248
  // _optimisticLane is only ever assigned by engine paths.
2235
2249
  if (el._optimisticLane) GlobalQueue._laneAsyncSettled(el);
2236
2250
  } catch (e) {
@@ -2291,7 +2305,15 @@ function recompute(el, create = false) {
2291
2305
  if (el._error);
2292
2306
  else if (valueChanged) {
2293
2307
  const prevVisible = hasOverride ? el._overrideValue : undefined;
2294
- 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
+ ) {
2295
2317
  el._value = value;
2296
2318
  // Lane-propagated correction: upstream data is fresh, correct the
2297
2319
  // override unconditionally. The direct _value commit is the lane's
@@ -2311,7 +2333,12 @@ function recompute(el, create = false) {
2311
2333
  if ((activeTransition || el._transition) && GlobalQueue._syncCompanions !== null)
2312
2334
  GlobalQueue._syncCompanions(el, value);
2313
2335
  }
2314
- 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
+ )
2315
2342
  insertSubs(el, isOptimisticDirty || hasOverride);
2316
2343
  } else if (hasOverride) {
2317
2344
  // Unchanged value (equals the override) recomputed while the override
@@ -2610,6 +2637,39 @@ function prepareComputed(comp, refresh) {
2610
2637
  updateIfNecessary(comp);
2611
2638
  }
2612
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
+ }
2613
2673
  function read(el) {
2614
2674
  // Handle latest() mode: read from _latestValueComputed
2615
2675
  // Checked before isPending so that isPending(() => latest(x)) checks
@@ -2805,7 +2865,12 @@ function setSignal(el, v) {
2805
2865
  if (!valueChanged) return v;
2806
2866
  if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
2807
2867
  el._pendingValue = v;
2808
- 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);
2809
2874
  el._time = clock;
2810
2875
  insertSubs(el);
2811
2876
  schedule();
@@ -3147,7 +3212,11 @@ function optimisticWrite(el, v) {
3147
3212
  // brand, and erasing it makes the write invisible and routes follow-up
3148
3213
  // writes off the optimistic path into permanent commits (#2898).
3149
3214
  el._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
3150
- 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);
3151
3220
  el._time = clock;
3152
3221
  insertSubs(el, true);
3153
3222
  schedule();
@@ -4591,27 +4660,70 @@ function keyedMatch(a, b, keyFn) {
4591
4660
  // and `in` dependencies should follow the new value's membership. Use
4592
4661
  // membership rather than length arithmetic so sparse arrays and named array
4593
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.
4594
4667
  function syncArrayNodeMembership(target, next) {
4595
4668
  let nodes = target[STORE_NODE];
4596
4669
  if (nodes) {
4597
- const keys = nodeKeys(nodes);
4598
- for (let i = 0, len = keys.length; i < len; i++) {
4599
- const key = keys[i];
4600
- 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
+ }
4601
4679
  }
4602
4680
  }
4603
4681
  if ((nodes = target[STORE_HAS])) {
4604
- const keys = nodeKeys(nodes);
4605
- for (let i = 0, len = keys.length; i < len; i++) {
4606
- const key = keys[i];
4607
- 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
+ }
4608
4691
  }
4609
4692
  }
4610
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
+ }
4611
4719
  // Reconcile a single array slot: recurse into a wrappable pair, otherwise replace
4612
4720
  // the node's value outright (covers object→primitive and primitive→object).
4613
4721
  function applyArrayItem(next, previous, target, node, keyFn) {
4614
- if (isWrappable(next) && isWrappable(previous)) {
4722
+ if (
4723
+ isWrappable(next) &&
4724
+ isWrappable(previous) &&
4725
+ !(rawValuesUsed && (isRawValue(previous) || isRawValue(next)))
4726
+ ) {
4615
4727
  const wrapped = wrap(previous, target);
4616
4728
  node && setSignal(node, wrapped);
4617
4729
  applyState(next, wrapped, keyFn);
@@ -4631,27 +4743,54 @@ function applyArrayItem(next, previous, target, node, keyFn) {
4631
4743
  */
4632
4744
  function applyDescendants(previous, next, target, nodes, keyFn, override, optOverride) {
4633
4745
  const lookup = target[STORE_LOOKUP] || storeLookup;
4634
- const keys = (override ? getKeys(previous, override) : Object.keys(previous)).concat(
4635
- getStoreSymbols(previous, override)
4636
- );
4637
- for (let i = 0, len = keys.length; i < len; i++) {
4638
- 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) {
4639
4762
  if (nodes?.[key]) continue; // main loop already diffed this slot
4640
- const previousValue = unwrap(
4641
- override ? getOverrideValue(previous, override, key, optOverride) : previous[key]
4642
- );
4763
+ const previousValue = unwrap(previous[key]);
4643
4764
  if (!isWrappable(previousValue)) continue;
4644
- const childTarget = (lookup.get(previousValue) ?? storeLookup.get(previousValue))?.[$TARGET];
4645
- if (!childTarget?.[STORE_DESC]) continue;
4646
- const nextValue = unwrap(next[key]);
4647
- if (
4648
- previousValue === nextValue ||
4649
- !isWrappable(nextValue) ||
4650
- Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4651
- (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4652
- )
4653
- continue;
4654
- 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);
4655
4794
  }
4656
4795
  }
4657
4796
  // Dispatcher: every applyState call (including recursion) checks for the
@@ -4665,18 +4804,101 @@ function applyState(next, state, keyFn) {
4665
4804
  next = unwrap(next);
4666
4805
  const target = state?.[$TARGET];
4667
4806
  if (!target) return;
4668
- 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]) {
4669
4810
  applyStateSlow(next, target, keyFn);
4670
4811
  } else {
4671
4812
  applyStateFast(next, target, keyFn);
4672
4813
  }
4673
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
+ }
4674
4893
  function applyStateFast(next, target, keyFn) {
4675
4894
  const previous = target[STORE_VALUE];
4676
4895
  if (next === previous) return;
4677
4896
  const arrayNodes = target[STORE_NODE];
4678
4897
  // swap
4679
- (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
+ }
4680
4902
  target[STORE_VALUE] = next;
4681
4903
  // merge
4682
4904
  if (Array.isArray(previous)) {
@@ -4689,10 +4911,22 @@ function applyStateFast(next, target, keyFn) {
4689
4911
  start < end && keyedMatch((item = previous[start]), next[start], keyFn);
4690
4912
  start++
4691
4913
  ) {
4692
- isWrappable(item) &&
4693
- isWrappable(next[start]) &&
4694
- 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
+ }
4695
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;
4696
4930
  const temp = new Array(next.length),
4697
4931
  newIndices = new Map();
4698
4932
  for (
@@ -4745,9 +4979,13 @@ function applyStateFast(next, target, keyFn) {
4745
4979
  } else if (next.length) {
4746
4980
  for (let i = 0, len = next.length; i < len; i++) {
4747
4981
  const item = previous[i];
4748
- if (isWrappable(item) && isWrappable(next[i]))
4749
- applyState(next[i], wrap(item, target), keyFn);
4750
- 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 {
4751
4989
  if (item !== next[i]) changed = true;
4752
4990
  arrayNodes?.[i] && setSignal(arrayNodes[i], wrapValue(next[i], target));
4753
4991
  }
@@ -4766,17 +5004,54 @@ function applyStateFast(next, target, keyFn) {
4766
5004
  let tracked;
4767
5005
  if (nodes) {
4768
5006
  tracked = nodes[$TRACK];
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).
4769
5011
  if (tracked || symbolKeyedRecords.has(nodes)) {
4770
5012
  const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
4771
5013
  for (let i = 0, len = keys.length; i < len; i++) {
4772
- diffNodeKey(keys[i], nodes, previous, next, target, tracked, keyFn);
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);
4773
5032
  }
4774
5033
  } else {
4775
5034
  // Untracked, string-only node records (the overwhelmingly common case)
4776
5035
  // iterate in place — nodeKeys() allocated a fresh key array per object
4777
5036
  // per pass, which dominates allocation on large-graph reconciles.
4778
5037
  for (const key in nodes) {
4779
- diffNodeKey(key, nodes, previous, next, target, tracked, keyFn);
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);
4780
5055
  }
4781
5056
  }
4782
5057
  }
@@ -4790,31 +5065,16 @@ function applyStateFast(next, target, keyFn) {
4790
5065
  }
4791
5066
  }
4792
5067
  }
4793
- // One node-key step of the fast object diff — shared by the array-iterating
4794
- // (tracked / symbol-keyed) and for-in (plain) loops in applyStateFast.
4795
- function diffNodeKey(key, nodes, previous, next, target, tracked, keyFn) {
4796
- const node = nodes[key];
4797
- const previousValue = unwrap(previous[key]);
4798
- let nextValue = unwrap(next[key]);
4799
- if (previousValue === nextValue) return;
4800
- if (
4801
- !previousValue ||
4802
- !isWrappable(previousValue) ||
4803
- !isWrappable(nextValue) ||
4804
- Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4805
- (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4806
- ) {
4807
- tracked && setSignal(tracked, void 0);
4808
- node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
4809
- } else applyState(nextValue, wrap(previousValue, target), keyFn);
4810
- }
4811
5068
  function applyStateSlow(next, target, keyFn) {
4812
5069
  const previous = target[STORE_VALUE];
4813
5070
  const override = target[STORE_OVERRIDE];
4814
5071
  const optOverride = target[STORE_OPTIMISTIC_OVERRIDE];
4815
5072
  let nodes = target[STORE_NODE];
4816
5073
  // swap
4817
- (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
+ }
4818
5078
  target[STORE_VALUE] = next;
4819
5079
  target[STORE_OVERRIDE] = undefined;
4820
5080
  // merge
@@ -4833,9 +5093,11 @@ function applyStateSlow(next, target, keyFn) {
4833
5093
  );
4834
5094
  start++
4835
5095
  ) {
4836
- isWrappable(item) &&
4837
- isWrappable(next[start]) &&
4838
- 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
+ }
4839
5101
  }
4840
5102
  const temp = new Array(next.length),
4841
5103
  newIndices = new Map();
@@ -4894,9 +5156,13 @@ function applyStateSlow(next, target, keyFn) {
4894
5156
  } else if (next.length) {
4895
5157
  for (let i = 0, len = next.length; i < len; i++) {
4896
5158
  const item = getOverrideValue(previous, override, i, optOverride);
4897
- if (isWrappable(item) && isWrappable(next[i]))
4898
- applyState(next[i], wrap(item, target), keyFn);
4899
- 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 {
4900
5166
  if (item !== next[i]) changed = true;
4901
5167
  nodes?.[i] && setSignal(nodes[i], wrapValue(next[i], target));
4902
5168
  }
@@ -4926,6 +5192,7 @@ function applyStateSlow(next, target, keyFn) {
4926
5192
  !previousValue ||
4927
5193
  !isWrappable(previousValue) ||
4928
5194
  !isWrappable(nextValue) ||
5195
+ (rawValuesUsed && (isRawValue(previousValue) || isRawValue(nextValue))) ||
4929
5196
  Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4930
5197
  (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4931
5198
  ) {
@@ -4997,9 +5264,16 @@ function reconcile(value, key = "id") {
4997
5264
  function createProjectionInternal(fn, seed, options) {
4998
5265
  let node;
4999
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;
5000
5270
  const wrapper = s => {
5001
5271
  s[STORE_WRAP] = wrapProjection;
5002
5272
  s[STORE_LOOKUP] = wrappedMap;
5273
+ if (shallow) {
5274
+ s[STORE_SHALLOW] = true;
5275
+ markRawIngest(s[STORE_VALUE]);
5276
+ }
5003
5277
  Object.defineProperty(s, STORE_FIREWALL, {
5004
5278
  get() {
5005
5279
  return node;
@@ -5186,7 +5460,8 @@ const STORE_VALUE = "v",
5186
5460
  STORE_OPTIMISTIC = "p",
5187
5461
  STORE_OPTIMISTIC_OWNERS = "t",
5188
5462
  STORE_PARENT = "u",
5189
- STORE_DESC = "d";
5463
+ STORE_DESC = "d",
5464
+ STORE_SHALLOW = "s";
5190
5465
  const STORE_SELF_PENDING = Symbol("STORE_SELF_PENDING");
5191
5466
  // Every StoreNode field is initialized up front, in one fixed order, so all
5192
5467
  // targets share a single hidden class. The traps and reconcile read these
@@ -5208,6 +5483,7 @@ function initStoreFields(newTarget) {
5208
5483
  newTarget[STORE_SNAPSHOT_PROPS] = undefined;
5209
5484
  newTarget[STORE_PARENT] = undefined;
5210
5485
  newTarget[STORE_DESC] = undefined;
5486
+ newTarget[STORE_SHALLOW] = undefined;
5211
5487
  newTarget[$PROXY] = null;
5212
5488
  }
5213
5489
  function createStoreProxy(value, traps = storeTraps, extend) {
@@ -5228,24 +5504,98 @@ function createStoreProxy(value, traps = storeTraps, extend) {
5228
5504
  extend && extend(newTarget);
5229
5505
  return (newTarget[$PROXY] = new Proxy(newTarget, traps));
5230
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().
5231
5513
  const storeLookup = new WeakMap();
5232
5514
  // Node records that hold at least one user (non-`$TRACK`) symbol-keyed node.
5233
5515
  // Lets reconcile enumerate symbols only for records that need it (#2851).
5234
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
+ }
5235
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;
5236
5565
  if (target?.[STORE_WRAP]) {
5237
5566
  const p = target[STORE_WRAP](value, target);
5238
5567
  const t = p[$TARGET];
5239
5568
  if (t && !t[STORE_PARENT] && t !== target) t[STORE_PARENT] = target;
5240
5569
  return p;
5241
5570
  }
5242
- 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];
5243
5574
  if (!p) {
5244
- storeLookup.set(value, (p = createStoreProxy(value)));
5245
- 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;
5246
5579
  }
5247
5580
  return p;
5248
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
+ }
5249
5599
  function isWrappable(obj) {
5250
5600
  if (obj == null || typeof obj !== "object" || Object.isFrozen(obj)) return false;
5251
5601
  // Dynamic Node check (kept dynamic so test/SSR overrides of `globalThis.Node`
@@ -5260,7 +5610,7 @@ function writeOnly(proxy) {
5260
5610
  return writeOverride || !!Writing?.has(proxy);
5261
5611
  }
5262
5612
  function unwrapStoreValue(value, map, lookup) {
5263
- const target = value?.[$TARGET] || lookup?.get(value)?.[$TARGET];
5613
+ const target = value?.[$TARGET] || lookupTarget(value, lookup);
5264
5614
  if (!target) return value;
5265
5615
  const override = target[STORE_OVERRIDE];
5266
5616
  if (!override) return target[STORE_VALUE];
@@ -5447,7 +5797,7 @@ function walkAffectsScope(
5447
5797
  visited
5448
5798
  ) {
5449
5799
  if (!isWrappable(value)) return;
5450
- const target = value[$TARGET] || (lookup ?? storeLookup).get(value)?.[$TARGET];
5800
+ const target = value[$TARGET] || lookupTarget(value, lookup);
5451
5801
  const raw = target ? target[STORE_VALUE] : value;
5452
5802
  if (visited.has(raw)) return;
5453
5803
  visited.add(raw);
@@ -5814,8 +6164,25 @@ const storeTraps = {
5814
6164
  const nodes = target[STORE_NODE];
5815
6165
  const node = nodes && nodes[property];
5816
6166
  if (node !== undefined && target[STORE_VALUE][$TARGET] === undefined) {
5817
- let value = read(node);
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);
5818
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
+ }
5819
6186
  return isWrappable(value) ? wrap(value, target) : value;
5820
6187
  }
5821
6188
  }
@@ -5859,6 +6226,11 @@ const storeTraps = {
5859
6226
  tracked && (overridden || !proxySource) ? visibleNodeValue(tracked) : storeValue[property];
5860
6227
  value === $DELETED && (value = undefined);
5861
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;
5862
6234
  const wrapped = wrap(value, target);
5863
6235
  Writing?.add(wrapped);
5864
6236
  return wrapped;
@@ -5944,6 +6316,7 @@ const storeTraps = {
5944
6316
  ? prevLayer[property] !== $DELETED
5945
6317
  : property in target[STORE_VALUE];
5946
6318
  const value = unwrapStoreValue(rawValue);
6319
+ if (target[STORE_SHALLOW] && isWrappable(value)) rawValues.add(value);
5947
6320
  // Symbol-keyed writes on arrays are metadata, not index writes — never run
5948
6321
  // them through the numeric index/length machinery (`parseInt` on a symbol
5949
6322
  // throws). #2769
@@ -6160,7 +6533,11 @@ function storeSetter(store, fn) {
6160
6533
  }
6161
6534
  function createStore(first, second, options) {
6162
6535
  const derived = typeof first === "function",
6163
- 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);
6164
6541
  registerGraph(wrappedStore, getOwner());
6165
6542
  return [
6166
6543
  wrappedStore,
@@ -6324,6 +6701,8 @@ function createOptimisticStore(first, second, options) {
6324
6701
  installOptimisticEngine();
6325
6702
  GlobalQueue._clearOptimisticStores ||= clearOptimisticStores;
6326
6703
  const derived = typeof first === "function";
6704
+ // Plain form: the second slot carries options.
6705
+ if (!derived && options === undefined) options = second;
6327
6706
  const initialValue = derived ? second : first;
6328
6707
  const fn = derived ? first : undefined;
6329
6708
  // Create optimistic projection store
@@ -6425,9 +6804,14 @@ function clearOptimisticOverride(target, completing) {
6425
6804
  function createOptimisticProjectionInternal(fn, initialValue, options) {
6426
6805
  let node;
6427
6806
  const wrappedMap = new WeakMap();
6807
+ const shallow = !!options?.shallow;
6428
6808
  const wrapper = s => {
6429
6809
  s[STORE_WRAP] = wrapProjection;
6430
6810
  s[STORE_LOOKUP] = wrappedMap;
6811
+ if (shallow) {
6812
+ s[STORE_SHALLOW] = true;
6813
+ markRawIngest(s[STORE_VALUE]);
6814
+ }
6431
6815
  s[STORE_OPTIMISTIC] = true; // Mark as optimistic store
6432
6816
  Object.defineProperty(s, STORE_FIREWALL, {
6433
6817
  get() {
@@ -6596,7 +6980,7 @@ function snapshotImpl(item, track, map, lookup) {
6596
6980
  if (!isWrappable(item)) return item;
6597
6981
  if (map && map.has(item)) return map.get(item);
6598
6982
  if (!map) map = new Map();
6599
- if ((target = item[$TARGET] || lookup?.get(item)?.[$TARGET])) {
6983
+ if ((target = item[$TARGET] || lookupTarget(item, lookup))) {
6600
6984
  if (track) {
6601
6985
  trackSelf(target, $TRACK);
6602
6986
  // A tracked walk reads THROUGH the record without touching the proxy
@@ -6634,7 +7018,7 @@ function snapshotImpl(item, track, map, lookup) {
6634
7018
  for (let i = 0; i < len; i++) {
6635
7019
  v = override && i in override ? override[i] : item[i];
6636
7020
  if (v === $DELETED) continue;
6637
- if (track && isWrappable(v)) wrap(v, target);
7021
+ if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
6638
7022
  if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== v || result) {
6639
7023
  if (!result) map.set(item, (result = [...item]));
6640
7024
  result[i] = unwrapped;
@@ -6648,7 +7032,7 @@ function snapshotImpl(item, track, map, lookup) {
6648
7032
  const desc = getPropertyDescriptor(item, override, prop);
6649
7033
  if (!desc || desc.get) continue;
6650
7034
  v = override && prop in override ? override[prop] : item[prop];
6651
- if (track && isWrappable(v)) wrap(v, target);
7035
+ if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
6652
7036
  unwrapped = snapshotImpl(v, track, map, lookup);
6653
7037
  if (unwrapped !== v || result) {
6654
7038
  if (!result) map.set(item, (result = Object.assign([...item], item)));
@@ -6670,7 +7054,7 @@ function snapshotImpl(item, track, map, lookup) {
6670
7054
  const desc = Object.getOwnPropertyDescriptor(item, prop);
6671
7055
  if (desc.get) continue;
6672
7056
  v = desc.value;
6673
- if (track && isWrappable(v)) wrap(v, target);
7057
+ if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
6674
7058
  if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== v || result) {
6675
7059
  if (!result) {
6676
7060
  result = Object.create(Object.getPrototypeOf(item));
@@ -6688,7 +7072,7 @@ function snapshotImpl(item, track, map, lookup) {
6688
7072
  const desc = getPropertyDescriptor(item, override, prop);
6689
7073
  if (desc.get) continue;
6690
7074
  v = prop in override ? override[prop] : item[prop];
6691
- if (track && isWrappable(v)) wrap(v, target);
7075
+ if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
6692
7076
  if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== item[prop] || result) {
6693
7077
  if (!result) {
6694
7078
  result = Object.create(Object.getPrototypeOf(item));