@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 +577 -87
- package/dist/node.cjs +1120 -683
- package/dist/prod/core/async.js +180 -107
- package/dist/prod/core/core.js +214 -180
- package/dist/prod/core/effect.js +19 -19
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +25 -25
- package/dist/prod/core/heap.js +23 -23
- package/dist/prod/core/lanes.js +15 -15
- package/dist/prod/core/optimistic.js +42 -40
- package/dist/prod/core/owner.js +41 -41
- package/dist/prod/core/scheduler.js +90 -90
- package/dist/prod/core/verdict.js +54 -54
- package/dist/prod/map.js +60 -60
- package/dist/prod/signals.js +1 -1
- package/dist/prod/store/optimistic.js +18 -11
- package/dist/prod/store/projection.js +25 -18
- package/dist/prod/store/reconcile.js +405 -211
- package/dist/prod/store/store.js +225 -105
- package/dist/prod/store/utils.js +22 -22
- package/dist/types/core/async.d.ts +1 -0
- package/dist/types/core/core.d.ts +15 -0
- package/dist/types/store/optimistic.d.ts +1 -1
- package/dist/types/store/store.d.ts +20 -2
- package/dist/types-cjs/core/async.d.cts +1 -0
- package/dist/types-cjs/core/core.d.cts +15 -0
- package/dist/types-cjs/store/optimistic.d.cts +1 -1
- package/dist/types-cjs/store/store.d.cts +20 -2
- package/package.json +1 -1
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
|
-
|
|
1844
|
+
settleTransition();
|
|
1782
1845
|
// NotReadyError from rejected promises should be treated as pending, not error
|
|
1783
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
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
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
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 (
|
|
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
|
-
|
|
4635
|
-
getStoreSymbols(previous, override)
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
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
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
(
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
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 (
|
|
4749
|
-
|
|
4750
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4838
|
-
|
|
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 (
|
|
4898
|
-
|
|
4899
|
-
|
|
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
|
-
|
|
5659
|
+
const t = storeLookup.get(value);
|
|
5660
|
+
if (t !== undefined) return t[$PROXY];
|
|
5661
|
+
let p = value[$PROXY];
|
|
5243
5662
|
if (!p) {
|
|
5244
|
-
|
|
5245
|
-
|
|
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] ||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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] ||
|
|
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));
|