@solidjs/signals 2.0.0-beta.24 → 2.0.0-beta.26

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
@@ -1707,8 +1707,40 @@ function forEachDependent(el, fn) {
1707
1707
  // Queue a node to re-run on the next flush (used both when a pending source
1708
1708
  // settles and when an `isPending` observer must re-evaluate after a real error):
1709
1709
  // shared scheduling helper in heap.ts (tracked effects bypass the heap).
1710
+ // Settle-time counterpart of unlinkSubs' last-one-out check. A lazy node that
1711
+ // loses its last subscriber while STATUS_PENDING is exempt from autodispose
1712
+ // (the in-flight work is an observer), so whatever CLEARS that pending state
1713
+ // must run the release — otherwise the node stays linked and recomputes
1714
+ // forever with zero subscribers (#2934). The node's own promise/iterator
1715
+ // callbacks handle their own release (settleAutodispose in handleAsync); this
1716
+ // covers derivatively-pending dependents, which have no callbacks of their own.
1717
+ function releaseIfSettledUnobserved(node) {
1718
+ node._fn &&
1719
+ node._config & CONFIG_AUTO_DISPOSE &&
1720
+ !node._subs &&
1721
+ !(node._flags & REACTIVE_ZOMBIE) &&
1722
+ !(node._statusFlags & STATUS_PENDING) &&
1723
+ unobserved(node);
1724
+ }
1725
+ // Error-path sweep: notifyStatus(STATUS_ERROR) clears dependents' pending
1726
+ // sources through its own recursion (no per-node settle callback), so after
1727
+ // the propagation completes, walk the same graph for stranded lazy nodes.
1728
+ // Collect-then-release so unobserved() never unlinks under the walk.
1729
+ function releaseSettledDependents(el) {
1730
+ let candidates;
1731
+ const visited = new Set();
1732
+ const visit = node => {
1733
+ if (visited.has(node)) return;
1734
+ visited.add(node);
1735
+ if (!node._subs && node._config & CONFIG_AUTO_DISPOSE) (candidates ??= []).push(node);
1736
+ forEachDependent(node, visit);
1737
+ };
1738
+ forEachDependent(el, visit);
1739
+ if (candidates) for (const node of candidates) releaseIfSettledUnobserved(node);
1740
+ }
1710
1741
  function settlePendingSource(el) {
1711
1742
  let scheduled = false;
1743
+ let released;
1712
1744
  const visited = new Set();
1713
1745
  // Companion updates no-op without the verdict layer (null hook).
1714
1746
  const updateCompanions = GlobalQueue._updatePendingSignal;
@@ -1729,10 +1761,17 @@ function settlePendingSource(el) {
1729
1761
  scheduled = true;
1730
1762
  }
1731
1763
  node._blocked = false;
1764
+ // Fully settled with nobody watching: release candidate (#2934). Checked
1765
+ // again at release time — deferred so unobserved() can't unlink subs
1766
+ // lists this walk is still iterating.
1767
+ if (!node._subs && node._config & CONFIG_AUTO_DISPOSE) (released ??= []).push(node);
1732
1768
  }
1733
1769
  forEachDependent(node, settle);
1734
1770
  };
1735
1771
  forEachDependent(el, settle);
1772
+ // Release before the flush schedule below: unobserved() pulls the node back
1773
+ // out of the heap, so the enqueueSub above never recomputes a released node.
1774
+ if (released) for (const node of released) releaseIfSettledUnobserved(node);
1736
1775
  if (scheduled) schedule();
1737
1776
  }
1738
1777
  // Object-thenable detection (Promises/A+ shape).
@@ -1776,12 +1815,41 @@ function handleAsync(el, result, setter) {
1776
1815
  }
1777
1816
  el._inFlight = result;
1778
1817
  let syncValue;
1818
+ // Settle-time transition re-entry. The loading rail is invisible to
1819
+ // transactions (#2933): a boundary-caught first load never registers as an
1820
+ // async reporter, so its settle — the boundary's fallback -> content
1821
+ // reveal — must flow ambiently. The node can still carry a `_transition`
1822
+ // stamp (pending-node bookkeeping rides through the stamping sites), and
1823
+ // blindly re-entering that stamped, still-incomplete transaction stashed
1824
+ // the reveal with it — a deadlock when the transaction's completion
1825
+ // depended on the reveal (#2937). An ESCAPED first load did register and
1826
+ // keeps transition scheduling; initialized (value-holding) pending settles
1827
+ // are the transaction's reveal machinery and always re-enter.
1828
+ const settleTransition = () => {
1829
+ const transition = resolveTransition(el);
1830
+ if (
1831
+ transition &&
1832
+ el._statusFlags & STATUS_UNINITIALIZED &&
1833
+ !currentTransition(transition)._asyncReporters.has(el)
1834
+ ) {
1835
+ // Drop the stale stamp too: the plain settle write (setSignal) and the
1836
+ // stash-path restamp both re-enter the transaction through it.
1837
+ el._transition = null;
1838
+ return;
1839
+ }
1840
+ globalQueue.initTransition(transition);
1841
+ };
1779
1842
  const handleError = error => {
1780
1843
  if (el._inFlight !== result) return;
1781
- globalQueue.initTransition(resolveTransition(el));
1844
+ settleTransition();
1782
1845
  // NotReadyError from rejected promises should be treated as pending, not error
1783
- notifyStatus(el, error instanceof NotReadyError ? STATUS_PENDING : STATUS_ERROR, error);
1846
+ const stillPending = error instanceof NotReadyError;
1847
+ notifyStatus(el, stillPending ? STATUS_PENDING : STATUS_ERROR, error);
1784
1848
  el._time = clock;
1849
+ // A real error settles derivatively-pending dependents (notifyStatus
1850
+ // cleared their pending sources), so stranded lazy ones release here —
1851
+ // the error twin of settlePendingSource's release (#2934).
1852
+ if (!stillPending) releaseSettledDependents(el);
1785
1853
  };
1786
1854
  const asyncWrite = (value, then) => {
1787
1855
  if (el._inFlight !== result) return;
@@ -1789,7 +1857,7 @@ function handleAsync(el, result, setter) {
1789
1857
  // skip this stale async result — the upcoming flush will recompute the node
1790
1858
  // with the new value, creating a fresh Promise that supersedes this one.
1791
1859
  if (el._flags & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return;
1792
- globalQueue.initTransition(resolveTransition(el));
1860
+ settleTransition();
1793
1861
  const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
1794
1862
  trimStaleDeps(el);
1795
1863
  clearStatus(el);
@@ -1861,10 +1929,14 @@ function handleAsync(el, result, setter) {
1861
1929
  // work (a lazy async memo would otherwise tear down and re-execute — one
1862
1930
  // fetch per suspended re-read). Settling is that observer's release, so
1863
1931
  // it runs the same last-one-out check the other release sites run.
1932
+ // Returns whether the node released, so the iterator branch can stop
1933
+ // pulling values instead of pumping an unobserved stream forever (#2935).
1864
1934
  const settleAutodispose = () => {
1865
1935
  if (el._config & CONFIG_AUTO_DISPOSE && !el._subs && !(el._statusFlags & STATUS_PENDING)) {
1866
1936
  unobserved(el);
1937
+ return true;
1867
1938
  }
1939
+ return false;
1868
1940
  };
1869
1941
  if (thenable) {
1870
1942
  let resolved = false,
@@ -1916,6 +1988,12 @@ function handleAsync(el, result, setter) {
1916
1988
  if (isThenable(returned)) returned.then(undefined, () => {});
1917
1989
  } catch {}
1918
1990
  });
1991
+ // Release check before each next pull: an unobserved lazy node must tear
1992
+ // down (its cleanup above closes the iterator) instead of pumping the
1993
+ // stream forever with zero subscribers (#2935).
1994
+ const iterateOrRelease = () => {
1995
+ if (!settleAutodispose()) iterate();
1996
+ };
1919
1997
  const iterate = () => {
1920
1998
  let syncResult,
1921
1999
  syncError,
@@ -1932,7 +2010,7 @@ function handleAsync(el, result, setter) {
1932
2010
  return;
1933
2011
  } else if (!r.done) {
1934
2012
  hadValue = true;
1935
- asyncWrite(r.value, iterate);
2013
+ asyncWrite(r.value, iterateOrRelease);
1936
2014
  } else {
1937
2015
  completed = true;
1938
2016
  if (hadValue) {
@@ -1942,6 +2020,7 @@ function handleAsync(el, result, setter) {
1942
2020
  // Empty completion settles like the immediately-done sync path.
1943
2021
  asyncWrite(undefined);
1944
2022
  }
2023
+ settleAutodispose();
1945
2024
  }
1946
2025
  },
1947
2026
  e => {
@@ -1951,6 +2030,7 @@ function handleAsync(el, result, setter) {
1951
2030
  } else if (el._inFlight === result) {
1952
2031
  completed = true;
1953
2032
  handleError(e);
2033
+ settleAutodispose();
1954
2034
  }
1955
2035
  }
1956
2036
  );
@@ -2230,7 +2310,21 @@ function recompute(el, create = false) {
2230
2310
  value = inFlightChanged || !isAsyncResult ? fnResult : handleAsync(el, fnResult);
2231
2311
  if (!inFlightChanged && !isAsyncResult) el._inFlight = null;
2232
2312
  }
2233
- clearStatus(el, create);
2313
+ // On a status-free node clearStatus is a guaranteed no-op: every branch
2314
+ // in its body is gated on one of these fields, and with _statusFlags === 0
2315
+ // the flags write (create or not) stores the 0 already there.
2316
+ if (
2317
+ el._statusFlags !== 0 ||
2318
+ el._notifyStatus !== undefined ||
2319
+ el._error ||
2320
+ el._reask ||
2321
+ el._blocked ||
2322
+ el._pendingSources !== undefined ||
2323
+ el._pendingSignal !== undefined ||
2324
+ el._latestValueComputed !== undefined ||
2325
+ el._child !== null
2326
+ )
2327
+ clearStatus(el, create);
2234
2328
  // _optimisticLane is only ever assigned by engine paths.
2235
2329
  if (el._optimisticLane) GlobalQueue._laneAsyncSettled(el);
2236
2330
  } catch (e) {
@@ -2291,7 +2385,15 @@ function recompute(el, create = false) {
2291
2385
  if (el._error);
2292
2386
  else if (valueChanged) {
2293
2387
  const prevVisible = hasOverride ? el._overrideValue : undefined;
2294
- if (create || (isEffect && activeTransition !== el._transition) || isOptimisticDirty) {
2388
+ if (
2389
+ create ||
2390
+ // Plain sync flush (no transition on either side) commits effect
2391
+ // values directly — the pending round-trip (queuePendingNode +
2392
+ // commitPendingNodes) exists to sequence transition reveals, and
2393
+ // paying it per effect on the plain path is pure overhead.
2394
+ (isEffect && (activeTransition !== el._transition || activeTransition === null)) ||
2395
+ isOptimisticDirty
2396
+ ) {
2295
2397
  el._value = value;
2296
2398
  // Lane-propagated correction: upstream data is fresh, correct the
2297
2399
  // override unconditionally. The direct _value commit is the lane's
@@ -2311,7 +2413,12 @@ function recompute(el, create = false) {
2311
2413
  if ((activeTransition || el._transition) && GlobalQueue._syncCompanions !== null)
2312
2414
  GlobalQueue._syncCompanions(el, value);
2313
2415
  }
2314
- if (!hasOverride || isOptimisticDirty || el._overrideValue !== prevVisible)
2416
+ // insertSubs only walks _subs (no scheduling of its own), so a
2417
+ // subscriber-less node has nothing to notify.
2418
+ if (
2419
+ el._subs !== null &&
2420
+ (!hasOverride || isOptimisticDirty || el._overrideValue !== prevVisible)
2421
+ )
2315
2422
  insertSubs(el, isOptimisticDirty || hasOverride);
2316
2423
  } else if (hasOverride) {
2317
2424
  // Unchanged value (equals the override) recomputed while the override
@@ -2610,6 +2717,39 @@ function prepareComputed(comp, refresh) {
2610
2717
  updateIfNecessary(comp);
2611
2718
  }
2612
2719
  }
2720
+ /**
2721
+ * Sentinel returned by readNodeFast when the plain-signal fast path does not
2722
+ * apply and the caller must fall back to the full read().
2723
+ */
2724
+ const READ_SLOW = Symbol("read-slow");
2725
+ /**
2726
+ * read()'s plain-signal fast path as a standalone entry for hot callers
2727
+ * (store traps). Safe to substitute for read() only because the bail
2728
+ * conditions mirror read()'s prelude and fast-path guard exactly: the
2729
+ * latestRead and pendingCheck windows run side-effectful hooks before the
2730
+ * fast path, `_fn` nodes need prepareComputed, and firewall / override /
2731
+ * snapshot / transition / lane / dev-strictRead state all take the full
2732
+ * resolution. Anything slow returns READ_SLOW; the caller then calls read().
2733
+ */
2734
+ function readNodeFast(el) {
2735
+ if (
2736
+ latestReadActive ||
2737
+ pendingCheckActive ||
2738
+ el._fn ||
2739
+ el._firewall ||
2740
+ el._overrideValue !== undefined ||
2741
+ el._snapshotValue !== undefined ||
2742
+ activeTransition !== null ||
2743
+ currentOptimisticLane !== null ||
2744
+ snapshotCaptureActive ||
2745
+ strictRead
2746
+ )
2747
+ return READ_SLOW;
2748
+ let c = context;
2749
+ if (c?._root) c = c._parentComputed;
2750
+ if (c && tracking) link(el, c);
2751
+ return !c || el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
2752
+ }
2613
2753
  function read(el) {
2614
2754
  // Handle latest() mode: read from _latestValueComputed
2615
2755
  // Checked before isPending so that isPending(() => latest(x)) checks
@@ -2805,7 +2945,12 @@ function setSignal(el, v) {
2805
2945
  if (!valueChanged) return v;
2806
2946
  if (el._pendingValue === NOT_PENDING) queuePendingNode(el);
2807
2947
  el._pendingValue = v;
2808
- GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, v);
2948
+ // syncCompanions only pokes _pendingSignal/_latestValueComputed — with
2949
+ // neither companion present the call is a guaranteed no-op (companions are
2950
+ // only ever created, never removed, and creating one installs the hook).
2951
+ (el._pendingSignal !== undefined || el._latestValueComputed !== undefined) &&
2952
+ GlobalQueue._syncCompanions !== null &&
2953
+ GlobalQueue._syncCompanions(el, v);
2809
2954
  el._time = clock;
2810
2955
  insertSubs(el);
2811
2956
  schedule();
@@ -3147,7 +3292,11 @@ function optimisticWrite(el, v) {
3147
3292
  // brand, and erasing it makes the write invisible and routes follow-up
3148
3293
  // writes off the optimistic path into permanent commits (#2898).
3149
3294
  el._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
3150
- GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, v);
3295
+ // syncCompanions only pokes _pendingSignal/_latestValueComputed — with
3296
+ // neither companion present the call is a guaranteed no-op.
3297
+ (el._pendingSignal !== undefined || el._latestValueComputed !== undefined) &&
3298
+ GlobalQueue._syncCompanions !== null &&
3299
+ GlobalQueue._syncCompanions(el, v);
3151
3300
  el._time = clock;
3152
3301
  insertSubs(el, true);
3153
3302
  schedule();
@@ -4591,27 +4740,70 @@ function keyedMatch(a, b, keyFn) {
4591
4740
  // and `in` dependencies should follow the new value's membership. Use
4592
4741
  // membership rather than length arithmetic so sparse arrays and named array
4593
4742
  // props behave like normal property reads.
4743
+ // Inline key iteration on purpose: these loops run per array target per
4744
+ // reconcile pass, and a shared helper means a closure + per-key call in
4745
+ // instruction counts. String-only records (the common case) iterate in
4746
+ // place; symbol-bearing records take the nodeKeys array path.
4594
4747
  function syncArrayNodeMembership(target, next) {
4595
4748
  let nodes = target[STORE_NODE];
4596
4749
  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);
4750
+ if (symbolKeyedRecords.has(nodes)) {
4751
+ const keys = nodeKeys(nodes);
4752
+ for (let i = 0, len = keys.length; i < len; i++) {
4753
+ keys[i] in next || setSignal(nodes[keys[i]], undefined);
4754
+ }
4755
+ } else {
4756
+ for (const key in nodes) {
4757
+ key in next || setSignal(nodes[key], undefined);
4758
+ }
4601
4759
  }
4602
4760
  }
4603
4761
  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);
4762
+ if (symbolKeyedRecords.has(nodes)) {
4763
+ const keys = nodeKeys(nodes);
4764
+ for (let i = 0, len = keys.length; i < len; i++) {
4765
+ setSignal(nodes[keys[i]], keys[i] in next);
4766
+ }
4767
+ } else {
4768
+ for (const key in nodes) {
4769
+ setSignal(nodes[key], key in next);
4770
+ }
4608
4771
  }
4609
4772
  }
4610
4773
  }
4774
+ // Recurse into a matched wrappable child pair without manufacturing a proxy:
4775
+ // resolve the child's target through the lookup and dispatch directly. A
4776
+ // lookup miss means the child was never observed — no proxy, no nodes, no
4777
+ // subscribers anywhere below — so the parent's swap making `next`
4778
+ // authoritative IS the whole update; a later read wraps next's child on
4779
+ // demand. This turns the diff from O(previous graph) into O(observed graph)
4780
+ // and drops the wrap()/$PROXY/$TARGET round-trip per visited child.
4781
+ // Wrap-family stores (projections/optimistic) own child proxy creation and
4782
+ // keep the proxy-based recursion.
4783
+ function applyStateChild(next, prevRaw, target, keyFn) {
4784
+ if (target[STORE_WRAP] !== undefined) {
4785
+ applyState(next, wrap(prevRaw, target), keyFn);
4786
+ return;
4787
+ }
4788
+ const childTarget = prevRaw[$TARGET] ?? storeLookup.get(prevRaw);
4789
+ if (childTarget === undefined) return;
4790
+ next = unwrap(next);
4791
+ if (childTarget[STORE_SHALLOW]) {
4792
+ applyStateShallow(next, childTarget);
4793
+ } else if (childTarget[STORE_OVERRIDE] || childTarget[STORE_OPTIMISTIC_OVERRIDE]) {
4794
+ applyStateSlow(next, childTarget, keyFn);
4795
+ } else {
4796
+ applyStateFast(next, childTarget, keyFn);
4797
+ }
4798
+ }
4611
4799
  // Reconcile a single array slot: recurse into a wrappable pair, otherwise replace
4612
4800
  // the node's value outright (covers object→primitive and primitive→object).
4613
4801
  function applyArrayItem(next, previous, target, node, keyFn) {
4614
- if (isWrappable(next) && isWrappable(previous)) {
4802
+ if (
4803
+ isWrappable(next) &&
4804
+ isWrappable(previous) &&
4805
+ !(rawValuesUsed && (isRawValue(previous) || isRawValue(next)))
4806
+ ) {
4615
4807
  const wrapped = wrap(previous, target);
4616
4808
  node && setSignal(node, wrapped);
4617
4809
  applyState(next, wrapped, keyFn);
@@ -4631,27 +4823,54 @@ function applyArrayItem(next, previous, target, node, keyFn) {
4631
4823
  */
4632
4824
  function applyDescendants(previous, next, target, nodes, keyFn, override, optOverride) {
4633
4825
  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];
4826
+ if (override) {
4827
+ const keys = getKeys(previous, override).concat(getStoreSymbols(previous, override));
4828
+ for (let i = 0, len = keys.length; i < len; i++) {
4829
+ const key = keys[i];
4830
+ if (nodes?.[key]) continue; // main loop already diffed this slot
4831
+ const previousValue = unwrap(getOverrideValue(previous, override, key, optOverride));
4832
+ if (!isWrappable(previousValue)) continue;
4833
+ descendInto(previousValue, next[key], lookup, keyFn);
4834
+ }
4835
+ return;
4836
+ }
4837
+ // No-override path (every applyStateFast call): iterate in place instead of
4838
+ // building keys + symbols + concat arrays per object per pass. The cheap
4839
+ // bails (noded key, primitive value) stay inline — only genuine descent
4840
+ // candidates pay a call.
4841
+ for (const key in previous) {
4639
4842
  if (nodes?.[key]) continue; // main loop already diffed this slot
4640
- const previousValue = unwrap(
4641
- override ? getOverrideValue(previous, override, key, optOverride) : previous[key]
4642
- );
4843
+ const previousValue = unwrap(previous[key]);
4643
4844
  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);
4845
+ descendInto(previousValue, next[key], lookup, keyFn);
4846
+ }
4847
+ const syms = Object.getOwnPropertySymbols(previous);
4848
+ for (let i = 0, len = syms.length; i < len; i++) {
4849
+ if (Object.prototype.propertyIsEnumerable.call(previous, syms[i])) {
4850
+ if (nodes?.[syms[i]]) continue;
4851
+ const previousValue = unwrap(previous[syms[i]]);
4852
+ if (!isWrappable(previousValue)) continue;
4853
+ descendInto(previousValue, next[syms[i]], lookup, keyFn);
4854
+ }
4855
+ }
4856
+ }
4857
+ function descendInto(previousValue, rawNext, lookup, keyFn) {
4858
+ const childTarget = lookupTarget(previousValue, lookup);
4859
+ if (!childTarget?.[STORE_DESC]) return;
4860
+ const nextValue = unwrap(rawNext);
4861
+ if (
4862
+ previousValue === nextValue ||
4863
+ !isWrappable(nextValue) ||
4864
+ Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4865
+ (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4866
+ )
4867
+ return;
4868
+ if (childTarget[STORE_SHALLOW]) {
4869
+ applyStateShallow(nextValue, childTarget);
4870
+ } else if (childTarget[STORE_OVERRIDE] || childTarget[STORE_OPTIMISTIC_OVERRIDE]) {
4871
+ applyStateSlow(nextValue, childTarget, keyFn);
4872
+ } else {
4873
+ applyStateFast(nextValue, childTarget, keyFn);
4655
4874
  }
4656
4875
  }
4657
4876
  // Dispatcher: every applyState call (including recursion) checks for the
@@ -4665,18 +4884,101 @@ function applyState(next, state, keyFn) {
4665
4884
  next = unwrap(next);
4666
4885
  const target = state?.[$TARGET];
4667
4886
  if (!target) return;
4668
- if (target[STORE_OVERRIDE] || target[STORE_OPTIMISTIC_OVERRIDE]) {
4887
+ if (target[STORE_SHALLOW]) {
4888
+ applyStateShallow(next, target);
4889
+ } else if (target[STORE_OVERRIDE] || target[STORE_OPTIMISTIC_OVERRIDE]) {
4669
4890
  applyStateSlow(next, target, keyFn);
4670
4891
  } else {
4671
4892
  applyStateFast(next, target, keyFn);
4672
4893
  }
4673
4894
  }
4895
+ // Shallow boundary diff: the target's own keys are reactive, its values are
4896
+ // raw records replaced by reference — no recursion, no wrapping. Arrays merge
4897
+ // positionally (below a shallow boundary the VALUE is the identity; keyed
4898
+ // row identity belongs to the consumer, e.g. <For keyed>). Incoming
4899
+ // wrappables are sticky-marked raw so they present raw through every store.
4900
+ // One pass over a shallow target's node record: replace changed slots with
4901
+ // raw next values, null out slots absent from next. Returns whether anything
4902
+ // differed.
4903
+ function shallowDiffNodes(nodes, next, prevAt, skipLength) {
4904
+ let changed = false;
4905
+ for (const key in nodes) {
4906
+ if (skipLength && key === "length") continue;
4907
+ if (key in next) {
4908
+ const v = next[key];
4909
+ if (v !== prevAt(key)) {
4910
+ changed = true;
4911
+ setSignal(nodes[key], v);
4912
+ }
4913
+ } else {
4914
+ changed = true;
4915
+ setSignal(nodes[key], undefined);
4916
+ }
4917
+ }
4918
+ return changed;
4919
+ }
4920
+ function applyStateShallow(next, target, keyFn) {
4921
+ const previous = target[STORE_VALUE];
4922
+ const override = target[STORE_OVERRIDE];
4923
+ const optOverride = target[STORE_OPTIMISTIC_OVERRIDE];
4924
+ if (next === previous && !override && !optOverride) return;
4925
+ // Setter-staged writes fold into the diff: previous values resolve through
4926
+ // the override layers (so a replaced slot compares against what readers
4927
+ // saw), and the regular override clears with the swap — reconcile makes
4928
+ // `next` the authoritative base, same as the deep slow path.
4929
+ const prevAt = key => {
4930
+ const v = getOverrideValue(previous, override, key, optOverride);
4931
+ return v === $DELETED ? undefined : v;
4932
+ };
4933
+ target[STORE_OVERRIDE] = undefined;
4934
+ const fam = target[STORE_LOOKUP];
4935
+ fam !== undefined ? fam.set(next, target[$PROXY]) : storeLookup.set(next, target);
4936
+ target[STORE_VALUE] = next;
4937
+ markRawIngest(next);
4938
+ const nodes = target[STORE_NODE];
4939
+ const tracked = nodes && nodes[$TRACK];
4940
+ let changed = false;
4941
+ if (Array.isArray(previous)) {
4942
+ const prevLength = override?.length ?? optOverride?.length ?? previous.length;
4943
+ if (nodes) {
4944
+ changed = shallowDiffNodes(nodes, next, prevAt, true);
4945
+ if (nodes.length && prevLength !== next.length) setSignal(nodes.length, next.length);
4946
+ }
4947
+ if (!changed && (tracked || target[STORE_HAS])) {
4948
+ // Slots without nodes still feed $TRACK enumerators / `in` probes.
4949
+ if (prevLength !== next.length) changed = true;
4950
+ else {
4951
+ for (let i = 0, len = next.length; i < len; i++) {
4952
+ if (prevAt(i) !== next[i]) {
4953
+ changed = true;
4954
+ break;
4955
+ }
4956
+ }
4957
+ }
4958
+ }
4959
+ } else {
4960
+ if (nodes) {
4961
+ changed = shallowDiffNodes(nodes, next, prevAt, false);
4962
+ }
4963
+ if (!changed && (tracked || target[STORE_HAS])) changed = true;
4964
+ }
4965
+ let has = target[STORE_HAS];
4966
+ if (has) {
4967
+ for (const key in has) {
4968
+ setSignal(has[key], key in next);
4969
+ }
4970
+ }
4971
+ changed && notifySelf(target);
4972
+ }
4674
4973
  function applyStateFast(next, target, keyFn) {
4675
4974
  const previous = target[STORE_VALUE];
4676
4975
  if (next === previous) return;
4677
4976
  const arrayNodes = target[STORE_NODE];
4678
4977
  // swap
4679
- (target[STORE_LOOKUP] || storeLookup).set(next, target[$PROXY]);
4978
+ {
4979
+ const fam = target[STORE_LOOKUP];
4980
+ fam !== undefined ? fam.set(next, target[$PROXY]) : storeLookup.set(next, target);
4981
+ }
4680
4982
  target[STORE_VALUE] = next;
4681
4983
  // merge
4682
4984
  if (Array.isArray(previous)) {
@@ -4689,10 +4991,22 @@ function applyStateFast(next, target, keyFn) {
4689
4991
  start < end && keyedMatch((item = previous[start]), next[start], keyFn);
4690
4992
  start++
4691
4993
  ) {
4692
- isWrappable(item) &&
4693
- isWrappable(next[start]) &&
4694
- applyState(next[start], wrap(item, target), keyFn);
4994
+ // keyedMatch established both sides wrappable unless they're the
4995
+ // SAME reference — and an identical slot is a guaranteed no-op
4996
+ // (the child's STORE_VALUE tracks the previous graph), so skip
4997
+ // the recursion dispatch entirely. Raw-marked values are leaves:
4998
+ // replace the slot node instead of recursing.
4999
+ if (item !== next[start]) {
5000
+ if (rawValuesUsed && (isRawValue(item) || isRawValue(next[start]))) {
5001
+ arrayNodes?.[start] && setSignal(arrayNodes[start], wrapValue(next[start], target));
5002
+ } else applyStateChild(next[start], item, target, keyFn);
5003
+ }
4695
5004
  }
5005
+ // Every position key-matched at equal length (the steady-state shape
5006
+ // of a polling tick): membership, length, and order are unchanged —
5007
+ // nothing below could do observable work, so skip the staging
5008
+ // allocations and membership sync outright.
5009
+ if (start === next.length && start === prevLength) return;
4696
5010
  const temp = new Array(next.length),
4697
5011
  newIndices = new Map();
4698
5012
  for (
@@ -4745,9 +5059,13 @@ function applyStateFast(next, target, keyFn) {
4745
5059
  } else if (next.length) {
4746
5060
  for (let i = 0, len = next.length; i < len; i++) {
4747
5061
  const item = previous[i];
4748
- if (isWrappable(item) && isWrappable(next[i]))
4749
- applyState(next[i], wrap(item, target), keyFn);
4750
- else {
5062
+ if (
5063
+ isWrappable(item) &&
5064
+ isWrappable(next[i]) &&
5065
+ !(rawValuesUsed && (isRawValue(item) || isRawValue(next[i])))
5066
+ ) {
5067
+ if (item !== next[i]) applyStateChild(next[i], item, target, keyFn);
5068
+ } else {
4751
5069
  if (item !== next[i]) changed = true;
4752
5070
  arrayNodes?.[i] && setSignal(arrayNodes[i], wrapValue(next[i], target));
4753
5071
  }
@@ -4766,17 +5084,54 @@ function applyStateFast(next, target, keyFn) {
4766
5084
  let tracked;
4767
5085
  if (nodes) {
4768
5086
  tracked = nodes[$TRACK];
5087
+ // The per-key body is duplicated across both loops on purpose: this is
5088
+ // the hottest object-diff site and a shared helper costs a per-key call
5089
+ // in instruction counts (CodSpeed regressed ~7% on deep-tree reconciles
5090
+ // with the extracted form).
4769
5091
  if (tracked || symbolKeyedRecords.has(nodes)) {
4770
5092
  const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
4771
5093
  for (let i = 0, len = keys.length; i < len; i++) {
4772
- diffNodeKey(keys[i], nodes, previous, next, target, tracked, keyFn);
5094
+ const key = keys[i];
5095
+ const node = nodes[key];
5096
+ const previousValue = unwrap(previous[key]);
5097
+ const nextValue = unwrap(next[key]);
5098
+ if (previousValue === nextValue) continue;
5099
+ if (
5100
+ !previousValue ||
5101
+ !isWrappable(previousValue) ||
5102
+ !isWrappable(nextValue) ||
5103
+ // Raw-marked values are leaves replaced by reference — a "wrappable
5104
+ // pair" is only recursable when both sides are actual store children.
5105
+ (rawValuesUsed && (isRawValue(previousValue) || isRawValue(nextValue))) ||
5106
+ Array.isArray(previousValue) !== Array.isArray(nextValue) ||
5107
+ (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
5108
+ ) {
5109
+ tracked && setSignal(tracked, void 0);
5110
+ node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
5111
+ } else applyStateChild(nextValue, previousValue, target, keyFn);
4773
5112
  }
4774
5113
  } else {
4775
5114
  // Untracked, string-only node records (the overwhelmingly common case)
4776
5115
  // iterate in place — nodeKeys() allocated a fresh key array per object
4777
5116
  // per pass, which dominates allocation on large-graph reconciles.
4778
5117
  for (const key in nodes) {
4779
- diffNodeKey(key, nodes, previous, next, target, tracked, keyFn);
5118
+ const node = nodes[key];
5119
+ const previousValue = unwrap(previous[key]);
5120
+ const nextValue = unwrap(next[key]);
5121
+ if (previousValue === nextValue) continue;
5122
+ if (
5123
+ !previousValue ||
5124
+ !isWrappable(previousValue) ||
5125
+ !isWrappable(nextValue) ||
5126
+ // Raw-marked values are leaves replaced by reference — a "wrappable
5127
+ // pair" is only recursable when both sides are actual store children.
5128
+ (rawValuesUsed && (isRawValue(previousValue) || isRawValue(nextValue))) ||
5129
+ Array.isArray(previousValue) !== Array.isArray(nextValue) ||
5130
+ (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
5131
+ ) {
5132
+ tracked && setSignal(tracked, void 0);
5133
+ node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
5134
+ } else applyStateChild(nextValue, previousValue, target, keyFn);
4780
5135
  }
4781
5136
  }
4782
5137
  }
@@ -4790,31 +5145,16 @@ function applyStateFast(next, target, keyFn) {
4790
5145
  }
4791
5146
  }
4792
5147
  }
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
5148
  function applyStateSlow(next, target, keyFn) {
4812
5149
  const previous = target[STORE_VALUE];
4813
5150
  const override = target[STORE_OVERRIDE];
4814
5151
  const optOverride = target[STORE_OPTIMISTIC_OVERRIDE];
4815
5152
  let nodes = target[STORE_NODE];
4816
5153
  // swap
4817
- (target[STORE_LOOKUP] || storeLookup).set(next, target[$PROXY]);
5154
+ {
5155
+ const fam = target[STORE_LOOKUP];
5156
+ fam !== undefined ? fam.set(next, target[$PROXY]) : storeLookup.set(next, target);
5157
+ }
4818
5158
  target[STORE_VALUE] = next;
4819
5159
  target[STORE_OVERRIDE] = undefined;
4820
5160
  // merge
@@ -4833,9 +5173,11 @@ function applyStateSlow(next, target, keyFn) {
4833
5173
  );
4834
5174
  start++
4835
5175
  ) {
4836
- isWrappable(item) &&
4837
- isWrappable(next[start]) &&
4838
- applyState(next[start], wrap(item, target), keyFn);
5176
+ if (isWrappable(item) && isWrappable(next[start]) && item !== next[start]) {
5177
+ if (rawValuesUsed && (isRawValue(item) || isRawValue(next[start]))) {
5178
+ nodes?.[start] && setSignal(nodes[start], wrapValue(next[start], target));
5179
+ } else applyState(next[start], wrap(item, target), keyFn);
5180
+ }
4839
5181
  }
4840
5182
  const temp = new Array(next.length),
4841
5183
  newIndices = new Map();
@@ -4894,9 +5236,13 @@ function applyStateSlow(next, target, keyFn) {
4894
5236
  } else if (next.length) {
4895
5237
  for (let i = 0, len = next.length; i < len; i++) {
4896
5238
  const item = getOverrideValue(previous, override, i, optOverride);
4897
- if (isWrappable(item) && isWrappable(next[i]))
4898
- applyState(next[i], wrap(item, target), keyFn);
4899
- else {
5239
+ if (
5240
+ isWrappable(item) &&
5241
+ isWrappable(next[i]) &&
5242
+ !(rawValuesUsed && (isRawValue(item) || isRawValue(next[i])))
5243
+ ) {
5244
+ if (item !== next[i]) applyState(next[i], wrap(item, target), keyFn);
5245
+ } else {
4900
5246
  if (item !== next[i]) changed = true;
4901
5247
  nodes?.[i] && setSignal(nodes[i], wrapValue(next[i], target));
4902
5248
  }
@@ -4926,6 +5272,7 @@ function applyStateSlow(next, target, keyFn) {
4926
5272
  !previousValue ||
4927
5273
  !isWrappable(previousValue) ||
4928
5274
  !isWrappable(nextValue) ||
5275
+ (rawValuesUsed && (isRawValue(previousValue) || isRawValue(nextValue))) ||
4929
5276
  Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4930
5277
  (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4931
5278
  ) {
@@ -4997,9 +5344,16 @@ function reconcile(value, key = "id") {
4997
5344
  function createProjectionInternal(fn, seed, options) {
4998
5345
  let node;
4999
5346
  const wrappedMap = new WeakMap();
5347
+ // A shallow projection's children are raw and never wrapped, so the
5348
+ // wrapper only ever runs for the root — flagging unconditionally is safe.
5349
+ const shallow = !!options?.shallow;
5000
5350
  const wrapper = s => {
5001
5351
  s[STORE_WRAP] = wrapProjection;
5002
5352
  s[STORE_LOOKUP] = wrappedMap;
5353
+ if (shallow) {
5354
+ s[STORE_SHALLOW] = true;
5355
+ markRawIngest(s[STORE_VALUE]);
5356
+ }
5003
5357
  Object.defineProperty(s, STORE_FIREWALL, {
5004
5358
  get() {
5005
5359
  return node;
@@ -5186,7 +5540,8 @@ const STORE_VALUE = "v",
5186
5540
  STORE_OPTIMISTIC = "p",
5187
5541
  STORE_OPTIMISTIC_OWNERS = "t",
5188
5542
  STORE_PARENT = "u",
5189
- STORE_DESC = "d";
5543
+ STORE_DESC = "d",
5544
+ STORE_SHALLOW = "s";
5190
5545
  const STORE_SELF_PENDING = Symbol("STORE_SELF_PENDING");
5191
5546
  // Every StoreNode field is initialized up front, in one fixed order, so all
5192
5547
  // targets share a single hidden class. The traps and reconcile read these
@@ -5208,6 +5563,7 @@ function initStoreFields(newTarget) {
5208
5563
  newTarget[STORE_SNAPSHOT_PROPS] = undefined;
5209
5564
  newTarget[STORE_PARENT] = undefined;
5210
5565
  newTarget[STORE_DESC] = undefined;
5566
+ newTarget[STORE_SHALLOW] = undefined;
5211
5567
  newTarget[$PROXY] = null;
5212
5568
  }
5213
5569
  function createStoreProxy(value, traps = storeTraps, extend) {
@@ -5228,24 +5584,106 @@ function createStoreProxy(value, traps = storeTraps, extend) {
5228
5584
  extend && extend(newTarget);
5229
5585
  return (newTarget[$PROXY] = new Proxy(newTarget, traps));
5230
5586
  }
5587
+ // The global lookup maps raw value -> StoreNode TARGET (not proxy): reconcile
5588
+ // and the unwrap/snapshot walks resolve targets through it constantly, and a
5589
+ // target hit is a plain field read away from its proxy while a proxy hit
5590
+ // costs a trap to get back to the target. Per-family STORE_LOOKUP maps
5591
+ // (projections/optimistic) still map raw -> proxy — their wrap functions own
5592
+ // that contract — so mixed-lookup consumers resolve through lookupTarget().
5231
5593
  const storeLookup = new WeakMap();
5232
5594
  // Node records that hold at least one user (non-`$TRACK`) symbol-keyed node.
5233
5595
  // Lets reconcile enumerate symbols only for records that need it (#2851).
5234
5596
  const symbolKeyedRecords = new WeakSet();
5597
+ function lookupTarget(value, lookup) {
5598
+ if (lookup !== undefined && lookup !== storeLookup) {
5599
+ const p = lookup.get(value);
5600
+ if (p !== undefined) return p[$TARGET];
5601
+ }
5602
+ return storeLookup.get(value);
5603
+ }
5604
+ // Values marked raw never acquire a proxy identity: wrap() serves them as-is
5605
+ // everywhere — deep stores hold them as leaf values replaced by reference.
5606
+ // Once raw, always raw (identity stays single, just unwrapped). Consulted
5607
+ // only on wrap-creation and ingest paths; reads never touch it.
5608
+ const rawValues = new WeakSet();
5609
+ /**
5610
+ * Marks a value as raw: no store will ever wrap it — every store presents it
5611
+ * as-is, tracked by reference at whatever slot holds it and updated by
5612
+ * replacement. Useful for class instances and external objects (editors,
5613
+ * scene graphs, Maps) and for record-shaped data updated wholesale. Sticky
5614
+ * for the value's lifetime.
5615
+ */
5616
+ // Flipped on the first mark and exported as a LIVE binding: reconcile
5617
+ // consults it on every recursable pair, and importing the boolean directly
5618
+ // lets those sites skip even the function call when no shallow store or raw
5619
+ // mark exists anywhere in the app.
5620
+ let rawValuesUsed = false;
5621
+ function isRawValue(value) {
5622
+ return rawValuesUsed && rawValues.has(value);
5623
+ }
5624
+ function markRawOne(v) {
5625
+ if (isWrappable(v)) {
5626
+ // A store proxy is already tracked elsewhere: the shallow boundary passes
5627
+ // it through by reference (replaced, never edited — same slot semantics
5628
+ // as a raw) instead of claiming it raw. The sticky mark is global, so
5629
+ // marking a live proxy would make wrap() serve it verbatim through every
5630
+ // OTHER store too — downstream deep stores then captured it instead of
5631
+ // wrapping it in their own family, and their writes landed in the
5632
+ // upstream store's override layer (#2932).
5633
+ if (v[$TARGET] !== undefined) return;
5634
+ if (storeLookup.has(v))
5635
+ throw new Error(
5636
+ "shallow store: an ingested record is already tracked as a deep store — one value cannot present both wrapped and raw"
5637
+ );
5638
+ rawValuesUsed = true;
5639
+ rawValues.add(v);
5640
+ }
5641
+ }
5642
+ function markRawIngest(container) {
5643
+ if (Array.isArray(container)) {
5644
+ for (let i = 0, len = container.length; i < len; i++) markRawOne(container[i]);
5645
+ } else {
5646
+ for (const k in container) markRawOne(container[k]);
5647
+ }
5648
+ }
5235
5649
  function wrap(value, target) {
5650
+ // Raw is raw in every family: the mark must preempt family wrapping too,
5651
+ // or a shallow projection/optimistic store would proxy its raw records.
5652
+ if (rawValuesUsed && rawValues.has(value)) return value;
5236
5653
  if (target?.[STORE_WRAP]) {
5237
5654
  const p = target[STORE_WRAP](value, target);
5238
5655
  const t = p[$TARGET];
5239
5656
  if (t && !t[STORE_PARENT] && t !== target) t[STORE_PARENT] = target;
5240
5657
  return p;
5241
5658
  }
5242
- let p = value[$PROXY] || storeLookup.get(value);
5659
+ const t = storeLookup.get(value);
5660
+ if (t !== undefined) return t[$PROXY];
5661
+ let p = value[$PROXY];
5243
5662
  if (!p) {
5244
- storeLookup.set(value, (p = createStoreProxy(value)));
5245
- if (target) p[$TARGET][STORE_PARENT] = target;
5663
+ p = createStoreProxy(value);
5664
+ const newTarget = p[$TARGET];
5665
+ storeLookup.set(value, newTarget);
5666
+ if (target) newTarget[STORE_PARENT] = target;
5246
5667
  }
5247
5668
  return p;
5248
5669
  }
5670
+ // Shallow store root: the target itself is fully reactive (per-key nodes,
5671
+ // membership, $TRACK); its values are served raw. Seed values are marked at
5672
+ // creation; reconcile and the set trap mark on ingest.
5673
+ function wrapShallow(value) {
5674
+ const existing = storeLookup.get(value);
5675
+ if (existing !== undefined) {
5676
+ if (existing[STORE_SHALLOW]) return existing[$PROXY];
5677
+ throw new Error("createStore({ shallow }): value is already tracked as a deep store");
5678
+ }
5679
+ if (value[$TARGET]) throw new Error("createStore({ shallow }): value is already a store proxy");
5680
+ const p = createStoreProxy(value);
5681
+ const newTarget = p[$TARGET];
5682
+ newTarget[STORE_SHALLOW] = true;
5683
+ storeLookup.set(value, newTarget);
5684
+ markRawIngest(value);
5685
+ return p;
5686
+ }
5249
5687
  function isWrappable(obj) {
5250
5688
  if (obj == null || typeof obj !== "object" || Object.isFrozen(obj)) return false;
5251
5689
  // Dynamic Node check (kept dynamic so test/SSR overrides of `globalThis.Node`
@@ -5260,7 +5698,7 @@ function writeOnly(proxy) {
5260
5698
  return writeOverride || !!Writing?.has(proxy);
5261
5699
  }
5262
5700
  function unwrapStoreValue(value, map, lookup) {
5263
- const target = value?.[$TARGET] || lookup?.get(value)?.[$TARGET];
5701
+ const target = value?.[$TARGET] || lookupTarget(value, lookup);
5264
5702
  if (!target) return value;
5265
5703
  const override = target[STORE_OVERRIDE];
5266
5704
  if (!override) return target[STORE_VALUE];
@@ -5447,7 +5885,7 @@ function walkAffectsScope(
5447
5885
  visited
5448
5886
  ) {
5449
5887
  if (!isWrappable(value)) return;
5450
- const target = value[$TARGET] || (lookup ?? storeLookup).get(value)?.[$TARGET];
5888
+ const target = value[$TARGET] || lookupTarget(value, lookup);
5451
5889
  const raw = target ? target[STORE_VALUE] : value;
5452
5890
  if (visited.has(raw)) return;
5453
5891
  visited.add(raw);
@@ -5814,8 +6252,25 @@ const storeTraps = {
5814
6252
  const nodes = target[STORE_NODE];
5815
6253
  const node = nodes && nodes[property];
5816
6254
  if (node !== undefined && target[STORE_VALUE][$TARGET] === undefined) {
5817
- let value = read(node);
6255
+ // readNodeFast is read()'s plain-signal fast path hoisted over the
6256
+ // call; READ_SLOW means a global read window (latest/pending-check/
6257
+ // transition/lane/snapshot capture) or a node layer is active, and
6258
+ // only then does the full read() resolution have anything to do.
6259
+ let value = readNodeFast(node);
6260
+ if (value === READ_SLOW) value = read(node);
5818
6261
  if (value === $DELETED) value = undefined;
6262
+ // Every node-writing site wraps wrappables before setSignal (see the
6263
+ // dev assertion below), so re-wrapping on read is redundant — except
6264
+ // during snapshot capture, where read() can surface a raw captured
6265
+ // value seeded from snapshot props.
6266
+ if (!snapshotCaptureActive) {
6267
+ if (isWrappable(value) && wrap(value, target) !== value) {
6268
+ throw new Error(
6269
+ "store node invariant violated: node held an unwrapped wrappable value"
6270
+ );
6271
+ }
6272
+ return value;
6273
+ }
5819
6274
  return isWrappable(value) ? wrap(value, target) : value;
5820
6275
  }
5821
6276
  }
@@ -5859,6 +6314,11 @@ const storeTraps = {
5859
6314
  tracked && (overridden || !proxySource) ? visibleNodeValue(tracked) : storeValue[property];
5860
6315
  value === $DELETED && (value = undefined);
5861
6316
  if (!isWrappable(value)) return value;
6317
+ // Shallow boundary: records are replaced, never edited in place. Reads
6318
+ // inside a setter serve the raw so read-then-replace, filter/pop and
6319
+ // projection derives all work; in-place mutation of a raw is inert by
6320
+ // construction — the same contract as a markRaw child in a deep store.
6321
+ if (target[STORE_SHALLOW]) return value;
5862
6322
  const wrapped = wrap(value, target);
5863
6323
  Writing?.add(wrapped);
5864
6324
  return wrapped;
@@ -5909,7 +6369,14 @@ const storeTraps = {
5909
6369
  }
5910
6370
  // Untracked fall-through (tracked reads already threw via their node in
5911
6371
  // read(); the dev strictRead error above wins first for memo parity).
5912
- if (!selfRead) throwIfUninitialized(target);
6372
+ // Observer-present reads must NOT re-consult the flag here: during the
6373
+ // settle flush the firewall has recomputed (first values live on the
6374
+ // pending rail, served by read() above) but its UNINITIALIZED clear is
6375
+ // deferred to batch commit — vetoing read()'s verdict with the stale flag
6376
+ // threw a fresh NotReadyError for an already-settled source, which no
6377
+ // sweep would ever release (#2938: projection over an async store wedged
6378
+ // its Loading boundary on `undefined`).
6379
+ if (!selfRead && !getObserver()) throwIfUninitialized(target);
5913
6380
  return isWrappable(value) ? wrap(value, target) : value;
5914
6381
  },
5915
6382
  has(target, property) {
@@ -5943,7 +6410,19 @@ const storeTraps = {
5943
6410
  const prevHas = prevLayer
5944
6411
  ? prevLayer[property] !== $DELETED
5945
6412
  : property in target[STORE_VALUE];
5946
- const value = unwrapStoreValue(rawValue);
6413
+ // Shallow slots hold store proxies verbatim (pass-through reference,
6414
+ // never raw-marked — see markRawOne/#2932); everything else unwraps
6415
+ // and marks as usual.
6416
+ const passThrough = !!target[STORE_SHALLOW] && rawValue?.[$TARGET] !== undefined;
6417
+ const value = passThrough ? rawValue : unwrapStoreValue(rawValue);
6418
+ if (target[STORE_SHALLOW] && !passThrough && isWrappable(value)) {
6419
+ // Flip the live gate too: a bare add was inert unless something else
6420
+ // had already marked a raw somewhere (wrap() checks rawValuesUsed
6421
+ // first), so the documented set-trap ingest mark silently no-oped in
6422
+ // apps whose only shallow data arrived through writes.
6423
+ rawValuesUsed = true;
6424
+ rawValues.add(value);
6425
+ }
5947
6426
  // Symbol-keyed writes on arrays are metadata, not index writes — never run
5948
6427
  // them through the numeric index/length machinery (`parseInt` on a symbol
5949
6428
  // throws). #2769
@@ -6160,7 +6639,11 @@ function storeSetter(store, fn) {
6160
6639
  }
6161
6640
  function createStore(first, second, options) {
6162
6641
  const derived = typeof first === "function",
6163
- wrappedStore = derived ? createProjectionInternal(first, second, options).store : wrap(first);
6642
+ wrappedStore = derived
6643
+ ? createProjectionInternal(first, second, options).store
6644
+ : second?.shallow
6645
+ ? wrapShallow(first)
6646
+ : wrap(first);
6164
6647
  registerGraph(wrappedStore, getOwner());
6165
6648
  return [
6166
6649
  wrappedStore,
@@ -6324,6 +6807,8 @@ function createOptimisticStore(first, second, options) {
6324
6807
  installOptimisticEngine();
6325
6808
  GlobalQueue._clearOptimisticStores ||= clearOptimisticStores;
6326
6809
  const derived = typeof first === "function";
6810
+ // Plain form: the second slot carries options.
6811
+ if (!derived && options === undefined) options = second;
6327
6812
  const initialValue = derived ? second : first;
6328
6813
  const fn = derived ? first : undefined;
6329
6814
  // Create optimistic projection store
@@ -6425,9 +6910,14 @@ function clearOptimisticOverride(target, completing) {
6425
6910
  function createOptimisticProjectionInternal(fn, initialValue, options) {
6426
6911
  let node;
6427
6912
  const wrappedMap = new WeakMap();
6913
+ const shallow = !!options?.shallow;
6428
6914
  const wrapper = s => {
6429
6915
  s[STORE_WRAP] = wrapProjection;
6430
6916
  s[STORE_LOOKUP] = wrappedMap;
6917
+ if (shallow) {
6918
+ s[STORE_SHALLOW] = true;
6919
+ markRawIngest(s[STORE_VALUE]);
6920
+ }
6431
6921
  s[STORE_OPTIMISTIC] = true; // Mark as optimistic store
6432
6922
  Object.defineProperty(s, STORE_FIREWALL, {
6433
6923
  get() {
@@ -6596,7 +7086,7 @@ function snapshotImpl(item, track, map, lookup) {
6596
7086
  if (!isWrappable(item)) return item;
6597
7087
  if (map && map.has(item)) return map.get(item);
6598
7088
  if (!map) map = new Map();
6599
- if ((target = item[$TARGET] || lookup?.get(item)?.[$TARGET])) {
7089
+ if ((target = item[$TARGET] || lookupTarget(item, lookup))) {
6600
7090
  if (track) {
6601
7091
  trackSelf(target, $TRACK);
6602
7092
  // A tracked walk reads THROUGH the record without touching the proxy
@@ -6634,7 +7124,7 @@ function snapshotImpl(item, track, map, lookup) {
6634
7124
  for (let i = 0; i < len; i++) {
6635
7125
  v = override && i in override ? override[i] : item[i];
6636
7126
  if (v === $DELETED) continue;
6637
- if (track && isWrappable(v)) wrap(v, target);
7127
+ if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
6638
7128
  if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== v || result) {
6639
7129
  if (!result) map.set(item, (result = [...item]));
6640
7130
  result[i] = unwrapped;
@@ -6648,7 +7138,7 @@ function snapshotImpl(item, track, map, lookup) {
6648
7138
  const desc = getPropertyDescriptor(item, override, prop);
6649
7139
  if (!desc || desc.get) continue;
6650
7140
  v = override && prop in override ? override[prop] : item[prop];
6651
- if (track && isWrappable(v)) wrap(v, target);
7141
+ if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
6652
7142
  unwrapped = snapshotImpl(v, track, map, lookup);
6653
7143
  if (unwrapped !== v || result) {
6654
7144
  if (!result) map.set(item, (result = Object.assign([...item], item)));
@@ -6670,7 +7160,7 @@ function snapshotImpl(item, track, map, lookup) {
6670
7160
  const desc = Object.getOwnPropertyDescriptor(item, prop);
6671
7161
  if (desc.get) continue;
6672
7162
  v = desc.value;
6673
- if (track && isWrappable(v)) wrap(v, target);
7163
+ if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
6674
7164
  if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== v || result) {
6675
7165
  if (!result) {
6676
7166
  result = Object.create(Object.getPrototypeOf(item));
@@ -6688,7 +7178,7 @@ function snapshotImpl(item, track, map, lookup) {
6688
7178
  const desc = getPropertyDescriptor(item, override, prop);
6689
7179
  if (desc.get) continue;
6690
7180
  v = prop in override ? override[prop] : item[prop];
6691
- if (track && isWrappable(v)) wrap(v, target);
7181
+ if (track && isWrappable(v) && !(rawValuesUsed && isRawValue(v))) wrap(v, target);
6692
7182
  if ((unwrapped = snapshotImpl(v, track, map, lookup)) !== item[prop] || result) {
6693
7183
  if (!result) {
6694
7184
  result = Object.create(Object.getPrototypeOf(item));