@solidjs/signals 2.0.0-beta.21 → 2.0.0-beta.23
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 +210 -301
- package/dist/node.cjs +1106 -1196
- package/dist/prod/affects.js +71 -163
- package/dist/prod/boundaries.js +141 -137
- package/dist/prod/core/action.js +3 -3
- package/dist/prod/core/async.js +103 -103
- package/dist/prod/core/core.js +254 -297
- package/dist/prod/core/effect.js +46 -46
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +60 -47
- package/dist/prod/core/heap.js +50 -50
- package/dist/prod/core/lanes.js +34 -34
- package/dist/prod/core/optimistic.js +64 -106
- package/dist/prod/core/owner.js +52 -52
- package/dist/prod/core/scheduler.js +210 -211
- package/dist/prod/core/verdict.js +129 -78
- package/dist/prod/map.js +101 -101
- package/dist/prod/signals.js +23 -5
- package/dist/prod/store/optimistic.js +11 -11
- package/dist/prod/store/projection.js +2 -2
- package/dist/prod/store/store.js +14 -14
- package/dist/types/affects.d.ts +9 -9
- package/dist/types/core/async.d.ts +1 -1
- package/dist/types/core/error.d.ts +7 -0
- package/dist/types/core/scheduler.d.ts +1 -6
- package/dist/types/core/types.d.ts +6 -11
- package/dist/types-cjs/affects.d.cts +9 -9
- package/dist/types-cjs/core/async.d.cts +1 -1
- package/dist/types-cjs/core/error.d.cts +7 -0
- package/dist/types-cjs/core/scheduler.d.cts +1 -6
- package/dist/types-cjs/core/types.d.cts +6 -11
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -669,15 +669,12 @@ class GlobalQueue extends Queue {
|
|
|
669
669
|
// _clearOptimisticStore).
|
|
670
670
|
static _releaseAffectsScope = null;
|
|
671
671
|
// affects()-side hooks (wired by affects.ts, mirroring _update): the mark
|
|
672
|
-
// engine — count/register/release
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
static _applyAffectsReads = null;
|
|
672
|
+
// engine — count/register/release — lives with the feature. Every call site
|
|
673
|
+
// is gated by state only that module creates, so `!` invocations are safe
|
|
674
|
+
// once the gate holds.
|
|
676
675
|
static _releaseAffectsMarks = null;
|
|
677
676
|
static _markAffects = null;
|
|
678
677
|
static _releaseAffectsMark = null;
|
|
679
|
-
static _onlyMarkPending = null;
|
|
680
|
-
static _collectMarkSources = null;
|
|
681
678
|
// External-source bridge (wired by enableExternalSource(); null while no
|
|
682
679
|
// config is active — including after _resetExternalSourceConfig()).
|
|
683
680
|
static _wireExternalSource = null;
|
|
@@ -707,11 +704,9 @@ class GlobalQueue extends Queue {
|
|
|
707
704
|
// once the gate holds.
|
|
708
705
|
static _optimisticWrite = null;
|
|
709
706
|
static _resolveOptimistic = null;
|
|
710
|
-
static _stashOptimistic = null;
|
|
711
707
|
static _transitionBlocked = null;
|
|
712
708
|
static _cleanupLanes = null;
|
|
713
709
|
static _runLaneEffects = null;
|
|
714
|
-
static _readStashed = null;
|
|
715
710
|
static _gatedRead = null;
|
|
716
711
|
static _laneSuspends = null;
|
|
717
712
|
static _laneReadsCommitted = null;
|
|
@@ -750,18 +745,7 @@ class GlobalQueue extends Queue {
|
|
|
750
745
|
scheduled = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
|
|
751
746
|
reassignPendingTransition(stashedTransition._pendingNodes);
|
|
752
747
|
activeTransition = null;
|
|
753
|
-
|
|
754
|
-
// wraps finalizePureQueue in the engine; a non-empty _optimisticNodes
|
|
755
|
-
// means _optimisticWrite ran, which installed the hook.
|
|
756
|
-
if (
|
|
757
|
-
!stashedTransition._actions.length &&
|
|
758
|
-
!stashedTransition._asyncReporters.size &&
|
|
759
|
-
stashedTransition._optimisticNodes.length
|
|
760
|
-
) {
|
|
761
|
-
GlobalQueue._stashOptimistic(stashedTransition);
|
|
762
|
-
} else {
|
|
763
|
-
finalizePureQueue(null, true);
|
|
764
|
-
}
|
|
748
|
+
finalizePureQueue(null, true);
|
|
765
749
|
return;
|
|
766
750
|
}
|
|
767
751
|
const completingTransition = activeTransition;
|
|
@@ -833,6 +817,11 @@ class GlobalQueue extends Queue {
|
|
|
833
817
|
if (mask & STATUS_PENDING) {
|
|
834
818
|
if (flags & STATUS_PENDING) {
|
|
835
819
|
const actualError = error !== undefined ? error : node._error;
|
|
820
|
+
// A visibility-only mark notification (the affects() boundary
|
|
821
|
+
// channel) updates display state on its way up but must be invisible
|
|
822
|
+
// to completion accounting BY CONSTRUCTION: it never registers a
|
|
823
|
+
// reporter and never counts toward the loading-boundary diagnostic.
|
|
824
|
+
if (actualError?._markVisual) return true;
|
|
836
825
|
if (activeTransition && actualError) {
|
|
837
826
|
const source = actualError.source;
|
|
838
827
|
let reporters = activeTransition._asyncReporters.get(source);
|
|
@@ -979,8 +968,14 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
979
968
|
}
|
|
980
969
|
// Declared motion ends with the transaction: settle (or plain flush end
|
|
981
970
|
// for ambient marks) releases each registration's refcount. A non-empty
|
|
982
|
-
// batch means registerAffectsMark ran, which installed the hook.
|
|
983
|
-
|
|
971
|
+
// batch means registerAffectsMark ran, which installed the hook. Marks
|
|
972
|
+
// held boundary display state through the visual channel, and their
|
|
973
|
+
// release is the display-state update point — re-run the boundary sweep
|
|
974
|
+
// (the earlier sweep above ran while the marks were still live).
|
|
975
|
+
if (batch._affectsNodes.length) {
|
|
976
|
+
GlobalQueue._releaseAffectsMarks(batch._affectsNodes);
|
|
977
|
+
if (globalQueue._children.length) checkBoundaryChildren(globalQueue);
|
|
978
|
+
}
|
|
984
979
|
// A non-empty set means trackOptimisticStore ran, which installed the
|
|
985
980
|
// hook; the hook iterates, clears, and schedules (keeping the loop out of
|
|
986
981
|
// core lets esbuild shake it — rollup already folds the null guard). The
|
|
@@ -1574,8 +1569,17 @@ function unlinkSubs(link) {
|
|
|
1574
1569
|
if (nextSub === null) {
|
|
1575
1570
|
dep._unobserved?.();
|
|
1576
1571
|
// No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
|
|
1572
|
+
// A pending node is exempt: its in-flight async work (or the
|
|
1573
|
+
// transition holding it) is an observer — tearing down would orphan
|
|
1574
|
+
// the work and re-execute it on the next read. The settle path runs
|
|
1575
|
+
// this same last-one-out check when that observer releases (the
|
|
1576
|
+
// untracked-read dispose in core.ts guards on pending identically).
|
|
1577
1577
|
const c = dep;
|
|
1578
|
-
c._fn &&
|
|
1578
|
+
c._fn &&
|
|
1579
|
+
c._config & CONFIG_AUTO_DISPOSE &&
|
|
1580
|
+
!(c._flags & REACTIVE_ZOMBIE) &&
|
|
1581
|
+
!(c._statusFlags & STATUS_PENDING) &&
|
|
1582
|
+
unobserved(c);
|
|
1579
1583
|
}
|
|
1580
1584
|
}
|
|
1581
1585
|
return nextDep;
|
|
@@ -1603,9 +1607,14 @@ function unobserved(el) {
|
|
|
1603
1607
|
}
|
|
1604
1608
|
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
|
|
1605
1609
|
function link(dep, sub, pendingObserver = false) {
|
|
1610
|
+
// Repeat touches within one pass AND-combine `_pendingObserver`: a probe
|
|
1611
|
+
// read (`isPending(() => x())`) beside a value read of the same dep must
|
|
1612
|
+
// not relabel the value dependency as probe-only — the value read is what
|
|
1613
|
+
// real-error propagation and affects() coverage key off, regardless of
|
|
1614
|
+
// read order within the computation.
|
|
1606
1615
|
const prevDep = sub._depsTail;
|
|
1607
1616
|
if (prevDep !== null && prevDep._dep === dep) {
|
|
1608
|
-
prevDep._pendingObserver
|
|
1617
|
+
prevDep._pendingObserver &&= pendingObserver;
|
|
1609
1618
|
return;
|
|
1610
1619
|
}
|
|
1611
1620
|
let nextDep = null;
|
|
@@ -1615,6 +1624,7 @@ function link(dep, sub, pendingObserver = false) {
|
|
|
1615
1624
|
if (nextDep !== null && nextDep._dep === dep) {
|
|
1616
1625
|
nextDep._gen = sub._depGen;
|
|
1617
1626
|
sub._depsTail = nextDep;
|
|
1627
|
+
// First touch of this pass: the previous pass's label is stale.
|
|
1618
1628
|
nextDep._pendingObserver = pendingObserver;
|
|
1619
1629
|
return;
|
|
1620
1630
|
}
|
|
@@ -1630,7 +1640,10 @@ function link(dep, sub, pendingObserver = false) {
|
|
|
1630
1640
|
prevSub._sub === sub &&
|
|
1631
1641
|
(!isRecomputing || prevSub._gen === sub._depGen)
|
|
1632
1642
|
) {
|
|
1633
|
-
|
|
1643
|
+
// Gen-matched during a recompute = repeat touch this pass (AND); outside
|
|
1644
|
+
// a recompute there is no pass boundary, so the latest read labels it.
|
|
1645
|
+
if (isRecomputing) prevSub._pendingObserver &&= pendingObserver;
|
|
1646
|
+
else prevSub._pendingObserver = pendingObserver;
|
|
1634
1647
|
return;
|
|
1635
1648
|
}
|
|
1636
1649
|
const newLink =
|
|
@@ -1694,20 +1707,13 @@ function forEachDependent(el, fn) {
|
|
|
1694
1707
|
// Queue a node to re-run on the next flush (used both when a pending source
|
|
1695
1708
|
// settles and when an `isPending` observer must re-evaluate after a real error):
|
|
1696
1709
|
// shared scheduling helper in heap.ts (tracked effects bypass the heap).
|
|
1697
|
-
function settlePendingSource(
|
|
1698
|
-
el,
|
|
1699
|
-
source = el,
|
|
1700
|
-
// Mark release runs inside queue finalization: companion writes must go
|
|
1701
|
-
// through the settlement snap (committed), because a setSignal here would
|
|
1702
|
-
// open a fresh transition-scoped override window that nothing reverts.
|
|
1703
|
-
snap = false
|
|
1704
|
-
) {
|
|
1710
|
+
function settlePendingSource(el) {
|
|
1705
1711
|
let scheduled = false;
|
|
1706
1712
|
const visited = new Set();
|
|
1707
|
-
// Companion updates no-op without the verdict layer (null
|
|
1708
|
-
const updateCompanions =
|
|
1713
|
+
// Companion updates no-op without the verdict layer (null hook).
|
|
1714
|
+
const updateCompanions = GlobalQueue._updatePendingSignal;
|
|
1709
1715
|
const settle = node => {
|
|
1710
|
-
if (visited.has(node) || !removePendingSource(node,
|
|
1716
|
+
if (visited.has(node) || !removePendingSource(node, el)) return;
|
|
1711
1717
|
visited.add(node);
|
|
1712
1718
|
node._time = clock;
|
|
1713
1719
|
const remaining = node._pendingSources?.values().next().value;
|
|
@@ -1850,6 +1856,16 @@ function handleAsync(el, result, setter) {
|
|
|
1850
1856
|
flush();
|
|
1851
1857
|
then?.();
|
|
1852
1858
|
};
|
|
1859
|
+
// A pending node's in-flight promise is an observer: `unlinkSubs` skips
|
|
1860
|
+
// autodispose while STATUS_PENDING so subscriber churn can't orphan the
|
|
1861
|
+
// work (a lazy async memo would otherwise tear down and re-execute — one
|
|
1862
|
+
// fetch per suspended re-read). Settling is that observer's release, so
|
|
1863
|
+
// it runs the same last-one-out check the other release sites run.
|
|
1864
|
+
const settleAutodispose = () => {
|
|
1865
|
+
if (el._config & CONFIG_AUTO_DISPOSE && !el._subs && !(el._statusFlags & STATUS_PENDING)) {
|
|
1866
|
+
unobserved(el);
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1853
1869
|
if (thenable) {
|
|
1854
1870
|
let resolved = false,
|
|
1855
1871
|
rejected = false,
|
|
@@ -1860,13 +1876,19 @@ function handleAsync(el, result, setter) {
|
|
|
1860
1876
|
if (isSync) {
|
|
1861
1877
|
syncValue = v;
|
|
1862
1878
|
resolved = true;
|
|
1863
|
-
} else
|
|
1879
|
+
} else {
|
|
1880
|
+
asyncWrite(v);
|
|
1881
|
+
settleAutodispose();
|
|
1882
|
+
}
|
|
1864
1883
|
},
|
|
1865
1884
|
e => {
|
|
1866
1885
|
if (isSync) {
|
|
1867
1886
|
syncError = e;
|
|
1868
1887
|
rejected = true;
|
|
1869
|
-
} else
|
|
1888
|
+
} else {
|
|
1889
|
+
handleError(e);
|
|
1890
|
+
settleAutodispose();
|
|
1891
|
+
}
|
|
1870
1892
|
}
|
|
1871
1893
|
);
|
|
1872
1894
|
isSync = false;
|
|
@@ -1983,18 +2005,6 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
1983
2005
|
error = new StatusError(el, error);
|
|
1984
2006
|
const pendingSource =
|
|
1985
2007
|
status === STATUS_PENDING && error instanceof NotReadyError ? error.source : undefined;
|
|
1986
|
-
// Mark-sourced propagation must not capture subscribers into the marking
|
|
1987
|
-
// action's transaction (#2893): they carry no held value needing a
|
|
1988
|
-
// transition-scheduled commit, and stamping `_transition` on them would
|
|
1989
|
-
// freeze unrelated writes that share a downstream memo until the action
|
|
1990
|
-
// settles. Real async keeps queuing (its commits ride the transition).
|
|
1991
|
-
const markSourced = pendingSource?._affectsFor !== undefined;
|
|
1992
|
-
// A real error is a settled verdict a mark must not erase (#2893): landing
|
|
1993
|
-
// STATUS_PENDING here would clobber `_error` with the sentinel's
|
|
1994
|
-
// NotReadyError, and unlike real async there is no arriving value whose
|
|
1995
|
-
// recompute would surface the error again. Descent stops too — everything
|
|
1996
|
-
// downstream holds the propagated error for the same reason.
|
|
1997
|
-
if (markSourced && el._statusFlags & STATUS_ERROR) return;
|
|
1998
2008
|
const isSource = pendingSource === el;
|
|
1999
2009
|
const isOptimisticBoundary =
|
|
2000
2010
|
status === STATUS_PENDING && el._overrideValue !== undefined && !isSource;
|
|
@@ -2048,7 +2058,7 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
2048
2058
|
schedule();
|
|
2049
2059
|
return;
|
|
2050
2060
|
}
|
|
2051
|
-
if (!downstreamBlockStatus && !
|
|
2061
|
+
if (!downstreamBlockStatus && !sub._transition) queuePendingNode(sub);
|
|
2052
2062
|
notifyStatus(sub, status, error, downstreamBlockStatus, downstreamLane);
|
|
2053
2063
|
}
|
|
2054
2064
|
});
|
|
@@ -2082,17 +2092,6 @@ let pendingCheckActive = false;
|
|
|
2082
2092
|
let latestReadActive = false;
|
|
2083
2093
|
let context = null;
|
|
2084
2094
|
let currentOptimisticLane = null;
|
|
2085
|
-
/**
|
|
2086
|
-
* Marked sources read by the recompute currently on the stack (saved/restored
|
|
2087
|
-
* per recompute, like `context`). A computed that reads a node with a live
|
|
2088
|
-
* `affects()` mark inherits the mark's pendingness — `recompute` applies the
|
|
2089
|
-
* collected sources through `applyAffectsReads` after its commit, because the
|
|
2090
|
-
* `clearStatus` at the top of the commit path wipes whatever sentinel entries
|
|
2091
|
-
* the node held from the registration-time push. This is the pull half of
|
|
2092
|
-
* mark propagation (real async re-establishes itself by re-throwing on read;
|
|
2093
|
-
* marks are value-transparent, so they re-establish here instead).
|
|
2094
|
-
*/
|
|
2095
|
-
let affectsReads = null;
|
|
2096
2095
|
let snapshotCaptureActive = false;
|
|
2097
2096
|
let snapshotSources = null;
|
|
2098
2097
|
function ownerInSnapshotScope(owner) {
|
|
@@ -2179,9 +2178,6 @@ function recompute(el, create = false) {
|
|
|
2179
2178
|
let value = el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
|
|
2180
2179
|
let oldHeight = el._height;
|
|
2181
2180
|
let prevTracking = tracking;
|
|
2182
|
-
const prevAffectsReads = affectsReads;
|
|
2183
|
-
affectsReads = null;
|
|
2184
|
-
let markedReads = null;
|
|
2185
2181
|
let prevLane = currentOptimisticLane;
|
|
2186
2182
|
let prevStrictRead = false;
|
|
2187
2183
|
{
|
|
@@ -2189,6 +2185,14 @@ function recompute(el, create = false) {
|
|
|
2189
2185
|
strictRead = false;
|
|
2190
2186
|
}
|
|
2191
2187
|
tracking = true;
|
|
2188
|
+
// A computed's fn establishes its OWN dependencies, so it must never run
|
|
2189
|
+
// inside a latest() read window: read() short-circuits through the
|
|
2190
|
+
// companion path before dependency linking, so a memo created (eagerly
|
|
2191
|
+
// computed) inside latest(fn) came out permanently dependency-less (#2926).
|
|
2192
|
+
// latestRead() already suspends the flag for its pull-recomputes; this
|
|
2193
|
+
// covers creation-time computes and flushes that run inside the window.
|
|
2194
|
+
const prevLatestRead = latestReadActive;
|
|
2195
|
+
latestReadActive = false;
|
|
2192
2196
|
// Lane posture lives with the engine: OPTIMISTIC_DIRTY is only ever set by
|
|
2193
2197
|
// engine-driven paths, and _optimisticNodes is only pushed by
|
|
2194
2198
|
// _optimisticWrite, so the hook is installed whenever either gate holds.
|
|
@@ -2245,12 +2249,11 @@ function recompute(el, create = false) {
|
|
|
2245
2249
|
if (reaskChanged) GlobalQueue._repollVerdicts(el);
|
|
2246
2250
|
} finally {
|
|
2247
2251
|
tracking = prevTracking;
|
|
2252
|
+
latestReadActive = prevLatestRead;
|
|
2248
2253
|
strictRead = prevStrictRead;
|
|
2249
2254
|
if (isStaleEffect) stale = prevStale;
|
|
2250
2255
|
el._flags = REACTIVE_NONE | (create ? el._flags & REACTIVE_SNAPSHOT_STALE : 0);
|
|
2251
2256
|
context = oldcontext;
|
|
2252
|
-
markedReads = affectsReads;
|
|
2253
|
-
affectsReads = prevAffectsReads;
|
|
2254
2257
|
}
|
|
2255
2258
|
if (!el._error) {
|
|
2256
2259
|
trimStaleDeps(el);
|
|
@@ -2320,28 +2323,11 @@ function recompute(el, create = false) {
|
|
|
2320
2323
|
}
|
|
2321
2324
|
}
|
|
2322
2325
|
currentOptimisticLane = prevLane;
|
|
2323
|
-
// Marks read during this recompute re-attach after the commit above:
|
|
2324
|
-
// earlier, the sentinel's NotReadyError in `_error` would have routed the
|
|
2325
|
-
// fresh value into the error-skip branch instead of committing it. A real
|
|
2326
|
-
// (non-NotReady) error wins over marks (#2893): applying the sentinel here
|
|
2327
|
-
// would clobber the user's error with a NotReadyError from a node whose
|
|
2328
|
-
// reads value-transparency promises can never throw NotReady.
|
|
2329
|
-
// `markedReads` only collects under a live mark, so the hook is installed.
|
|
2330
|
-
if (markedReads && !(el._statusFlags & STATUS_ERROR))
|
|
2331
|
-
GlobalQueue._applyAffectsReads(el, markedReads);
|
|
2332
|
-
// Mark-only pending doesn't queue (#2893): the node holds no value needing
|
|
2333
|
-
// a transition-scheduled commit, and queueing would stamp the marking
|
|
2334
|
-
// action's transaction onto it — freezing unrelated writes that share it.
|
|
2335
|
-
// (Single mask test up front keeps the mark-free hot path at original cost.)
|
|
2336
|
-
const pendFlags = el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED);
|
|
2337
2326
|
const needsPendingCommit =
|
|
2338
2327
|
el._pendingValue !== NOT_PENDING ||
|
|
2339
2328
|
el._pendingFirstChild !== null ||
|
|
2340
2329
|
el._pendingDisposal !== null ||
|
|
2341
|
-
(
|
|
2342
|
-
(pendFlags !== STATUS_PENDING ||
|
|
2343
|
-
activeAffectsMarks === 0 ||
|
|
2344
|
-
!GlobalQueue._onlyMarkPending(el)));
|
|
2330
|
+
(el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) !== 0;
|
|
2345
2331
|
// Override-covered holds (hasOverride) always queue: their commit belongs
|
|
2346
2332
|
// to their own transition's schedule (A18 re-rule) and is unobservable
|
|
2347
2333
|
// under the override (A17). Revert no longer commits anything, so an
|
|
@@ -2650,17 +2636,14 @@ function read(el) {
|
|
|
2650
2636
|
!snapshotCaptureActive &&
|
|
2651
2637
|
!strictRead
|
|
2652
2638
|
) {
|
|
2653
|
-
if (c && tracking)
|
|
2654
|
-
link(el, c);
|
|
2655
|
-
// A live mark on a read source flows to the reader (probe reads are
|
|
2656
|
-
// excluded: the probe's own collection owns that verdict, and marking
|
|
2657
|
-
// the enclosing memo would make an `isPending` wrapper itself pending).
|
|
2658
|
-
if (activeAffectsMarks !== 0 && el._affectsCount && !pendingCheckActive)
|
|
2659
|
-
(affectsReads ??= []).push(el);
|
|
2660
|
-
}
|
|
2639
|
+
if (c && tracking) link(el, c);
|
|
2661
2640
|
return !c || el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
|
|
2662
2641
|
}
|
|
2663
|
-
|
|
2642
|
+
// The dev component-body safeguard (#2897) must not fire inside an
|
|
2643
|
+
// isPending() probe: its plain Error would be swallowed by the probe's
|
|
2644
|
+
// catch (which only rethrows NotReadyError), making dev return false where
|
|
2645
|
+
// prod propagates NotReady (#2928). Probe reads follow the prod path.
|
|
2646
|
+
if (strictRead && !pendingCheckActive && owner._statusFlags & STATUS_PENDING)
|
|
2664
2647
|
throwPendingUntrackedRead(strictRead, {
|
|
2665
2648
|
ownerId: c?.id,
|
|
2666
2649
|
ownerName: c?._name,
|
|
@@ -2668,16 +2651,6 @@ function read(el) {
|
|
|
2668
2651
|
});
|
|
2669
2652
|
if (c && tracking) {
|
|
2670
2653
|
link(el, c, pendingCheckActive);
|
|
2671
|
-
// Mark inheritance through derivation (see the fast path above), and its
|
|
2672
|
-
// transitive half (#2893): a mark-pended owner's sentinel sources flow to
|
|
2673
|
-
// the reader like a direct mark — value-transparent reads have no throw
|
|
2674
|
-
// to re-establish through, so without this a mid-window recompute sheds
|
|
2675
|
-
// pendingness permanently past one derivation level.
|
|
2676
|
-
if (activeAffectsMarks !== 0 && !pendingCheckActive) {
|
|
2677
|
-
if (el._affectsCount) (affectsReads ??= []).push(el);
|
|
2678
|
-
if (owner._statusFlags & STATUS_PENDING)
|
|
2679
|
-
GlobalQueue._collectMarkSources(owner, (affectsReads ??= []));
|
|
2680
|
-
}
|
|
2681
2654
|
if (owner._fn) {
|
|
2682
2655
|
const elQueue = queueFor(el);
|
|
2683
2656
|
if (owner._height >= elQueue._min) {
|
|
@@ -2692,16 +2665,7 @@ function read(el) {
|
|
|
2692
2665
|
}
|
|
2693
2666
|
}
|
|
2694
2667
|
}
|
|
2695
|
-
|
|
2696
|
-
// a promise of change, not an absence of value, so a derived node whose only
|
|
2697
|
-
// pending sources are mark sentinels keeps its real value readable —
|
|
2698
|
-
// pendingness reaches readers through verdicts (isPending), not throws.
|
|
2699
|
-
// (`activeAffectsMarks` gates the source scan off the mark-free hot path;
|
|
2700
|
-
// sentinels can't survive in pending sources past their last release.)
|
|
2701
|
-
if (
|
|
2702
|
-
owner._statusFlags & STATUS_PENDING &&
|
|
2703
|
-
!(activeAffectsMarks !== 0 && GlobalQueue._onlyMarkPending(owner))
|
|
2704
|
-
) {
|
|
2668
|
+
if (owner._statusFlags & STATUS_PENDING) {
|
|
2705
2669
|
if (c && !(stale && owner._transition && activeTransition !== owner._transition)) {
|
|
2706
2670
|
if (c && c._config & CONFIG_CHILDREN_FORBIDDEN) {
|
|
2707
2671
|
const message =
|
|
@@ -2758,9 +2722,7 @@ function read(el) {
|
|
|
2758
2722
|
nodeName: owner?._name
|
|
2759
2723
|
});
|
|
2760
2724
|
if (el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING) {
|
|
2761
|
-
//
|
|
2762
|
-
// the value for every reader — that check itself stays right here).
|
|
2763
|
-
if (c && stale && GlobalQueue._readStashed(el)) return el._value;
|
|
2725
|
+
// A17: the override IS the value for every reader.
|
|
2764
2726
|
return unwrapOverride(el._overrideValue);
|
|
2765
2727
|
}
|
|
2766
2728
|
// Entanglement gate: a reader recomputing under an optimistic lane that reads
|
|
@@ -3151,9 +3113,6 @@ function isUndefined(value) {
|
|
|
3151
3113
|
* `_optimisticNodes.length`, `activeLanes.size`), so `!` invocations are safe
|
|
3152
3114
|
* once the gate holds — the same late-binding contract as verdict.ts.
|
|
3153
3115
|
*/
|
|
3154
|
-
// When a background transition is stashed, plain optimistic signals need one
|
|
3155
|
-
// committed-view rerun. Keep that override local to the stash flush.
|
|
3156
|
-
let stashedOptimisticReads = null;
|
|
3157
3116
|
/** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */
|
|
3158
3117
|
function optimisticWrite(el, v) {
|
|
3159
3118
|
const hasOverride = el._overrideValue !== NOT_PENDING;
|
|
@@ -3191,34 +3150,6 @@ function optimisticWrite(el, v) {
|
|
|
3191
3150
|
schedule();
|
|
3192
3151
|
return v;
|
|
3193
3152
|
}
|
|
3194
|
-
function readStashed(node) {
|
|
3195
|
-
return !!stashedOptimisticReads?.has(node);
|
|
3196
|
-
}
|
|
3197
|
-
function queueStashedOptimisticEffects(node) {
|
|
3198
|
-
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
3199
|
-
const sub = s._sub;
|
|
3200
|
-
if (!sub._type) continue;
|
|
3201
|
-
enqueueSub(sub);
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
/**
|
|
3205
|
-
* Incomplete-transition finalization when the stashed transition holds
|
|
3206
|
-
* optimistic nodes: give plain optimistic signals one committed-view rerun.
|
|
3207
|
-
*/
|
|
3208
|
-
function stashOptimistic(stashedTransition) {
|
|
3209
|
-
stashedOptimisticReads = new Set();
|
|
3210
|
-
for (let i = 0; i < stashedTransition._optimisticNodes.length; i++) {
|
|
3211
|
-
const node = stashedTransition._optimisticNodes[i];
|
|
3212
|
-
if (node._fn || node._config & CONFIG_OWNED_WRITE) continue;
|
|
3213
|
-
stashedOptimisticReads.add(node);
|
|
3214
|
-
queueStashedOptimisticEffects(node);
|
|
3215
|
-
}
|
|
3216
|
-
try {
|
|
3217
|
-
finalizePureQueue(null, true);
|
|
3218
|
-
} finally {
|
|
3219
|
-
stashedOptimisticReads = null;
|
|
3220
|
-
}
|
|
3221
|
-
}
|
|
3222
3153
|
/**
|
|
3223
3154
|
* transitionComplete's override blockage: a settling transition stays open
|
|
3224
3155
|
* while one of its optimistic nodes holds an active override that is still
|
|
@@ -3231,11 +3162,7 @@ function transitionBlocked(transition) {
|
|
|
3231
3162
|
hasActiveOverride(node) &&
|
|
3232
3163
|
"_statusFlags" in node &&
|
|
3233
3164
|
node._statusFlags & STATUS_PENDING &&
|
|
3234
|
-
node._error instanceof NotReadyError
|
|
3235
|
-
// Mark-sourced pending never blocks settlement: affects() releases AT
|
|
3236
|
-
// settle, so counting its sentinel here would deadlock the window it
|
|
3237
|
-
// is scoped to.
|
|
3238
|
-
!node._error.source?._affectsFor
|
|
3165
|
+
node._error instanceof NotReadyError
|
|
3239
3166
|
) {
|
|
3240
3167
|
return true;
|
|
3241
3168
|
}
|
|
@@ -3397,11 +3324,9 @@ function installOptimisticEngine() {
|
|
|
3397
3324
|
if (GlobalQueue._optimisticWrite !== null) return;
|
|
3398
3325
|
GlobalQueue._optimisticWrite = optimisticWrite;
|
|
3399
3326
|
GlobalQueue._resolveOptimistic = resolveOptimisticNodes;
|
|
3400
|
-
GlobalQueue._stashOptimistic = stashOptimistic;
|
|
3401
3327
|
GlobalQueue._transitionBlocked = transitionBlocked;
|
|
3402
3328
|
GlobalQueue._cleanupLanes = cleanupCompletedLanes;
|
|
3403
3329
|
GlobalQueue._runLaneEffects = runLaneEffects;
|
|
3404
|
-
GlobalQueue._readStashed = readStashed;
|
|
3405
3330
|
GlobalQueue._gatedRead = gatedRead;
|
|
3406
3331
|
GlobalQueue._laneSuspends = laneSuspends;
|
|
3407
3332
|
GlobalQueue._laneReadsCommitted = laneReadsCommitted;
|
|
@@ -3450,6 +3375,46 @@ function collectPendingSources(el) {
|
|
|
3450
3375
|
function witnessAffects(node) {
|
|
3451
3376
|
pendingProbe?.sources.add(node);
|
|
3452
3377
|
}
|
|
3378
|
+
/**
|
|
3379
|
+
* The affects() coverage walk — the read half of the dedicated mark channel.
|
|
3380
|
+
* A node is covered by a live mark iff it carries one (`_affectsCount`) or
|
|
3381
|
+
* derives, through its CURRENT deps (hopping store firewalls), from a node
|
|
3382
|
+
* that does. Pull-based coverage means graph rewires, mid-window recomputes,
|
|
3383
|
+
* and probe-triggered recomputes can never strand or strip a mark — there is
|
|
3384
|
+
* nothing stored downstream to corrupt. Probe-created links
|
|
3385
|
+
* (`_pendingObserver`) are skipped so an `isPending` wrapper memo never
|
|
3386
|
+
* inherits the coverage it reports on.
|
|
3387
|
+
*/
|
|
3388
|
+
function markWalk(el, seen) {
|
|
3389
|
+
if (el._affectsCount) return true;
|
|
3390
|
+
// A real error outranks an inherited mark (A16/A24c): an errored node
|
|
3391
|
+
// answers probes with its error, not a coverage verdict, and coverage does
|
|
3392
|
+
// not flow through it — matching the rails' behavior, where propagation
|
|
3393
|
+
// stopped at errored nodes. A DIRECT mark on an errored node still reads
|
|
3394
|
+
// pending (the count check above), also matching.
|
|
3395
|
+
if (el._statusFlags & STATUS_ERROR) return false;
|
|
3396
|
+
if (seen.has(el)) return false;
|
|
3397
|
+
seen.add(el);
|
|
3398
|
+
const firewall = el._firewall;
|
|
3399
|
+
if (firewall && markWalk(firewall, seen)) return true;
|
|
3400
|
+
// Mid-recompute (the clearStatus companion poke runs before
|
|
3401
|
+
// trimStaleDeps), only the validated prefix [_deps.._depsTail] is this
|
|
3402
|
+
// pass's dependency set — walking past it would read dropped deps and
|
|
3403
|
+
// latch a stale verdict on the companion.
|
|
3404
|
+
const comp = el;
|
|
3405
|
+
const tail = comp._flags & REACTIVE_RECOMPUTING_DEPS ? comp._depsTail : undefined;
|
|
3406
|
+
if (tail !== null) {
|
|
3407
|
+
for (let d = comp._deps ?? null; d !== null; d = d._nextDep) {
|
|
3408
|
+
if (!d._pendingObserver && markWalk(d._dep, seen)) return true;
|
|
3409
|
+
if (d === tail) break;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
return false;
|
|
3413
|
+
}
|
|
3414
|
+
/** Gated entry: apps with no live mark pay one integer compare. */
|
|
3415
|
+
function markCovered(el) {
|
|
3416
|
+
return activeAffectsMarks !== 0 && markWalk(el, new Set());
|
|
3417
|
+
}
|
|
3453
3418
|
function quietPending(el) {
|
|
3454
3419
|
if (el._pendingSources) {
|
|
3455
3420
|
for (const source of el._pendingSources) if (!source._reask) return false;
|
|
@@ -3467,11 +3432,13 @@ function newQuestionInFlight(comp) {
|
|
|
3467
3432
|
function computePendingState(el) {
|
|
3468
3433
|
const comp = el;
|
|
3469
3434
|
if (comp._flags & REACTIVE_DISPOSED) return false;
|
|
3470
|
-
|
|
3435
|
+
// Mark coverage is transitive by dep-graph reachability: a latest() shadow
|
|
3436
|
+
// reaches its owner (and a store leaf its firewall) through its own deps,
|
|
3437
|
+
// so the one walk covers direct marks, derivation, and companion chains.
|
|
3438
|
+
if (markCovered(el)) return true;
|
|
3471
3439
|
const firewall = el._firewall;
|
|
3472
3440
|
if (el._parentSource) {
|
|
3473
3441
|
const parentNode = el._parentSource;
|
|
3474
|
-
if (parentNode._affectsCount) return true;
|
|
3475
3442
|
const parent = parentNode._firewall || parentNode;
|
|
3476
3443
|
return newQuestionInFlight(parent);
|
|
3477
3444
|
}
|
|
@@ -3504,12 +3471,22 @@ function updateChildCompanions(el) {
|
|
|
3504
3471
|
if (child._pendingSignal || child._latestValueComputed) updatePendingSignal(child);
|
|
3505
3472
|
}
|
|
3506
3473
|
}
|
|
3507
|
-
|
|
3474
|
+
/**
|
|
3475
|
+
* Re-derive every verdict companion downstream of `el` (subs + firewall
|
|
3476
|
+
* children, dedup'd). The affects() channel's poke walk: registration and
|
|
3477
|
+
* re-ask flips use the live write path (companion setSignal — its own lane
|
|
3478
|
+
* lets the wake escape an incomplete transition's effect stash, #2887);
|
|
3479
|
+
* mark release passes `snap` because it runs inside queue finalization,
|
|
3480
|
+
* where companion writes must land committed (a setSignal there would open
|
|
3481
|
+
* a fresh override window that nothing settles).
|
|
3482
|
+
*/
|
|
3483
|
+
function repollDownstreamVerdicts(el, snap = false) {
|
|
3484
|
+
const update = snap ? snapCompanionsToState : updatePendingSignal;
|
|
3508
3485
|
const visited = new Set();
|
|
3509
3486
|
const visit = node => {
|
|
3510
3487
|
if (visited.has(node)) return;
|
|
3511
3488
|
visited.add(node);
|
|
3512
|
-
if (node._pendingSignal || node._latestValueComputed)
|
|
3489
|
+
if (node._pendingSignal || node._latestValueComputed) update(node);
|
|
3513
3490
|
for (let s = node._subs; s !== null; s = s._nextSub) visit(s._sub);
|
|
3514
3491
|
for (let child = node._child ?? null; child !== null; child = child._nextChild) {
|
|
3515
3492
|
visit(child);
|
|
@@ -4336,6 +4313,12 @@ function createReaction(effectFn, options) {
|
|
|
4336
4313
|
});
|
|
4337
4314
|
};
|
|
4338
4315
|
}
|
|
4316
|
+
/** Delivers effect applies on a microtask instead of queueing them (#2930). */
|
|
4317
|
+
class MicrotaskQueue extends Queue {
|
|
4318
|
+
enqueue(type, fn) {
|
|
4319
|
+
queueMicrotask(() => fn(type));
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4339
4322
|
/**
|
|
4340
4323
|
* Awaits a reactive expression and returns its first fully-settled value as a
|
|
4341
4324
|
* `Promise`. Pending async reads (`createMemo` returning a promise, etc.) are
|
|
@@ -4365,6 +4348,17 @@ function resolve(fn) {
|
|
|
4365
4348
|
}
|
|
4366
4349
|
return new Promise((res, rej) => {
|
|
4367
4350
|
createRoot(dispose => {
|
|
4351
|
+
// Deliver effect applies on a microtask instead of the owner queue: an
|
|
4352
|
+
// incomplete transition stashes its effect queues until it settles, but
|
|
4353
|
+
// an action yielding this promise is itself what keeps the transition
|
|
4354
|
+
// open — the stashed res() deadlocked the action (#2930). The compute
|
|
4355
|
+
// still runs in place (under the transaction's view when created inside
|
|
4356
|
+
// an action step), and status/boundary notifications keep their normal
|
|
4357
|
+
// route through the inherited queue.
|
|
4358
|
+
const owner = getOwner();
|
|
4359
|
+
const queue = new MicrotaskQueue();
|
|
4360
|
+
queue._parent = owner._queue; // notify() forwards up the normal chain
|
|
4361
|
+
owner._queue = queue;
|
|
4368
4362
|
// A user effect rather than a bare computed: computeds are pull-based and
|
|
4369
4363
|
// are only re-enqueued when a pending source *resolves* — a rejection just
|
|
4370
4364
|
// marks them errored, so nothing would re-run and the promise would never
|
|
@@ -6103,127 +6097,76 @@ function createStore(first, second, options) {
|
|
|
6103
6097
|
}
|
|
6104
6098
|
|
|
6105
6099
|
/**
|
|
6106
|
-
* The
|
|
6107
|
-
*
|
|
6108
|
-
*
|
|
6109
|
-
*
|
|
6110
|
-
*
|
|
6111
|
-
*
|
|
6112
|
-
* - `_reask` is permanently `false`: a mark is by definition a declared
|
|
6113
|
-
* value change, so `quietPending` never silences a window it participates
|
|
6114
|
-
* in — even when the mark rides over an otherwise-quiet `refresh()`
|
|
6115
|
-
* re-ask of the same node (the whole point of declaring one).
|
|
6116
|
-
* - A landing on the marked node settles only the node's OWN source entry;
|
|
6117
|
-
* the sentinel entry survives until the mark's transaction releases it.
|
|
6118
|
-
* - The sentinel itself never carries `STATUS_PENDING`, so
|
|
6119
|
-
* `transitionComplete` never counts a mark as a blocker of its own
|
|
6120
|
-
* transaction (release happens AT settle — self-blocking would deadlock),
|
|
6121
|
-
* and reads of the marked node never throw (marks are value-transparent
|
|
6122
|
-
* at the source; pendingness is what propagates).
|
|
6100
|
+
* The counting half of a mark, shared by direct registration and store-scope
|
|
6101
|
+
* inheritance (a node created inside a live keyless mark's identity scope).
|
|
6102
|
+
* A mark is ONLY this count: coverage of everything derived from the node is
|
|
6103
|
+
* pull-derived by the verdict layer's `markWalk` (dep-graph reachability), so
|
|
6104
|
+
* nothing is stored downstream and nothing can be stranded or stripped by
|
|
6105
|
+
* mid-window recomputes.
|
|
6123
6106
|
*/
|
|
6124
|
-
function
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
// Brand + backref: lets the scheduler's settlement checks recognize
|
|
6128
|
-
// mark-sourced pending (which must never block its own transaction —
|
|
6129
|
-
// release happens AT settle).
|
|
6130
|
-
_affectsFor: node,
|
|
6131
|
-
_flags: 0,
|
|
6132
|
-
_statusFlags: 0,
|
|
6133
|
-
_reask: false,
|
|
6134
|
-
_error: undefined,
|
|
6135
|
-
_subs: null,
|
|
6136
|
-
_deps: null
|
|
6137
|
-
});
|
|
6107
|
+
function markAffects(node) {
|
|
6108
|
+
node._affectsCount = (node._affectsCount || 0) + 1;
|
|
6109
|
+
shiftAffectsMarks(1);
|
|
6138
6110
|
}
|
|
6139
6111
|
/**
|
|
6140
|
-
*
|
|
6141
|
-
*
|
|
6142
|
-
*
|
|
6143
|
-
*
|
|
6144
|
-
*
|
|
6145
|
-
* `
|
|
6146
|
-
* re-
|
|
6147
|
-
*
|
|
6148
|
-
*
|
|
6149
|
-
*
|
|
6150
|
-
* that merely share a downstream memo) would be captured and frozen until the
|
|
6151
|
-
* action settles.
|
|
6112
|
+
* Boundary visual channel: within a transaction, a live mark holds Loading
|
|
6113
|
+
* fallbacks / reveal ordering the way real in-flight async would — but the
|
|
6114
|
+
* notification is tagged visibility-only at the source (`_markVisual`), so
|
|
6115
|
+
* the root queue never registers reporters from it: marks are invisible to
|
|
6116
|
+
* ALL completion and settlement accounting by construction. Boundaries hold
|
|
6117
|
+
* the marked node in `_sources` while `_affectsCount` is live; the release
|
|
6118
|
+
* sweep (finalizePureQueue re-checks boundary children after mark release)
|
|
6119
|
+
* is the display-state update point. Ambient marks release inside the same
|
|
6120
|
+
* flush that would surface them, before effects run — verdict-only, netting
|
|
6121
|
+
* no visual change.
|
|
6152
6122
|
*/
|
|
6153
|
-
function
|
|
6123
|
+
function notifyMarkBoundaries(node) {
|
|
6154
6124
|
if (!node._subs && !node._child) return;
|
|
6155
|
-
const
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
* any sentinel entries it held). Called by `recompute` after the commit —
|
|
6167
|
-
* not before, because setting `_error` earlier would make the commit path
|
|
6168
|
-
* treat the node as errored and skip the value write.
|
|
6169
|
-
*/
|
|
6170
|
-
function applyAffectsReads(el, sources) {
|
|
6171
|
-
let applied = false;
|
|
6172
|
-
for (let i = 0; i < sources.length; i++) {
|
|
6173
|
-
const src = sources[i];
|
|
6174
|
-
if (!src._affectsCount) continue;
|
|
6175
|
-
const sentinel = getAffectsSentinel(src);
|
|
6176
|
-
if (addPendingSource(el, sentinel)) {
|
|
6177
|
-
el._statusFlags |= STATUS_PENDING;
|
|
6178
|
-
setPendingError(el, sentinel);
|
|
6179
|
-
applied = true;
|
|
6125
|
+
const error = new NotReadyError(node);
|
|
6126
|
+
error._markVisual = true;
|
|
6127
|
+
const visited = new Set();
|
|
6128
|
+
const visit = sub => {
|
|
6129
|
+
if (visited.has(sub)) return;
|
|
6130
|
+
visited.add(sub);
|
|
6131
|
+
// Display consumers (render effects, boundary computeds) act on the
|
|
6132
|
+
// notification; descent stops there, exactly like the status rails.
|
|
6133
|
+
if (sub._notifyStatus) {
|
|
6134
|
+
sub._notifyStatus(STATUS_PENDING, error);
|
|
6135
|
+
return;
|
|
6180
6136
|
}
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
/**
|
|
6185
|
-
* The counting half of a mark, shared by direct registration and store-scope
|
|
6186
|
-
* inheritance (a node created inside a live keyless mark's identity scope):
|
|
6187
|
-
* bumps the refcount and pokes the node's verdict companions so an
|
|
6188
|
-
* already-materialized `false` flips reactively.
|
|
6189
|
-
*/
|
|
6190
|
-
function markAffects(node) {
|
|
6191
|
-
node._affectsCount = (node._affectsCount || 0) + 1;
|
|
6192
|
-
shiftAffectsMarks(1);
|
|
6193
|
-
// Companions only exist once the verdict layer (isPending/latest) loaded;
|
|
6194
|
-
// without them there is no materialized verdict to flip.
|
|
6195
|
-
if (node._affectsCount === 1 && GlobalQueue._updatePendingSignal !== null)
|
|
6196
|
-
GlobalQueue._updatePendingSignal(node);
|
|
6137
|
+
forEachDependent(sub, visit);
|
|
6138
|
+
};
|
|
6139
|
+
forEachDependent(node, visit);
|
|
6197
6140
|
}
|
|
6198
6141
|
/**
|
|
6199
6142
|
* Registers one `affects()` mark on a node: counts it, records the
|
|
6200
6143
|
* registration with the current transaction (after initTransition the queue's
|
|
6201
|
-
* batch IS the active transition, mirroring `_optimisticNodes`),
|
|
6202
|
-
*
|
|
6203
|
-
*
|
|
6204
|
-
* registration (not just the first): subscribers gained since an
|
|
6205
|
-
* overlapping registration get covered, and dedup stops re-descent
|
|
6144
|
+
* batch IS the active transition, mirroring `_optimisticNodes`), re-derives
|
|
6145
|
+
* every downstream verdict companion (the mark channel's only push — verdict
|
|
6146
|
+
* pokes, not state), and notifies boundary display state. Both walks run on
|
|
6147
|
+
* every registration (not just the first): subscribers gained since an
|
|
6148
|
+
* earlier overlapping registration get covered, and dedup stops re-descent.
|
|
6206
6149
|
*/
|
|
6207
6150
|
function registerAffectsMark(node) {
|
|
6208
6151
|
markAffects(node);
|
|
6209
6152
|
globalQueue._batch._affectsNodes.push(node);
|
|
6210
|
-
|
|
6153
|
+
// Companions only exist once the verdict layer (isPending/latest) loaded;
|
|
6154
|
+
// without them there is no materialized verdict to poke.
|
|
6155
|
+
GlobalQueue._repollVerdicts !== null && GlobalQueue._repollVerdicts(node);
|
|
6156
|
+
notifyMarkBoundaries(node);
|
|
6211
6157
|
schedule();
|
|
6212
6158
|
}
|
|
6213
6159
|
/**
|
|
6214
|
-
* Releases one registration. When the node's last mark drops,
|
|
6215
|
-
*
|
|
6216
|
-
*
|
|
6217
|
-
*
|
|
6218
|
-
* can't open a fresh override window that would itself need settlement.
|
|
6160
|
+
* Releases one registration. When the node's last mark drops, re-derives
|
|
6161
|
+
* every downstream verdict through the settlement snap (committed, not
|
|
6162
|
+
* transition-scoped — release runs inside queue finalization, where a
|
|
6163
|
+
* setSignal would open a fresh override window that nothing settles).
|
|
6219
6164
|
*/
|
|
6220
6165
|
function releaseAffectsMark(node) {
|
|
6221
6166
|
shiftAffectsMarks(-1);
|
|
6222
6167
|
node._affectsCount--;
|
|
6223
6168
|
if (!node._affectsCount) {
|
|
6224
|
-
|
|
6225
|
-
if (sentinel) settlePendingSource(node, sentinel, true);
|
|
6226
|
-
GlobalQueue._snapCompanions !== null && GlobalQueue._snapCompanions(node);
|
|
6169
|
+
GlobalQueue._repollVerdicts !== null && GlobalQueue._repollVerdicts(node, true);
|
|
6227
6170
|
GlobalQueue._releaseAffectsScope?.(node);
|
|
6228
6171
|
}
|
|
6229
6172
|
}
|
|
@@ -6235,53 +6178,14 @@ function releaseAffectsMarks(nodes) {
|
|
|
6235
6178
|
for (let i = 0; i < nodes.length; i++) releaseAffectsMark(nodes[i]);
|
|
6236
6179
|
nodes.length = 0;
|
|
6237
6180
|
}
|
|
6238
|
-
/**
|
|
6239
|
-
* True when a node's pending status comes ONLY from affects() sentinels. A
|
|
6240
|
-
* mark is a promise of change, not an absence of value: reads of mark-pended
|
|
6241
|
-
* derived nodes stay value-transparent (verdicts report pending; the read
|
|
6242
|
-
* path must not suspend). Any real async source among the pending sources
|
|
6243
|
-
* keeps normal suspension semantics. Core's read path reaches this through
|
|
6244
|
-
* `GlobalQueue._onlyMarkPending`, gated on the `activeAffectsMarks` counter.
|
|
6245
|
-
*/
|
|
6246
|
-
function onlyMarkPending(el) {
|
|
6247
|
-
const sources = el._pendingSources;
|
|
6248
|
-
if (sources) {
|
|
6249
|
-
for (const s of sources) if (!s._affectsFor) return false;
|
|
6250
|
-
return true;
|
|
6251
|
-
}
|
|
6252
|
-
return false;
|
|
6253
|
-
}
|
|
6254
|
-
/**
|
|
6255
|
-
* Collect the still-live marked nodes behind a pended owner's sentinel
|
|
6256
|
-
* sources into a recompute's `affectsReads`. This is the transitive half of
|
|
6257
|
-
* read-path re-establishment (#2893): real async re-establishes at every
|
|
6258
|
-
* derivation level because its re-throw on read re-registers the source, but
|
|
6259
|
-
* mark-pended reads are value-transparent — without this, pendingness dies on
|
|
6260
|
-
* the first mid-window recompute past depth one, and the isPending() probe
|
|
6261
|
-
* itself (whose prepare step recomputes retryable NotReady holders) strips
|
|
6262
|
-
* the very status it reports on. Reached through
|
|
6263
|
-
* `GlobalQueue._collectMarkSources`, gated on `activeAffectsMarks`.
|
|
6264
|
-
*/
|
|
6265
|
-
function collectMarkSources(el, into) {
|
|
6266
|
-
if (el._pendingSources) {
|
|
6267
|
-
for (const s of el._pendingSources) {
|
|
6268
|
-
const marked = s._affectsFor;
|
|
6269
|
-
if (marked && marked._affectsCount) into.push(marked);
|
|
6270
|
-
}
|
|
6271
|
-
}
|
|
6272
|
-
}
|
|
6273
6181
|
// Late installation (same pattern as `GlobalQueue._update`): the mark engine
|
|
6274
6182
|
// lives with the feature so graphs that never declare a mark never ship it.
|
|
6275
|
-
// Each call site is gated by state only this module creates (
|
|
6276
|
-
//
|
|
6277
|
-
//
|
|
6278
|
-
// before the first time any of them can fire.
|
|
6279
|
-
GlobalQueue._applyAffectsReads = applyAffectsReads;
|
|
6183
|
+
// Each call site is gated by state only this module creates (a non-empty
|
|
6184
|
+
// `_affectsNodes` batch, a live scope in the store's `affectsScopes`), so the
|
|
6185
|
+
// hooks are installed before the first time any of them can fire.
|
|
6280
6186
|
GlobalQueue._releaseAffectsMarks = releaseAffectsMarks;
|
|
6281
6187
|
GlobalQueue._markAffects = markAffects;
|
|
6282
6188
|
GlobalQueue._releaseAffectsMark = releaseAffectsMark;
|
|
6283
|
-
GlobalQueue._onlyMarkPending = onlyMarkPending;
|
|
6284
|
-
GlobalQueue._collectMarkSources = collectMarkSources;
|
|
6285
6189
|
function affects(target, key) {
|
|
6286
6190
|
if (arguments.length > 2) {
|
|
6287
6191
|
const message =
|
|
@@ -7512,9 +7416,14 @@ class CollectionQueue extends Queue {
|
|
|
7512
7416
|
}
|
|
7513
7417
|
_checkSources() {
|
|
7514
7418
|
for (const source of this._sources) {
|
|
7419
|
+
// A source with a live affects() mark holds display state for the
|
|
7420
|
+
// mark's lifetime (the visual channel): the marked node carries no
|
|
7421
|
+
// status of its own, so the count is the liveness test. The release
|
|
7422
|
+
// sweep (finalizePureQueue after mark release) re-runs this check.
|
|
7515
7423
|
if (
|
|
7516
7424
|
source._flags & REACTIVE_DISPOSED ||
|
|
7517
|
-
(!
|
|
7425
|
+
(!source._affectsCount &&
|
|
7426
|
+
!(source._statusFlags & this._collectionType) &&
|
|
7518
7427
|
!(this._collectionType & STATUS_ERROR && source._statusFlags & STATUS_PENDING))
|
|
7519
7428
|
)
|
|
7520
7429
|
this._sources.delete(source);
|