@solidjs/signals 2.0.0-beta.25 → 2.0.0-beta.27
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 +370 -73
- package/dist/node.cjs +1247 -946
- package/dist/prod/core/action.js +11 -6
- package/dist/prod/core/async.js +212 -108
- package/dist/prod/core/core.js +203 -189
- package/dist/prod/core/effect.js +28 -28
- package/dist/prod/core/external.js +3 -3
- package/dist/prod/core/graph.js +28 -28
- package/dist/prod/core/heap.js +30 -30
- package/dist/prod/core/lanes.js +15 -15
- package/dist/prod/core/optimistic.js +39 -39
- package/dist/prod/core/owner.js +44 -44
- package/dist/prod/core/scheduler.js +127 -106
- 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 +55 -36
- package/dist/prod/store/projection.js +38 -33
- package/dist/prod/store/reconcile.js +299 -243
- package/dist/prod/store/store.js +127 -50
- package/dist/types/core/async.d.ts +2 -0
- package/dist/types/core/scheduler.d.ts +1 -0
- package/dist/types/store/optimistic.d.ts +2 -2
- package/dist/types/store/projection.d.ts +7 -2
- package/dist/types/store/reconcile.d.ts +14 -0
- package/dist/types/store/store.d.ts +2 -2
- package/dist/types-cjs/core/async.d.cts +2 -0
- package/dist/types-cjs/core/scheduler.d.cts +1 -0
- package/dist/types-cjs/store/optimistic.d.cts +2 -2
- package/dist/types-cjs/store/projection.d.cts +7 -2
- package/dist/types-cjs/store/reconcile.d.cts +14 -0
- package/dist/types-cjs/store/store.d.cts +2 -2
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -591,10 +591,14 @@ function resetErrorHalt() {
|
|
|
591
591
|
halted = false;
|
|
592
592
|
haltNotified = false;
|
|
593
593
|
}
|
|
594
|
+
// Identifies one child-traversal pass in `Queue.run` so a rescan after the
|
|
595
|
+
// child list shifts can tell "already run this pass" from "still pending".
|
|
596
|
+
let queueRunToken = 0;
|
|
594
597
|
class Queue {
|
|
595
598
|
_parent = null;
|
|
596
599
|
_queues = [[], []];
|
|
597
600
|
_children = [];
|
|
601
|
+
_ranAt = 0;
|
|
598
602
|
created = clock;
|
|
599
603
|
addChild(child) {
|
|
600
604
|
this._children.push(child);
|
|
@@ -617,7 +621,27 @@ class Queue {
|
|
|
617
621
|
this._queues[type - 1] = [];
|
|
618
622
|
runQueue$1(effects, type);
|
|
619
623
|
}
|
|
620
|
-
|
|
624
|
+
// Effects run here can dispose owners, and disposal removes queues from
|
|
625
|
+
// this list — the running child itself, an earlier sibling, or several at
|
|
626
|
+
// once. A plain index walk then skips whatever shifted into the cursor.
|
|
627
|
+
// Stamping each child before it runs makes the pass idempotent, so a shift
|
|
628
|
+
// can be recovered by rescanning from the front and every child still runs
|
|
629
|
+
// exactly once. Children appended mid-pass carry a stale stamp and run,
|
|
630
|
+
// matching the previous live-array behaviour.
|
|
631
|
+
const children = this._children;
|
|
632
|
+
const token = ++queueRunToken;
|
|
633
|
+
for (let i = 0; i < children.length; ) {
|
|
634
|
+
const child = children[i];
|
|
635
|
+
if (child._ranAt !== token) {
|
|
636
|
+
child._ranAt = token;
|
|
637
|
+
child.run?.(type);
|
|
638
|
+
if (children[i] !== child) {
|
|
639
|
+
i = 0;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
i++;
|
|
644
|
+
}
|
|
621
645
|
}
|
|
622
646
|
enqueue(type, fn) {
|
|
623
647
|
if (type) {
|
|
@@ -1091,11 +1115,10 @@ function transitionComplete(transition) {
|
|
|
1091
1115
|
break;
|
|
1092
1116
|
}
|
|
1093
1117
|
}
|
|
1094
|
-
// Override blockage lives with the engine
|
|
1095
|
-
// blockage"
|
|
1096
|
-
//
|
|
1097
|
-
if (done &&
|
|
1098
|
-
done = false;
|
|
1118
|
+
// Override blockage lives with the engine (absent hook = "no optimistic
|
|
1119
|
+
// blockage"); the hook's loops over _optimisticNodes/_optimisticStores are
|
|
1120
|
+
// no-ops when the transition holds neither, so no pre-check is needed.
|
|
1121
|
+
if (done && GlobalQueue._transitionBlocked?.(transition)) done = false;
|
|
1099
1122
|
done && (transition._done = true);
|
|
1100
1123
|
return done;
|
|
1101
1124
|
}
|
|
@@ -1103,9 +1126,6 @@ function currentTransition(transition) {
|
|
|
1103
1126
|
while (transition._done && typeof transition._done === "object") transition = transition._done;
|
|
1104
1127
|
return transition;
|
|
1105
1128
|
}
|
|
1106
|
-
function setActiveTransition(transition) {
|
|
1107
|
-
activeTransition = transition;
|
|
1108
|
-
}
|
|
1109
1129
|
function runInTransition(transition, fn) {
|
|
1110
1130
|
const prevTransition = activeTransition;
|
|
1111
1131
|
try {
|
|
@@ -1707,8 +1727,70 @@ function forEachDependent(el, fn) {
|
|
|
1707
1727
|
// Queue a node to re-run on the next flush (used both when a pending source
|
|
1708
1728
|
// settles and when an `isPending` observer must re-evaluate after a real error):
|
|
1709
1729
|
// shared scheduling helper in heap.ts (tracked effects bypass the heap).
|
|
1730
|
+
// Settle-time counterpart of unlinkSubs' last-one-out check. A lazy node that
|
|
1731
|
+
// loses its last subscriber while STATUS_PENDING is exempt from autodispose
|
|
1732
|
+
// (the in-flight work is an observer), so whatever CLEARS that pending state
|
|
1733
|
+
// must run the release — otherwise the node stays linked and recomputes
|
|
1734
|
+
// forever with zero subscribers (#2934). The node's own promise/iterator
|
|
1735
|
+
// callbacks handle their own release (settleAutodispose in handleAsync); this
|
|
1736
|
+
// covers derivatively-pending dependents, which have no callbacks of their own.
|
|
1737
|
+
function releaseIfSettledUnobserved(node) {
|
|
1738
|
+
node._fn &&
|
|
1739
|
+
node._config & CONFIG_AUTO_DISPOSE &&
|
|
1740
|
+
!node._subs &&
|
|
1741
|
+
!(node._flags & REACTIVE_ZOMBIE) &&
|
|
1742
|
+
!(node._statusFlags & STATUS_PENDING) &&
|
|
1743
|
+
unobserved(node);
|
|
1744
|
+
}
|
|
1745
|
+
// Error-path sweep: notifyStatus(STATUS_ERROR) clears dependents' pending
|
|
1746
|
+
// sources through its own recursion (no per-node settle callback), so after
|
|
1747
|
+
// the propagation completes, walk the same graph for stranded lazy nodes.
|
|
1748
|
+
// Collect-then-release so unobserved() never unlinks under the walk.
|
|
1749
|
+
function releaseSettledDependents(el) {
|
|
1750
|
+
let candidates;
|
|
1751
|
+
const visited = new Set();
|
|
1752
|
+
const visit = node => {
|
|
1753
|
+
if (visited.has(node)) return;
|
|
1754
|
+
visited.add(node);
|
|
1755
|
+
if (!node._subs && node._config & CONFIG_AUTO_DISPOSE) (candidates ??= []).push(node);
|
|
1756
|
+
forEachDependent(node, visit);
|
|
1757
|
+
};
|
|
1758
|
+
forEachDependent(el, visit);
|
|
1759
|
+
if (candidates) for (const node of candidates) releaseIfSettledUnobserved(node);
|
|
1760
|
+
}
|
|
1761
|
+
// Error-dimension twin of settlePendingSource's blocked re-enqueue (#2949):
|
|
1762
|
+
// a node in STATUS_ERROR that recovers by recomputing to an UNCHANGED value
|
|
1763
|
+
// fires no value notification — the recovery is completely silent. But a
|
|
1764
|
+
// dependent that re-ran during the error window consumed its dirty flag and
|
|
1765
|
+
// committed nothing (the fresh sibling values it read were absorbed into an
|
|
1766
|
+
// errored run), so its committed value is stale. The propagated error is one
|
|
1767
|
+
// object identity down the whole dependent tree, and holding it is exactly
|
|
1768
|
+
// the "blocked on this error" marker — re-enqueue those holders so they
|
|
1769
|
+
// re-run: fresh values commit and flow, and a dependent with another
|
|
1770
|
+
// still-broken source simply re-errors. The async dimension needs no twin of
|
|
1771
|
+
// its own: recovery there passes through a pending window whose re-runners
|
|
1772
|
+
// set _blocked and ride settlePendingSource. Walks the full dependent graph
|
|
1773
|
+
// (releaseSettledDependents shape): identity holders can sit below an
|
|
1774
|
+
// intermediate whose own error state has since been scrubbed or replaced
|
|
1775
|
+
// (e.g. an error boundary's tree node).
|
|
1776
|
+
function settleErroredDependents(el, error) {
|
|
1777
|
+
let scheduled = false;
|
|
1778
|
+
const visited = new Set();
|
|
1779
|
+
const visit = node => {
|
|
1780
|
+
if (visited.has(node)) return;
|
|
1781
|
+
visited.add(node);
|
|
1782
|
+
if (node._error === error) {
|
|
1783
|
+
enqueueSub(node);
|
|
1784
|
+
scheduled = true;
|
|
1785
|
+
}
|
|
1786
|
+
forEachDependent(node, visit);
|
|
1787
|
+
};
|
|
1788
|
+
forEachDependent(el, visit);
|
|
1789
|
+
if (scheduled) schedule();
|
|
1790
|
+
}
|
|
1710
1791
|
function settlePendingSource(el) {
|
|
1711
1792
|
let scheduled = false;
|
|
1793
|
+
let released;
|
|
1712
1794
|
const visited = new Set();
|
|
1713
1795
|
// Companion updates no-op without the verdict layer (null hook).
|
|
1714
1796
|
const updateCompanions = GlobalQueue._updatePendingSignal;
|
|
@@ -1729,10 +1811,17 @@ function settlePendingSource(el) {
|
|
|
1729
1811
|
scheduled = true;
|
|
1730
1812
|
}
|
|
1731
1813
|
node._blocked = false;
|
|
1814
|
+
// Fully settled with nobody watching: release candidate (#2934). Checked
|
|
1815
|
+
// again at release time — deferred so unobserved() can't unlink subs
|
|
1816
|
+
// lists this walk is still iterating.
|
|
1817
|
+
if (!node._subs && node._config & CONFIG_AUTO_DISPOSE) (released ??= []).push(node);
|
|
1732
1818
|
}
|
|
1733
1819
|
forEachDependent(node, settle);
|
|
1734
1820
|
};
|
|
1735
1821
|
forEachDependent(el, settle);
|
|
1822
|
+
// Release before the flush schedule below: unobserved() pulls the node back
|
|
1823
|
+
// out of the heap, so the enqueueSub above never recomputes a released node.
|
|
1824
|
+
if (released) for (const node of released) releaseIfSettledUnobserved(node);
|
|
1736
1825
|
if (scheduled) schedule();
|
|
1737
1826
|
}
|
|
1738
1827
|
// Object-thenable detection (Promises/A+ shape).
|
|
@@ -1776,12 +1865,41 @@ function handleAsync(el, result, setter) {
|
|
|
1776
1865
|
}
|
|
1777
1866
|
el._inFlight = result;
|
|
1778
1867
|
let syncValue;
|
|
1868
|
+
// Settle-time transition re-entry. The loading rail is invisible to
|
|
1869
|
+
// transactions (#2933): a boundary-caught first load never registers as an
|
|
1870
|
+
// async reporter, so its settle — the boundary's fallback -> content
|
|
1871
|
+
// reveal — must flow ambiently. The node can still carry a `_transition`
|
|
1872
|
+
// stamp (pending-node bookkeeping rides through the stamping sites), and
|
|
1873
|
+
// blindly re-entering that stamped, still-incomplete transaction stashed
|
|
1874
|
+
// the reveal with it — a deadlock when the transaction's completion
|
|
1875
|
+
// depended on the reveal (#2937). An ESCAPED first load did register and
|
|
1876
|
+
// keeps transition scheduling; initialized (value-holding) pending settles
|
|
1877
|
+
// are the transaction's reveal machinery and always re-enter.
|
|
1878
|
+
const settleTransition = () => {
|
|
1879
|
+
const transition = resolveTransition(el);
|
|
1880
|
+
if (
|
|
1881
|
+
transition &&
|
|
1882
|
+
el._statusFlags & STATUS_UNINITIALIZED &&
|
|
1883
|
+
!currentTransition(transition)._asyncReporters.has(el)
|
|
1884
|
+
) {
|
|
1885
|
+
// Drop the stale stamp too: the plain settle write (setSignal) and the
|
|
1886
|
+
// stash-path restamp both re-enter the transaction through it.
|
|
1887
|
+
el._transition = null;
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
globalQueue.initTransition(transition);
|
|
1891
|
+
};
|
|
1779
1892
|
const handleError = error => {
|
|
1780
1893
|
if (el._inFlight !== result) return;
|
|
1781
|
-
|
|
1894
|
+
settleTransition();
|
|
1782
1895
|
// NotReadyError from rejected promises should be treated as pending, not error
|
|
1783
|
-
|
|
1896
|
+
const stillPending = error instanceof NotReadyError;
|
|
1897
|
+
notifyStatus(el, stillPending ? STATUS_PENDING : STATUS_ERROR, error);
|
|
1784
1898
|
el._time = clock;
|
|
1899
|
+
// A real error settles derivatively-pending dependents (notifyStatus
|
|
1900
|
+
// cleared their pending sources), so stranded lazy ones release here —
|
|
1901
|
+
// the error twin of settlePendingSource's release (#2934).
|
|
1902
|
+
if (!stillPending) releaseSettledDependents(el);
|
|
1785
1903
|
};
|
|
1786
1904
|
const asyncWrite = (value, then) => {
|
|
1787
1905
|
if (el._inFlight !== result) return;
|
|
@@ -1789,7 +1907,7 @@ function handleAsync(el, result, setter) {
|
|
|
1789
1907
|
// skip this stale async result — the upcoming flush will recompute the node
|
|
1790
1908
|
// with the new value, creating a fresh Promise that supersedes this one.
|
|
1791
1909
|
if (el._flags & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return;
|
|
1792
|
-
|
|
1910
|
+
settleTransition();
|
|
1793
1911
|
const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
|
|
1794
1912
|
trimStaleDeps(el);
|
|
1795
1913
|
clearStatus(el);
|
|
@@ -1861,10 +1979,14 @@ function handleAsync(el, result, setter) {
|
|
|
1861
1979
|
// work (a lazy async memo would otherwise tear down and re-execute — one
|
|
1862
1980
|
// fetch per suspended re-read). Settling is that observer's release, so
|
|
1863
1981
|
// it runs the same last-one-out check the other release sites run.
|
|
1982
|
+
// Returns whether the node released, so the iterator branch can stop
|
|
1983
|
+
// pulling values instead of pumping an unobserved stream forever (#2935).
|
|
1864
1984
|
const settleAutodispose = () => {
|
|
1865
1985
|
if (el._config & CONFIG_AUTO_DISPOSE && !el._subs && !(el._statusFlags & STATUS_PENDING)) {
|
|
1866
1986
|
unobserved(el);
|
|
1987
|
+
return true;
|
|
1867
1988
|
}
|
|
1989
|
+
return false;
|
|
1868
1990
|
};
|
|
1869
1991
|
if (thenable) {
|
|
1870
1992
|
let resolved = false,
|
|
@@ -1916,6 +2038,12 @@ function handleAsync(el, result, setter) {
|
|
|
1916
2038
|
if (isThenable(returned)) returned.then(undefined, () => {});
|
|
1917
2039
|
} catch {}
|
|
1918
2040
|
});
|
|
2041
|
+
// Release check before each next pull: an unobserved lazy node must tear
|
|
2042
|
+
// down (its cleanup above closes the iterator) instead of pumping the
|
|
2043
|
+
// stream forever with zero subscribers (#2935).
|
|
2044
|
+
const iterateOrRelease = () => {
|
|
2045
|
+
if (!settleAutodispose()) iterate();
|
|
2046
|
+
};
|
|
1919
2047
|
const iterate = () => {
|
|
1920
2048
|
let syncResult,
|
|
1921
2049
|
syncError,
|
|
@@ -1932,7 +2060,7 @@ function handleAsync(el, result, setter) {
|
|
|
1932
2060
|
return;
|
|
1933
2061
|
} else if (!r.done) {
|
|
1934
2062
|
hadValue = true;
|
|
1935
|
-
asyncWrite(r.value,
|
|
2063
|
+
asyncWrite(r.value, iterateOrRelease);
|
|
1936
2064
|
} else {
|
|
1937
2065
|
completed = true;
|
|
1938
2066
|
if (hadValue) {
|
|
@@ -1942,6 +2070,7 @@ function handleAsync(el, result, setter) {
|
|
|
1942
2070
|
// Empty completion settles like the immediately-done sync path.
|
|
1943
2071
|
asyncWrite(undefined);
|
|
1944
2072
|
}
|
|
2073
|
+
settleAutodispose();
|
|
1945
2074
|
}
|
|
1946
2075
|
},
|
|
1947
2076
|
e => {
|
|
@@ -1951,6 +2080,7 @@ function handleAsync(el, result, setter) {
|
|
|
1951
2080
|
} else if (el._inFlight === result) {
|
|
1952
2081
|
completed = true;
|
|
1953
2082
|
handleError(e);
|
|
2083
|
+
settleAutodispose();
|
|
1954
2084
|
}
|
|
1955
2085
|
}
|
|
1956
2086
|
);
|
|
@@ -2169,6 +2299,10 @@ function recompute(el, create = false) {
|
|
|
2169
2299
|
let isOptimisticDirty = !!(el._flags & REACTIVE_OPTIMISTIC_DIRTY);
|
|
2170
2300
|
const hasOverride = el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING;
|
|
2171
2301
|
const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
|
|
2302
|
+
// Outgoing error, captured before the compute clears status: if this run
|
|
2303
|
+
// recovers to an unchanged value, dependents still holding this object must
|
|
2304
|
+
// be swept (settleErroredDependents, #2949).
|
|
2305
|
+
const outgoingError = el._statusFlags & STATUS_ERROR ? el._error : undefined;
|
|
2172
2306
|
// Re-ask classification lives in the verdict module; capture the flag before
|
|
2173
2307
|
// the recompute wipes _flags below.
|
|
2174
2308
|
const hadReask = (el._flags & REACTIVE_REASK) !== 0;
|
|
@@ -2351,6 +2485,13 @@ function recompute(el, create = false) {
|
|
|
2351
2485
|
insertIntoHeapHeight(s._sub, queueFor(s._sub));
|
|
2352
2486
|
}
|
|
2353
2487
|
}
|
|
2488
|
+
// Silent recovery: errored → unchanged value fires no notification, but
|
|
2489
|
+
// dependents still holding the propagated error consumed their dirty flag
|
|
2490
|
+
// in an errored run and may sit on stale commits (#2949). Changed-value
|
|
2491
|
+
// recoveries ride insertSubs above; a comparator throw re-errored the node
|
|
2492
|
+
// (el._error re-set), so this only runs on a genuinely clean recovery.
|
|
2493
|
+
if (outgoingError !== undefined && !valueChanged && !el._error)
|
|
2494
|
+
settleErroredDependents(el, outgoingError);
|
|
2354
2495
|
}
|
|
2355
2496
|
currentOptimisticLane = prevLane;
|
|
2356
2497
|
const needsPendingCommit =
|
|
@@ -2759,15 +2900,19 @@ function read(el) {
|
|
|
2759
2900
|
throw owner._error;
|
|
2760
2901
|
}
|
|
2761
2902
|
}
|
|
2762
|
-
|
|
2903
|
+
// `owner` is the computed itself, or the firewall behind a store node —
|
|
2904
|
+
// firewall-backed reads follow the same rules (memo parity, #2897 ruling):
|
|
2905
|
+
// an errored derive throws for every late reader instead of silently
|
|
2906
|
+
// serving node values (the seed, or last-good data after a failed refetch).
|
|
2907
|
+
if (owner._fn && owner._statusFlags & STATUS_ERROR) {
|
|
2763
2908
|
// Only a genuine reactive re-read may retry an errored async source:
|
|
2764
2909
|
// - tracking: owned/tracked scope only (never events / `untrack` / effect side-effect phase)
|
|
2765
2910
|
// - !pendingCheckActive: an `isPending` probe observes the error, never refetches
|
|
2766
|
-
// -
|
|
2767
|
-
if (tracking && !pendingCheckActive &&
|
|
2768
|
-
recompute(
|
|
2911
|
+
// - owner._time < clock: only on a later cycle than the one the error was found
|
|
2912
|
+
if (tracking && !pendingCheckActive && owner._time < clock) {
|
|
2913
|
+
recompute(owner);
|
|
2769
2914
|
return read(el);
|
|
2770
|
-
} else throw
|
|
2915
|
+
} else throw owner._error;
|
|
2771
2916
|
}
|
|
2772
2917
|
if (snapshotCaptureActive && c && c._config & CONFIG_IN_SNAPSHOT_SCOPE) {
|
|
2773
2918
|
const sv = el._snapshotValue;
|
|
@@ -4064,7 +4209,12 @@ function action(genFn) {
|
|
|
4064
4209
|
ctx = currentTransition(ctx);
|
|
4065
4210
|
const i = ctx._actions.indexOf(it);
|
|
4066
4211
|
if (i >= 0) ctx._actions.splice(i, 1);
|
|
4067
|
-
|
|
4212
|
+
// Re-adopt through initTransition like every other resumption site:
|
|
4213
|
+
// a bare setActiveTransition leaves globalQueue._batch as a detached
|
|
4214
|
+
// ambient batch, and anything registered before the scheduled flush
|
|
4215
|
+
// (held writes on a merging transition, optimistic overrides,
|
|
4216
|
+
// affects() marks) lands there with nothing to ever finalize it.
|
|
4217
|
+
globalQueue.initTransition(ctx);
|
|
4068
4218
|
schedule();
|
|
4069
4219
|
failed ? reject(e) : resolve(v);
|
|
4070
4220
|
};
|
|
@@ -4655,6 +4805,21 @@ function itemKey(item, keyFn) {
|
|
|
4655
4805
|
function keyedMatch(a, b, keyFn) {
|
|
4656
4806
|
return a === b || (isWrappable(a) && isWrappable(b) && keyFn(a) === keyFn(b));
|
|
4657
4807
|
}
|
|
4808
|
+
// A pair of array slots may only be merged into when both sides are real store
|
|
4809
|
+
// children of the SAME container kind. An array and an object are different
|
|
4810
|
+
// shapes, and merging one into the other leaves the slot's proxy permanently
|
|
4811
|
+
// mismatched with its value (an array target holding an object, so
|
|
4812
|
+
// `Array.isArray`/spread/`map` lie). The object diff has always applied this
|
|
4813
|
+
// rule; the array paths reach the same recursion through `keyedMatch` /
|
|
4814
|
+
// positional pairing, where two keyless wrappables "match" regardless of kind.
|
|
4815
|
+
function recursablePair(previous, next) {
|
|
4816
|
+
return (
|
|
4817
|
+
isWrappable(previous) &&
|
|
4818
|
+
isWrappable(next) &&
|
|
4819
|
+
!(rawValuesUsed && (isRawValue(previous) || isRawValue(next))) &&
|
|
4820
|
+
Array.isArray(previous) === Array.isArray(next)
|
|
4821
|
+
);
|
|
4822
|
+
}
|
|
4658
4823
|
// Array reconciliation updates the slots it visits, then swaps STORE_VALUE.
|
|
4659
4824
|
// Previously tracked keys that are absent from `next` still need invalidating,
|
|
4660
4825
|
// and `in` dependencies should follow the new value's membership. Use
|
|
@@ -4719,11 +4884,7 @@ function applyStateChild(next, prevRaw, target, keyFn) {
|
|
|
4719
4884
|
// Reconcile a single array slot: recurse into a wrappable pair, otherwise replace
|
|
4720
4885
|
// the node's value outright (covers object→primitive and primitive→object).
|
|
4721
4886
|
function applyArrayItem(next, previous, target, node, keyFn) {
|
|
4722
|
-
if (
|
|
4723
|
-
isWrappable(next) &&
|
|
4724
|
-
isWrappable(previous) &&
|
|
4725
|
-
!(rawValuesUsed && (isRawValue(previous) || isRawValue(next)))
|
|
4726
|
-
) {
|
|
4887
|
+
if (recursablePair(previous, next)) {
|
|
4727
4888
|
const wrapped = wrap(previous, target);
|
|
4728
4889
|
node && setSignal(node, wrapped);
|
|
4729
4890
|
applyState(next, wrapped, keyFn);
|
|
@@ -4917,7 +5078,7 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4917
5078
|
// the recursion dispatch entirely. Raw-marked values are leaves:
|
|
4918
5079
|
// replace the slot node instead of recursing.
|
|
4919
5080
|
if (item !== next[start]) {
|
|
4920
|
-
if (
|
|
5081
|
+
if (!recursablePair(item, next[start])) {
|
|
4921
5082
|
arrayNodes?.[start] && setSignal(arrayNodes[start], wrapValue(next[start], target));
|
|
4922
5083
|
} else applyStateChild(next[start], item, target, keyFn);
|
|
4923
5084
|
}
|
|
@@ -4979,11 +5140,7 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4979
5140
|
} else if (next.length) {
|
|
4980
5141
|
for (let i = 0, len = next.length; i < len; i++) {
|
|
4981
5142
|
const item = previous[i];
|
|
4982
|
-
if (
|
|
4983
|
-
isWrappable(item) &&
|
|
4984
|
-
isWrappable(next[i]) &&
|
|
4985
|
-
!(rawValuesUsed && (isRawValue(item) || isRawValue(next[i])))
|
|
4986
|
-
) {
|
|
5143
|
+
if (recursablePair(item, next[i])) {
|
|
4987
5144
|
if (item !== next[i]) applyStateChild(next[i], item, target, keyFn);
|
|
4988
5145
|
} else {
|
|
4989
5146
|
if (item !== next[i]) changed = true;
|
|
@@ -5093,8 +5250,8 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
5093
5250
|
);
|
|
5094
5251
|
start++
|
|
5095
5252
|
) {
|
|
5096
|
-
if (
|
|
5097
|
-
if (
|
|
5253
|
+
if (item !== next[start] && isWrappable(item) && isWrappable(next[start])) {
|
|
5254
|
+
if (!recursablePair(item, next[start])) {
|
|
5098
5255
|
nodes?.[start] && setSignal(nodes[start], wrapValue(next[start], target));
|
|
5099
5256
|
} else applyState(next[start], wrap(item, target), keyFn);
|
|
5100
5257
|
}
|
|
@@ -5156,11 +5313,7 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
5156
5313
|
} else if (next.length) {
|
|
5157
5314
|
for (let i = 0, len = next.length; i < len; i++) {
|
|
5158
5315
|
const item = getOverrideValue(previous, override, i, optOverride);
|
|
5159
|
-
if (
|
|
5160
|
-
isWrappable(item) &&
|
|
5161
|
-
isWrappable(next[i]) &&
|
|
5162
|
-
!(rawValuesUsed && (isRawValue(item) || isRawValue(next[i])))
|
|
5163
|
-
) {
|
|
5316
|
+
if (recursablePair(item, next[i])) {
|
|
5164
5317
|
if (item !== next[i]) applyState(next[i], wrap(item, target), keyFn);
|
|
5165
5318
|
} else {
|
|
5166
5319
|
if (item !== next[i]) changed = true;
|
|
@@ -5215,6 +5368,58 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
5215
5368
|
// No-key reconcile: every item reports "no key", which routes array diffs to
|
|
5216
5369
|
// the positional branch and object descent to plain per-property merging.
|
|
5217
5370
|
const NOKEY = () => null;
|
|
5371
|
+
// Identity-as-key: distinct objects never match, so every slot takes the diff's
|
|
5372
|
+
// "not the same entity" branch and is replaced by reference rather than merged
|
|
5373
|
+
// into. Slots holding the same raw on both sides still keep their proxy.
|
|
5374
|
+
const IDENTITY = item => item;
|
|
5375
|
+
/**
|
|
5376
|
+
* Shared body of `reconcile()` and the projection commit. `replace` is the
|
|
5377
|
+
* only difference: a projection commit is a value swap, not a merge — its root
|
|
5378
|
+
* proxy is a cell handed out by `createProjection` that can never change
|
|
5379
|
+
* reference, so a derive returning a different entity is not the slot mistake
|
|
5380
|
+
* `reconcile()` throws on. Nothing below the root survives that swap, which is
|
|
5381
|
+
* the rule the keyed diff already applies at a nested slot on a key mismatch.
|
|
5382
|
+
*
|
|
5383
|
+
* @internal
|
|
5384
|
+
*/
|
|
5385
|
+
function reconcileState(value, state, key, replace) {
|
|
5386
|
+
if (state == null) throw new Error("Cannot reconcile null or undefined state");
|
|
5387
|
+
// A projection commit whose derive returns a foreign store adopts it as the
|
|
5388
|
+
// live backing (store-in-store chain — the same shape as a store-proxy
|
|
5389
|
+
// seed): reads route through the inner store's own graph, so its updates
|
|
5390
|
+
// flow with no re-derive, and a derive with no dependencies never recomputes
|
|
5391
|
+
// (#2941). The diff below still runs against raw values so THIS store's
|
|
5392
|
+
// existing subscribers see the swap; the live proxy is installed after.
|
|
5393
|
+
// Shallow projections keep their raw-ingest contract and never chain.
|
|
5394
|
+
let chain;
|
|
5395
|
+
const target = replace ? state[$TARGET] : undefined;
|
|
5396
|
+
if (target !== undefined) {
|
|
5397
|
+
if (value?.[$TARGET] !== undefined && value[$TARGET] !== target && !target[STORE_SHALLOW]) {
|
|
5398
|
+
if (target[STORE_VALUE] === value) return; // already chained to this store
|
|
5399
|
+
chain = value;
|
|
5400
|
+
}
|
|
5401
|
+
// Re-diffing a previously chained backing goes through its raw — reads
|
|
5402
|
+
// off the outgoing proxy would subscribe this computed to a store it is
|
|
5403
|
+
// about to drop.
|
|
5404
|
+
while (target[STORE_VALUE]?.[$TARGET] !== undefined)
|
|
5405
|
+
target[STORE_VALUE] = unwrap(target[STORE_VALUE]);
|
|
5406
|
+
}
|
|
5407
|
+
if (key === null) applyState(value, state, NOKEY);
|
|
5408
|
+
else {
|
|
5409
|
+
let keyFn = typeof key === "string" ? item => item[key] : key;
|
|
5410
|
+
const eq = keyFn(state);
|
|
5411
|
+
if (eq !== undefined && keyFn(value) !== eq) {
|
|
5412
|
+
if (!replace) throw new Error("Cannot reconcile states with different identity");
|
|
5413
|
+
// Or the outgoing raw keeps resolving to this proxy and surfaces the
|
|
5414
|
+
// incoming entity wherever it reappears in this family.
|
|
5415
|
+
const t = state[$TARGET];
|
|
5416
|
+
if (t && t[STORE_VALUE] !== unwrap(value)) t[STORE_LOOKUP]?.delete(t[STORE_VALUE]);
|
|
5417
|
+
keyFn = IDENTITY;
|
|
5418
|
+
}
|
|
5419
|
+
applyState(value, state, keyFn);
|
|
5420
|
+
}
|
|
5421
|
+
if (chain !== undefined) target[STORE_VALUE] = chain;
|
|
5422
|
+
}
|
|
5218
5423
|
/**
|
|
5219
5424
|
* Returns a draft-mutating function that smart-merges `value` into a store,
|
|
5220
5425
|
* preserving fine-grained reactivity: only changed leaves trigger updates.
|
|
@@ -5229,6 +5434,9 @@ const NOKEY = () => null;
|
|
|
5229
5434
|
* the classic pattern for fixed-shape data that churns in place (dashboards,
|
|
5230
5435
|
* monitors), where no keyed diff pass is needed or wanted.
|
|
5231
5436
|
*
|
|
5437
|
+
* Merging into a slot that holds a *different* entity throws — the caller
|
|
5438
|
+
* picked the slot, so a key mismatch there is a bug.
|
|
5439
|
+
*
|
|
5232
5440
|
* @param value the next state to merge in
|
|
5233
5441
|
* @param key property name (string) or extractor function for stable
|
|
5234
5442
|
* identity (default `"id"`); pass `null` for positional merging
|
|
@@ -5247,18 +5455,7 @@ const NOKEY = () => null;
|
|
|
5247
5455
|
* ```
|
|
5248
5456
|
*/
|
|
5249
5457
|
function reconcile(value, key = "id") {
|
|
5250
|
-
return state =>
|
|
5251
|
-
if (state == null) throw new Error("Cannot reconcile null or undefined state");
|
|
5252
|
-
if (key === null) {
|
|
5253
|
-
applyState(value, state, NOKEY);
|
|
5254
|
-
return;
|
|
5255
|
-
}
|
|
5256
|
-
const keyFn = typeof key === "string" ? item => item[key] : key;
|
|
5257
|
-
const eq = keyFn(state);
|
|
5258
|
-
if (eq !== undefined && keyFn(value) !== eq)
|
|
5259
|
-
throw new Error("Cannot reconcile states with different identity");
|
|
5260
|
-
applyState(value, state, keyFn);
|
|
5261
|
-
};
|
|
5458
|
+
return state => reconcileState(value, state, key, false);
|
|
5262
5459
|
}
|
|
5263
5460
|
|
|
5264
5461
|
function createProjectionInternal(fn, seed, options) {
|
|
@@ -5292,7 +5489,7 @@ function createProjectionInternal(fn, seed, options) {
|
|
|
5292
5489
|
node = computed(
|
|
5293
5490
|
() => {
|
|
5294
5491
|
if (!node) node = getOwner();
|
|
5295
|
-
runProjectionComputed(wrappedStore, fn, options?.key
|
|
5492
|
+
runProjectionComputed(wrappedStore, fn, options?.key === undefined ? "id" : options.key);
|
|
5296
5493
|
},
|
|
5297
5494
|
options?.name ? { name: options.name } : undefined
|
|
5298
5495
|
);
|
|
@@ -5307,6 +5504,10 @@ function createProjectionInternal(fn, seed, options) {
|
|
|
5307
5504
|
* items keep their proxy identity — only added/removed items are
|
|
5308
5505
|
* created/disposed.
|
|
5309
5506
|
*
|
|
5507
|
+
* If the derive returns a different entity than the one currently held (the
|
|
5508
|
+
* `/users/1` → `/users/2` shape), the store swaps to it rather than merging,
|
|
5509
|
+
* and nothing below it is treated as surviving.
|
|
5510
|
+
*
|
|
5310
5511
|
* Returns the projected store directly (no setter — reads only).
|
|
5311
5512
|
*
|
|
5312
5513
|
* Use this when you want the structural-sharing / per-property tracking
|
|
@@ -5319,7 +5520,8 @@ function createProjectionInternal(fn, seed, options) {
|
|
|
5319
5520
|
* @param seed the backing store value to wrap and reconcile into
|
|
5320
5521
|
* @param options `ProjectionOptions` — `name`, `key`. `key` defaults to
|
|
5321
5522
|
* `"id"`; specify it only when your data uses a different identity field
|
|
5322
|
-
* (e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`)
|
|
5523
|
+
* (e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`), or `null` to merge
|
|
5524
|
+
* positionally with no keyed pass.
|
|
5323
5525
|
*
|
|
5324
5526
|
* @example
|
|
5325
5527
|
* ```ts
|
|
@@ -5371,7 +5573,7 @@ function runProjectionComputed(wrappedStore, fn, key, wrapCommit, onDraftWrite)
|
|
|
5371
5573
|
settled = true;
|
|
5372
5574
|
const commit = v => {
|
|
5373
5575
|
if (v === s || v === undefined) return;
|
|
5374
|
-
const write = () => storeSetter(wrappedStore,
|
|
5576
|
+
const write = () => storeSetter(wrappedStore, s => reconcileState(v, s, key, true));
|
|
5375
5577
|
wrapCommit ? wrapCommit(write) : write();
|
|
5376
5578
|
};
|
|
5377
5579
|
commit(handleAsync(owner, result, commit));
|
|
@@ -5543,6 +5745,14 @@ function isRawValue(value) {
|
|
|
5543
5745
|
}
|
|
5544
5746
|
function markRawOne(v) {
|
|
5545
5747
|
if (isWrappable(v)) {
|
|
5748
|
+
// A store proxy is already tracked elsewhere: the shallow boundary passes
|
|
5749
|
+
// it through by reference (replaced, never edited — same slot semantics
|
|
5750
|
+
// as a raw) instead of claiming it raw. The sticky mark is global, so
|
|
5751
|
+
// marking a live proxy would make wrap() serve it verbatim through every
|
|
5752
|
+
// OTHER store too — downstream deep stores then captured it instead of
|
|
5753
|
+
// wrapping it in their own family, and their writes landed in the
|
|
5754
|
+
// upstream store's override layer (#2932).
|
|
5755
|
+
if (v[$TARGET] !== undefined) return;
|
|
5546
5756
|
if (storeLookup.has(v))
|
|
5547
5757
|
throw new Error(
|
|
5548
5758
|
"shallow store: an ingested record is already tracked as a deep store — one value cannot present both wrapped and raw"
|
|
@@ -5596,11 +5806,38 @@ function wrapShallow(value) {
|
|
|
5596
5806
|
markRawIngest(value);
|
|
5597
5807
|
return p;
|
|
5598
5808
|
}
|
|
5809
|
+
const OBJECT_PROTO = Object.prototype;
|
|
5810
|
+
// Per-prototype memo for the custom-proto branch of isWrappable: the verdict
|
|
5811
|
+
// is fully determined by the prototype (tag and Node lineage both live on
|
|
5812
|
+
// the chain), so each class pays the tag call once — not per read.
|
|
5813
|
+
const wrappableProtos = new WeakMap();
|
|
5599
5814
|
function isWrappable(obj) {
|
|
5600
5815
|
if (obj == null || typeof obj !== "object" || Object.isFrozen(obj)) return false;
|
|
5601
|
-
//
|
|
5602
|
-
//
|
|
5603
|
-
|
|
5816
|
+
// Plain data and user class instances wrap; platform objects never do
|
|
5817
|
+
// (#2952). Native code brand-checks internal slots and throws through a
|
|
5818
|
+
// proxy (`Map.prototype.size`, `Date.prototype.getTime`, ...), so
|
|
5819
|
+
// collections and other built-ins can't honestly be stores — they get the
|
|
5820
|
+
// markRaw-children contract automatically: served raw, mutations land raw,
|
|
5821
|
+
// the property holding them still tracks (reassignment notifies). The tag
|
|
5822
|
+
// check separates them structurally: user classes stringify as
|
|
5823
|
+
// `[object Object]` while every native/host object carries its own brand
|
|
5824
|
+
// (`[object Map]`, `[object Date]`, `[object Headers]`, ...), including
|
|
5825
|
+
// subclasses, which inherit the tag. getPrototypeOf keeps the hot path
|
|
5826
|
+
// (plain and null-proto objects) intrinsic-only — no property lookup.
|
|
5827
|
+
const proto = Object.getPrototypeOf(obj);
|
|
5828
|
+
if (proto === OBJECT_PROTO || proto === null) return true;
|
|
5829
|
+
if (Array.isArray(obj)) return true;
|
|
5830
|
+
let wrappable = wrappableProtos.get(proto);
|
|
5831
|
+
if (wrappable === undefined) {
|
|
5832
|
+
wrappable =
|
|
5833
|
+
Object.prototype.toString.call(obj) === "[object Object]" &&
|
|
5834
|
+
// Dynamic Node check (kept dynamic so test/SSR overrides of
|
|
5835
|
+
// `globalThis.Node` are observed at call time): shimmed DOMs implement
|
|
5836
|
+
// nodes as plain user classes, which pass the tag check.
|
|
5837
|
+
(typeof Node === "undefined" || !(obj instanceof Node));
|
|
5838
|
+
wrappableProtos.set(proto, wrappable);
|
|
5839
|
+
}
|
|
5840
|
+
return wrappable;
|
|
5604
5841
|
}
|
|
5605
5842
|
let writeOverride = false;
|
|
5606
5843
|
function setWriteOverride(value) {
|
|
@@ -6072,8 +6309,11 @@ function armOptimisticStoreWrite(target, store) {
|
|
|
6072
6309
|
* concurrent actions writing disjoint keys must revert independently, exactly
|
|
6073
6310
|
* like optimistic signal nodes do via the transition's _optimisticNodes.
|
|
6074
6311
|
* `activeTransition` is the write's transaction (action() opens it before the
|
|
6075
|
-
* body runs); null marks an ambient write
|
|
6076
|
-
*
|
|
6312
|
+
* body runs); null marks an ambient write, which clears at plain flush end —
|
|
6313
|
+
* unless its flush's transition is blocked on the store's own in-flight truth
|
|
6314
|
+
* (pending firewall, #2951), in which case it rides that transaction to
|
|
6315
|
+
* settle. Same-key writes across actions keep last-write-wins layer
|
|
6316
|
+
* semantics.
|
|
6077
6317
|
*/
|
|
6078
6318
|
function stampOptimisticOwner(target, overrideKey, property) {
|
|
6079
6319
|
if (overrideKey === STORE_OPTIMISTIC_OVERRIDE)
|
|
@@ -6124,17 +6364,37 @@ function notifyStoreProperty(target, property, mode, value, prev, prevHas) {
|
|
|
6124
6364
|
}
|
|
6125
6365
|
let Writing = null;
|
|
6126
6366
|
/**
|
|
6127
|
-
* A derived store
|
|
6128
|
-
*
|
|
6129
|
-
*
|
|
6130
|
-
*
|
|
6131
|
-
*
|
|
6132
|
-
*
|
|
6133
|
-
*
|
|
6367
|
+
* A derived store follows async memo rules (#2897 ruling): its seed is a
|
|
6368
|
+
* draft for the derive function, never an observable value, and an errored
|
|
6369
|
+
* derive is an error state, never a silent stale/seed serve. Until the
|
|
6370
|
+
* firewall first resolves there is nothing to read, so every consumer path
|
|
6371
|
+
* throws NotReady — tracked reads through their node (core read()), and the
|
|
6372
|
+
* untracked fall-throughs in the traps through this guard. Returning the
|
|
6373
|
+
* seed leaked it; returning `undefined` would break non-nullable types.
|
|
6374
|
+
* Callers exempt the firewall itself (the derive function works its own
|
|
6375
|
+
* draft while uninitialized).
|
|
6376
|
+
*
|
|
6377
|
+
* Error rail: a firewall carrying STATUS_ERROR throws its error for every
|
|
6378
|
+
* late reader — memo parity, where read()'s error branch does the same for
|
|
6379
|
+
* plain computeds. Rejection clears STATUS_UNINITIALIZED at commit, so
|
|
6380
|
+
* without this check late readers silently got the seed while settle-time
|
|
6381
|
+
* subscribers saw the error.
|
|
6382
|
+
*
|
|
6383
|
+
* Loading rail: the veto requires the firewall to still be in flight, not
|
|
6384
|
+
* just flagged: STATUS_UNINITIALIZED's clear is deferred to batch commit,
|
|
6385
|
+
* so during the settle flush a firewall that has already recomputed — and
|
|
6386
|
+
* reconciled real values into STORE_VALUE — still carries the stale flag.
|
|
6387
|
+
* STATUS_PENDING is the live bit (it clears eagerly at settle, mirroring
|
|
6388
|
+
* core read()'s verdict), so gating on it stops the guard from throwing a
|
|
6389
|
+
* fresh NotReadyError that nothing would ever sweep. #2944: mapArray's
|
|
6390
|
+
* keyed diff reads items inside its internal owner (untracked by design)
|
|
6391
|
+
* in exactly this window, and the stale throw wedged <For> permanently.
|
|
6134
6392
|
*/
|
|
6135
|
-
function
|
|
6393
|
+
function throwIfUnreadable(target) {
|
|
6136
6394
|
const firewall = target[STORE_FIREWALL];
|
|
6137
|
-
if (firewall
|
|
6395
|
+
if (!firewall) return;
|
|
6396
|
+
const flags = firewall._statusFlags;
|
|
6397
|
+
if (flags & STATUS_ERROR || (flags & STATUS_UNINITIALIZED && flags & STATUS_PENDING))
|
|
6138
6398
|
throw firewall._error ?? new NotReadyError(firewall);
|
|
6139
6399
|
}
|
|
6140
6400
|
const storeTraps = {
|
|
@@ -6281,7 +6541,14 @@ const storeTraps = {
|
|
|
6281
6541
|
}
|
|
6282
6542
|
// Untracked fall-through (tracked reads already threw via their node in
|
|
6283
6543
|
// read(); the dev strictRead error above wins first for memo parity).
|
|
6284
|
-
|
|
6544
|
+
// Observer-present reads must NOT re-consult the flag here: during the
|
|
6545
|
+
// settle flush the firewall has recomputed (first values live on the
|
|
6546
|
+
// pending rail, served by read() above) but its UNINITIALIZED clear is
|
|
6547
|
+
// deferred to batch commit — vetoing read()'s verdict with the stale flag
|
|
6548
|
+
// threw a fresh NotReadyError for an already-settled source, which no
|
|
6549
|
+
// sweep would ever release (#2938: projection over an async store wedged
|
|
6550
|
+
// its Loading boundary on `undefined`).
|
|
6551
|
+
if (!selfRead && !getObserver()) throwIfUnreadable(target);
|
|
6285
6552
|
return isWrappable(value) ? wrap(value, target) : value;
|
|
6286
6553
|
},
|
|
6287
6554
|
has(target, property) {
|
|
@@ -6301,7 +6568,7 @@ const storeTraps = {
|
|
|
6301
6568
|
if (getObserver()) {
|
|
6302
6569
|
return read(getNode(target, nodes, property, has));
|
|
6303
6570
|
}
|
|
6304
|
-
|
|
6571
|
+
throwIfUnreadable(target);
|
|
6305
6572
|
return has;
|
|
6306
6573
|
},
|
|
6307
6574
|
set(target, property, rawValue) {
|
|
@@ -6315,8 +6582,19 @@ const storeTraps = {
|
|
|
6315
6582
|
const prevHas = prevLayer
|
|
6316
6583
|
? prevLayer[property] !== $DELETED
|
|
6317
6584
|
: property in target[STORE_VALUE];
|
|
6318
|
-
|
|
6319
|
-
|
|
6585
|
+
// Shallow slots hold store proxies verbatim (pass-through reference,
|
|
6586
|
+
// never raw-marked — see markRawOne/#2932); everything else unwraps
|
|
6587
|
+
// and marks as usual.
|
|
6588
|
+
const passThrough = !!target[STORE_SHALLOW] && rawValue?.[$TARGET] !== undefined;
|
|
6589
|
+
const value = passThrough ? rawValue : unwrapStoreValue(rawValue);
|
|
6590
|
+
if (target[STORE_SHALLOW] && !passThrough && isWrappable(value)) {
|
|
6591
|
+
// Flip the live gate too: a bare add was inert unless something else
|
|
6592
|
+
// had already marked a raw somewhere (wrap() checks rawValuesUsed
|
|
6593
|
+
// first), so the documented set-trap ingest mark silently no-oped in
|
|
6594
|
+
// apps whose only shallow data arrived through writes.
|
|
6595
|
+
rawValuesUsed = true;
|
|
6596
|
+
rawValues.add(value);
|
|
6597
|
+
}
|
|
6320
6598
|
// Symbol-keyed writes on arrays are metadata, not index writes — never run
|
|
6321
6599
|
// them through the numeric index/length machinery (`parseInt` on a symbol
|
|
6322
6600
|
// throws). #2769
|
|
@@ -6457,7 +6735,7 @@ const storeTraps = {
|
|
|
6457
6735
|
// path is exempt (like the get/has traps' writeOnly early returns):
|
|
6458
6736
|
// the first landing's reconcile enumerates the store while
|
|
6459
6737
|
// STATUS_UNINITIALIZED is still set — it IS the initialization.
|
|
6460
|
-
if (!getObserver() && !writeOnly(target[$PROXY]))
|
|
6738
|
+
if (!getObserver() && !writeOnly(target[$PROXY])) throwIfUnreadable(target);
|
|
6461
6739
|
}
|
|
6462
6740
|
// Merge optimistic override with regular override for key enumeration
|
|
6463
6741
|
let keys = getKeys(target[STORE_VALUE], target[STORE_OVERRIDE], false);
|
|
@@ -6699,7 +6977,26 @@ function createOptimisticStore(first, second, options) {
|
|
|
6699
6977
|
// STORE_OPTIMISTIC take the engine's write path, so install it before any
|
|
6700
6978
|
// node can be created.
|
|
6701
6979
|
installOptimisticEngine();
|
|
6702
|
-
GlobalQueue._clearOptimisticStores
|
|
6980
|
+
if (!GlobalQueue._clearOptimisticStores) {
|
|
6981
|
+
GlobalQueue._clearOptimisticStores = clearOptimisticStores;
|
|
6982
|
+
// Store half of the engine's override blockage (#2951): signal-form
|
|
6983
|
+
// createOptimistic carries the pending async and the override on ONE node,
|
|
6984
|
+
// so transitionBlocked sees both; a derived optimistic STORE splits them —
|
|
6985
|
+
// the layer sits on store targets while the in-flight truth lives on the
|
|
6986
|
+
// firewall computed. Without this, the transition adopting a bare store
|
|
6987
|
+
// write settled in the same flush that started the refetch and its settle
|
|
6988
|
+
// consumed the layer mid-flight (follow-up writes then drafted from base,
|
|
6989
|
+
// clobbering instead of composing). Optimistic state clears when truth
|
|
6990
|
+
// lands or its transaction ends — never mid-refetch. Wrapped here (engine
|
|
6991
|
+
// is already installed above) so store-free apps never carry the check.
|
|
6992
|
+
const engineBlocked = GlobalQueue._transitionBlocked;
|
|
6993
|
+
GlobalQueue._transitionBlocked = transition => {
|
|
6994
|
+
for (const store of transition._optimisticStores) {
|
|
6995
|
+
if ((store[$TARGET]?.[STORE_FIREWALL]?._statusFlags ?? 0) & STATUS_PENDING) return true;
|
|
6996
|
+
}
|
|
6997
|
+
return engineBlocked(transition);
|
|
6998
|
+
};
|
|
6999
|
+
}
|
|
6703
7000
|
const derived = typeof first === "function";
|
|
6704
7001
|
// Plain form: the second slot carries options.
|
|
6705
7002
|
if (!derived && options === undefined) options = second;
|
|
@@ -6856,7 +7153,7 @@ function createOptimisticProjectionInternal(fn, initialValue, options) {
|
|
|
6856
7153
|
runProjectionComputed(
|
|
6857
7154
|
wrappedStore,
|
|
6858
7155
|
fn,
|
|
6859
|
-
options?.key
|
|
7156
|
+
options?.key === undefined ? "id" : options.key,
|
|
6860
7157
|
wrapCommit,
|
|
6861
7158
|
clearProjectionOverride
|
|
6862
7159
|
);
|