@solidjs/signals 2.0.0-beta.20 → 2.0.0-beta.22
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/README.md +5 -1
- package/dist/dev.js +267 -307
- package/dist/node.cjs +1125 -1168
- package/dist/prod/affects.js +71 -163
- package/dist/prod/boundaries.js +141 -137
- package/dist/prod/core/action.js +50 -12
- package/dist/prod/core/async.js +83 -99
- package/dist/prod/core/core.js +255 -298
- package/dist/prod/core/effect.js +46 -46
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +54 -46
- package/dist/prod/core/heap.js +58 -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 +217 -211
- package/dist/prod/core/verdict.js +144 -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/action.d.ts +16 -0
- 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/action.d.cts +16 -0
- 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;
|
|
@@ -731,8 +726,12 @@ class GlobalQueue extends Queue {
|
|
|
731
726
|
const stashedTransition = activeTransition;
|
|
732
727
|
runHeap(zombieQueue, GlobalQueue._update);
|
|
733
728
|
// Detach: the stashed transition keeps its batch; ambient work that
|
|
734
|
-
// follows lands in a fresh one.
|
|
735
|
-
|
|
729
|
+
// follows lands in a fresh one. If the batch is already a separate
|
|
730
|
+
// ambient one — action done() restored activeTransition without
|
|
731
|
+
// adopting the batch, and an ordinary write landed there before
|
|
732
|
+
// the scheduled flush (#2916) — keep it: replacing it would strand
|
|
733
|
+
// its queued pending nodes with held _pendingValues forever.
|
|
734
|
+
if (this._batch === stashedTransition) currentBatch = this._batch = createBatch();
|
|
736
735
|
// Run lane effects immediately (before stashing) - lanes with no pending async
|
|
737
736
|
if (activeLanes.size) {
|
|
738
737
|
GlobalQueue._runLaneEffects(EFFECT_RENDER);
|
|
@@ -740,21 +739,13 @@ class GlobalQueue extends Queue {
|
|
|
740
739
|
}
|
|
741
740
|
this.stashQueues(stashedTransition._queueStash);
|
|
742
741
|
clock++;
|
|
743
|
-
|
|
742
|
+
// A kept ambient batch may hold pending nodes (#2916): stay
|
|
743
|
+
// scheduled so the outer drain loop commits them via the plain
|
|
744
|
+
// flush path instead of leaving them until the next natural flush.
|
|
745
|
+
scheduled = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
|
|
744
746
|
reassignPendingTransition(stashedTransition._pendingNodes);
|
|
745
747
|
activeTransition = null;
|
|
746
|
-
|
|
747
|
-
// wraps finalizePureQueue in the engine; a non-empty _optimisticNodes
|
|
748
|
-
// means _optimisticWrite ran, which installed the hook.
|
|
749
|
-
if (
|
|
750
|
-
!stashedTransition._actions.length &&
|
|
751
|
-
!stashedTransition._asyncReporters.size &&
|
|
752
|
-
stashedTransition._optimisticNodes.length
|
|
753
|
-
) {
|
|
754
|
-
GlobalQueue._stashOptimistic(stashedTransition);
|
|
755
|
-
} else {
|
|
756
|
-
finalizePureQueue(null, true);
|
|
757
|
-
}
|
|
748
|
+
finalizePureQueue(null, true);
|
|
758
749
|
return;
|
|
759
750
|
}
|
|
760
751
|
const completingTransition = activeTransition;
|
|
@@ -826,6 +817,11 @@ class GlobalQueue extends Queue {
|
|
|
826
817
|
if (mask & STATUS_PENDING) {
|
|
827
818
|
if (flags & STATUS_PENDING) {
|
|
828
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;
|
|
829
825
|
if (activeTransition && actualError) {
|
|
830
826
|
const source = actualError.source;
|
|
831
827
|
let reporters = activeTransition._asyncReporters.get(source);
|
|
@@ -972,8 +968,14 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
972
968
|
}
|
|
973
969
|
// Declared motion ends with the transaction: settle (or plain flush end
|
|
974
970
|
// for ambient marks) releases each registration's refcount. A non-empty
|
|
975
|
-
// batch means registerAffectsMark ran, which installed the hook.
|
|
976
|
-
|
|
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
|
+
}
|
|
977
979
|
// A non-empty set means trackOptimisticStore ran, which installed the
|
|
978
980
|
// hook; the hook iterates, clears, and schedules (keeping the loop out of
|
|
979
981
|
// core lets esbuild shake it — rollup already folds the null guard). The
|
|
@@ -1156,7 +1158,15 @@ function insertIntoHeap(n, heap) {
|
|
|
1156
1158
|
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return;
|
|
1157
1159
|
if (flags & REACTIVE_CHECK) {
|
|
1158
1160
|
n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
1159
|
-
} else
|
|
1161
|
+
} else {
|
|
1162
|
+
n._flags = flags | REACTIVE_IN_HEAP;
|
|
1163
|
+
// An unmarked node entering a marked heap invalidates the markHeap memo:
|
|
1164
|
+
// `_marked` is only reset by runHeap, so a write between two mid-tick
|
|
1165
|
+
// pulls (read-time markHeap + updateIfNecessary) would otherwise leave
|
|
1166
|
+
// this node unmarked and every downstream pull stale until the next
|
|
1167
|
+
// flush (#2922: the second `latest()` returned the first write's value).
|
|
1168
|
+
if (heap._marked && !(flags & REACTIVE_DIRTY)) heap._marked = false;
|
|
1169
|
+
}
|
|
1160
1170
|
if (!(flags & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(n, heap);
|
|
1161
1171
|
}
|
|
1162
1172
|
function insertIntoHeapHeight(n, heap) {
|
|
@@ -1588,9 +1598,14 @@ function unobserved(el) {
|
|
|
1588
1598
|
}
|
|
1589
1599
|
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
|
|
1590
1600
|
function link(dep, sub, pendingObserver = false) {
|
|
1601
|
+
// Repeat touches within one pass AND-combine `_pendingObserver`: a probe
|
|
1602
|
+
// read (`isPending(() => x())`) beside a value read of the same dep must
|
|
1603
|
+
// not relabel the value dependency as probe-only — the value read is what
|
|
1604
|
+
// real-error propagation and affects() coverage key off, regardless of
|
|
1605
|
+
// read order within the computation.
|
|
1591
1606
|
const prevDep = sub._depsTail;
|
|
1592
1607
|
if (prevDep !== null && prevDep._dep === dep) {
|
|
1593
|
-
prevDep._pendingObserver
|
|
1608
|
+
prevDep._pendingObserver &&= pendingObserver;
|
|
1594
1609
|
return;
|
|
1595
1610
|
}
|
|
1596
1611
|
let nextDep = null;
|
|
@@ -1600,6 +1615,7 @@ function link(dep, sub, pendingObserver = false) {
|
|
|
1600
1615
|
if (nextDep !== null && nextDep._dep === dep) {
|
|
1601
1616
|
nextDep._gen = sub._depGen;
|
|
1602
1617
|
sub._depsTail = nextDep;
|
|
1618
|
+
// First touch of this pass: the previous pass's label is stale.
|
|
1603
1619
|
nextDep._pendingObserver = pendingObserver;
|
|
1604
1620
|
return;
|
|
1605
1621
|
}
|
|
@@ -1615,7 +1631,10 @@ function link(dep, sub, pendingObserver = false) {
|
|
|
1615
1631
|
prevSub._sub === sub &&
|
|
1616
1632
|
(!isRecomputing || prevSub._gen === sub._depGen)
|
|
1617
1633
|
) {
|
|
1618
|
-
|
|
1634
|
+
// Gen-matched during a recompute = repeat touch this pass (AND); outside
|
|
1635
|
+
// a recompute there is no pass boundary, so the latest read labels it.
|
|
1636
|
+
if (isRecomputing) prevSub._pendingObserver &&= pendingObserver;
|
|
1637
|
+
else prevSub._pendingObserver = pendingObserver;
|
|
1619
1638
|
return;
|
|
1620
1639
|
}
|
|
1621
1640
|
const newLink =
|
|
@@ -1679,20 +1698,13 @@ function forEachDependent(el, fn) {
|
|
|
1679
1698
|
// Queue a node to re-run on the next flush (used both when a pending source
|
|
1680
1699
|
// settles and when an `isPending` observer must re-evaluate after a real error):
|
|
1681
1700
|
// shared scheduling helper in heap.ts (tracked effects bypass the heap).
|
|
1682
|
-
function settlePendingSource(
|
|
1683
|
-
el,
|
|
1684
|
-
source = el,
|
|
1685
|
-
// Mark release runs inside queue finalization: companion writes must go
|
|
1686
|
-
// through the settlement snap (committed), because a setSignal here would
|
|
1687
|
-
// open a fresh transition-scoped override window that nothing reverts.
|
|
1688
|
-
snap = false
|
|
1689
|
-
) {
|
|
1701
|
+
function settlePendingSource(el) {
|
|
1690
1702
|
let scheduled = false;
|
|
1691
1703
|
const visited = new Set();
|
|
1692
|
-
// Companion updates no-op without the verdict layer (null
|
|
1693
|
-
const updateCompanions =
|
|
1704
|
+
// Companion updates no-op without the verdict layer (null hook).
|
|
1705
|
+
const updateCompanions = GlobalQueue._updatePendingSignal;
|
|
1694
1706
|
const settle = node => {
|
|
1695
|
-
if (visited.has(node) || !removePendingSource(node,
|
|
1707
|
+
if (visited.has(node) || !removePendingSource(node, el)) return;
|
|
1696
1708
|
visited.add(node);
|
|
1697
1709
|
node._time = clock;
|
|
1698
1710
|
const remaining = node._pendingSources?.values().next().value;
|
|
@@ -1968,18 +1980,6 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
1968
1980
|
error = new StatusError(el, error);
|
|
1969
1981
|
const pendingSource =
|
|
1970
1982
|
status === STATUS_PENDING && error instanceof NotReadyError ? error.source : undefined;
|
|
1971
|
-
// Mark-sourced propagation must not capture subscribers into the marking
|
|
1972
|
-
// action's transaction (#2893): they carry no held value needing a
|
|
1973
|
-
// transition-scheduled commit, and stamping `_transition` on them would
|
|
1974
|
-
// freeze unrelated writes that share a downstream memo until the action
|
|
1975
|
-
// settles. Real async keeps queuing (its commits ride the transition).
|
|
1976
|
-
const markSourced = pendingSource?._affectsFor !== undefined;
|
|
1977
|
-
// A real error is a settled verdict a mark must not erase (#2893): landing
|
|
1978
|
-
// STATUS_PENDING here would clobber `_error` with the sentinel's
|
|
1979
|
-
// NotReadyError, and unlike real async there is no arriving value whose
|
|
1980
|
-
// recompute would surface the error again. Descent stops too — everything
|
|
1981
|
-
// downstream holds the propagated error for the same reason.
|
|
1982
|
-
if (markSourced && el._statusFlags & STATUS_ERROR) return;
|
|
1983
1983
|
const isSource = pendingSource === el;
|
|
1984
1984
|
const isOptimisticBoundary =
|
|
1985
1985
|
status === STATUS_PENDING && el._overrideValue !== undefined && !isSource;
|
|
@@ -2033,7 +2033,7 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
2033
2033
|
schedule();
|
|
2034
2034
|
return;
|
|
2035
2035
|
}
|
|
2036
|
-
if (!downstreamBlockStatus && !
|
|
2036
|
+
if (!downstreamBlockStatus && !sub._transition) queuePendingNode(sub);
|
|
2037
2037
|
notifyStatus(sub, status, error, downstreamBlockStatus, downstreamLane);
|
|
2038
2038
|
}
|
|
2039
2039
|
});
|
|
@@ -2067,17 +2067,6 @@ let pendingCheckActive = false;
|
|
|
2067
2067
|
let latestReadActive = false;
|
|
2068
2068
|
let context = null;
|
|
2069
2069
|
let currentOptimisticLane = null;
|
|
2070
|
-
/**
|
|
2071
|
-
* Marked sources read by the recompute currently on the stack (saved/restored
|
|
2072
|
-
* per recompute, like `context`). A computed that reads a node with a live
|
|
2073
|
-
* `affects()` mark inherits the mark's pendingness — `recompute` applies the
|
|
2074
|
-
* collected sources through `applyAffectsReads` after its commit, because the
|
|
2075
|
-
* `clearStatus` at the top of the commit path wipes whatever sentinel entries
|
|
2076
|
-
* the node held from the registration-time push. This is the pull half of
|
|
2077
|
-
* mark propagation (real async re-establishes itself by re-throwing on read;
|
|
2078
|
-
* marks are value-transparent, so they re-establish here instead).
|
|
2079
|
-
*/
|
|
2080
|
-
let affectsReads = null;
|
|
2081
2070
|
let snapshotCaptureActive = false;
|
|
2082
2071
|
let snapshotSources = null;
|
|
2083
2072
|
function ownerInSnapshotScope(owner) {
|
|
@@ -2164,9 +2153,6 @@ function recompute(el, create = false) {
|
|
|
2164
2153
|
let value = el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
|
|
2165
2154
|
let oldHeight = el._height;
|
|
2166
2155
|
let prevTracking = tracking;
|
|
2167
|
-
const prevAffectsReads = affectsReads;
|
|
2168
|
-
affectsReads = null;
|
|
2169
|
-
let markedReads = null;
|
|
2170
2156
|
let prevLane = currentOptimisticLane;
|
|
2171
2157
|
let prevStrictRead = false;
|
|
2172
2158
|
{
|
|
@@ -2174,6 +2160,14 @@ function recompute(el, create = false) {
|
|
|
2174
2160
|
strictRead = false;
|
|
2175
2161
|
}
|
|
2176
2162
|
tracking = true;
|
|
2163
|
+
// A computed's fn establishes its OWN dependencies, so it must never run
|
|
2164
|
+
// inside a latest() read window: read() short-circuits through the
|
|
2165
|
+
// companion path before dependency linking, so a memo created (eagerly
|
|
2166
|
+
// computed) inside latest(fn) came out permanently dependency-less (#2926).
|
|
2167
|
+
// latestRead() already suspends the flag for its pull-recomputes; this
|
|
2168
|
+
// covers creation-time computes and flushes that run inside the window.
|
|
2169
|
+
const prevLatestRead = latestReadActive;
|
|
2170
|
+
latestReadActive = false;
|
|
2177
2171
|
// Lane posture lives with the engine: OPTIMISTIC_DIRTY is only ever set by
|
|
2178
2172
|
// engine-driven paths, and _optimisticNodes is only pushed by
|
|
2179
2173
|
// _optimisticWrite, so the hook is installed whenever either gate holds.
|
|
@@ -2230,12 +2224,11 @@ function recompute(el, create = false) {
|
|
|
2230
2224
|
if (reaskChanged) GlobalQueue._repollVerdicts(el);
|
|
2231
2225
|
} finally {
|
|
2232
2226
|
tracking = prevTracking;
|
|
2227
|
+
latestReadActive = prevLatestRead;
|
|
2233
2228
|
strictRead = prevStrictRead;
|
|
2234
2229
|
if (isStaleEffect) stale = prevStale;
|
|
2235
2230
|
el._flags = REACTIVE_NONE | (create ? el._flags & REACTIVE_SNAPSHOT_STALE : 0);
|
|
2236
2231
|
context = oldcontext;
|
|
2237
|
-
markedReads = affectsReads;
|
|
2238
|
-
affectsReads = prevAffectsReads;
|
|
2239
2232
|
}
|
|
2240
2233
|
if (!el._error) {
|
|
2241
2234
|
trimStaleDeps(el);
|
|
@@ -2305,28 +2298,11 @@ function recompute(el, create = false) {
|
|
|
2305
2298
|
}
|
|
2306
2299
|
}
|
|
2307
2300
|
currentOptimisticLane = prevLane;
|
|
2308
|
-
// Marks read during this recompute re-attach after the commit above:
|
|
2309
|
-
// earlier, the sentinel's NotReadyError in `_error` would have routed the
|
|
2310
|
-
// fresh value into the error-skip branch instead of committing it. A real
|
|
2311
|
-
// (non-NotReady) error wins over marks (#2893): applying the sentinel here
|
|
2312
|
-
// would clobber the user's error with a NotReadyError from a node whose
|
|
2313
|
-
// reads value-transparency promises can never throw NotReady.
|
|
2314
|
-
// `markedReads` only collects under a live mark, so the hook is installed.
|
|
2315
|
-
if (markedReads && !(el._statusFlags & STATUS_ERROR))
|
|
2316
|
-
GlobalQueue._applyAffectsReads(el, markedReads);
|
|
2317
|
-
// Mark-only pending doesn't queue (#2893): the node holds no value needing
|
|
2318
|
-
// a transition-scheduled commit, and queueing would stamp the marking
|
|
2319
|
-
// action's transaction onto it — freezing unrelated writes that share it.
|
|
2320
|
-
// (Single mask test up front keeps the mark-free hot path at original cost.)
|
|
2321
|
-
const pendFlags = el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED);
|
|
2322
2301
|
const needsPendingCommit =
|
|
2323
2302
|
el._pendingValue !== NOT_PENDING ||
|
|
2324
2303
|
el._pendingFirstChild !== null ||
|
|
2325
2304
|
el._pendingDisposal !== null ||
|
|
2326
|
-
(
|
|
2327
|
-
(pendFlags !== STATUS_PENDING ||
|
|
2328
|
-
activeAffectsMarks === 0 ||
|
|
2329
|
-
!GlobalQueue._onlyMarkPending(el)));
|
|
2305
|
+
(el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) !== 0;
|
|
2330
2306
|
// Override-covered holds (hasOverride) always queue: their commit belongs
|
|
2331
2307
|
// to their own transition's schedule (A18 re-rule) and is unobservable
|
|
2332
2308
|
// under the override (A17). Revert no longer commits anything, so an
|
|
@@ -2635,17 +2611,14 @@ function read(el) {
|
|
|
2635
2611
|
!snapshotCaptureActive &&
|
|
2636
2612
|
!strictRead
|
|
2637
2613
|
) {
|
|
2638
|
-
if (c && tracking)
|
|
2639
|
-
link(el, c);
|
|
2640
|
-
// A live mark on a read source flows to the reader (probe reads are
|
|
2641
|
-
// excluded: the probe's own collection owns that verdict, and marking
|
|
2642
|
-
// the enclosing memo would make an `isPending` wrapper itself pending).
|
|
2643
|
-
if (activeAffectsMarks !== 0 && el._affectsCount && !pendingCheckActive)
|
|
2644
|
-
(affectsReads ??= []).push(el);
|
|
2645
|
-
}
|
|
2614
|
+
if (c && tracking) link(el, c);
|
|
2646
2615
|
return !c || el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
|
|
2647
2616
|
}
|
|
2648
|
-
|
|
2617
|
+
// The dev component-body safeguard (#2897) must not fire inside an
|
|
2618
|
+
// isPending() probe: its plain Error would be swallowed by the probe's
|
|
2619
|
+
// catch (which only rethrows NotReadyError), making dev return false where
|
|
2620
|
+
// prod propagates NotReady (#2928). Probe reads follow the prod path.
|
|
2621
|
+
if (strictRead && !pendingCheckActive && owner._statusFlags & STATUS_PENDING)
|
|
2649
2622
|
throwPendingUntrackedRead(strictRead, {
|
|
2650
2623
|
ownerId: c?.id,
|
|
2651
2624
|
ownerName: c?._name,
|
|
@@ -2653,16 +2626,6 @@ function read(el) {
|
|
|
2653
2626
|
});
|
|
2654
2627
|
if (c && tracking) {
|
|
2655
2628
|
link(el, c, pendingCheckActive);
|
|
2656
|
-
// Mark inheritance through derivation (see the fast path above), and its
|
|
2657
|
-
// transitive half (#2893): a mark-pended owner's sentinel sources flow to
|
|
2658
|
-
// the reader like a direct mark — value-transparent reads have no throw
|
|
2659
|
-
// to re-establish through, so without this a mid-window recompute sheds
|
|
2660
|
-
// pendingness permanently past one derivation level.
|
|
2661
|
-
if (activeAffectsMarks !== 0 && !pendingCheckActive) {
|
|
2662
|
-
if (el._affectsCount) (affectsReads ??= []).push(el);
|
|
2663
|
-
if (owner._statusFlags & STATUS_PENDING)
|
|
2664
|
-
GlobalQueue._collectMarkSources(owner, (affectsReads ??= []));
|
|
2665
|
-
}
|
|
2666
2629
|
if (owner._fn) {
|
|
2667
2630
|
const elQueue = queueFor(el);
|
|
2668
2631
|
if (owner._height >= elQueue._min) {
|
|
@@ -2677,16 +2640,7 @@ function read(el) {
|
|
|
2677
2640
|
}
|
|
2678
2641
|
}
|
|
2679
2642
|
}
|
|
2680
|
-
|
|
2681
|
-
// a promise of change, not an absence of value, so a derived node whose only
|
|
2682
|
-
// pending sources are mark sentinels keeps its real value readable —
|
|
2683
|
-
// pendingness reaches readers through verdicts (isPending), not throws.
|
|
2684
|
-
// (`activeAffectsMarks` gates the source scan off the mark-free hot path;
|
|
2685
|
-
// sentinels can't survive in pending sources past their last release.)
|
|
2686
|
-
if (
|
|
2687
|
-
owner._statusFlags & STATUS_PENDING &&
|
|
2688
|
-
!(activeAffectsMarks !== 0 && GlobalQueue._onlyMarkPending(owner))
|
|
2689
|
-
) {
|
|
2643
|
+
if (owner._statusFlags & STATUS_PENDING) {
|
|
2690
2644
|
if (c && !(stale && owner._transition && activeTransition !== owner._transition)) {
|
|
2691
2645
|
if (c && c._config & CONFIG_CHILDREN_FORBIDDEN) {
|
|
2692
2646
|
const message =
|
|
@@ -2743,9 +2697,7 @@ function read(el) {
|
|
|
2743
2697
|
nodeName: owner?._name
|
|
2744
2698
|
});
|
|
2745
2699
|
if (el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING) {
|
|
2746
|
-
//
|
|
2747
|
-
// the value for every reader — that check itself stays right here).
|
|
2748
|
-
if (c && stale && GlobalQueue._readStashed(el)) return el._value;
|
|
2700
|
+
// A17: the override IS the value for every reader.
|
|
2749
2701
|
return unwrapOverride(el._overrideValue);
|
|
2750
2702
|
}
|
|
2751
2703
|
// Entanglement gate: a reader recomputing under an optimistic lane that reads
|
|
@@ -3136,9 +3088,6 @@ function isUndefined(value) {
|
|
|
3136
3088
|
* `_optimisticNodes.length`, `activeLanes.size`), so `!` invocations are safe
|
|
3137
3089
|
* once the gate holds — the same late-binding contract as verdict.ts.
|
|
3138
3090
|
*/
|
|
3139
|
-
// When a background transition is stashed, plain optimistic signals need one
|
|
3140
|
-
// committed-view rerun. Keep that override local to the stash flush.
|
|
3141
|
-
let stashedOptimisticReads = null;
|
|
3142
3091
|
/** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */
|
|
3143
3092
|
function optimisticWrite(el, v) {
|
|
3144
3093
|
const hasOverride = el._overrideValue !== NOT_PENDING;
|
|
@@ -3176,34 +3125,6 @@ function optimisticWrite(el, v) {
|
|
|
3176
3125
|
schedule();
|
|
3177
3126
|
return v;
|
|
3178
3127
|
}
|
|
3179
|
-
function readStashed(node) {
|
|
3180
|
-
return !!stashedOptimisticReads?.has(node);
|
|
3181
|
-
}
|
|
3182
|
-
function queueStashedOptimisticEffects(node) {
|
|
3183
|
-
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
3184
|
-
const sub = s._sub;
|
|
3185
|
-
if (!sub._type) continue;
|
|
3186
|
-
enqueueSub(sub);
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3189
|
-
/**
|
|
3190
|
-
* Incomplete-transition finalization when the stashed transition holds
|
|
3191
|
-
* optimistic nodes: give plain optimistic signals one committed-view rerun.
|
|
3192
|
-
*/
|
|
3193
|
-
function stashOptimistic(stashedTransition) {
|
|
3194
|
-
stashedOptimisticReads = new Set();
|
|
3195
|
-
for (let i = 0; i < stashedTransition._optimisticNodes.length; i++) {
|
|
3196
|
-
const node = stashedTransition._optimisticNodes[i];
|
|
3197
|
-
if (node._fn || node._config & CONFIG_OWNED_WRITE) continue;
|
|
3198
|
-
stashedOptimisticReads.add(node);
|
|
3199
|
-
queueStashedOptimisticEffects(node);
|
|
3200
|
-
}
|
|
3201
|
-
try {
|
|
3202
|
-
finalizePureQueue(null, true);
|
|
3203
|
-
} finally {
|
|
3204
|
-
stashedOptimisticReads = null;
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
3128
|
/**
|
|
3208
3129
|
* transitionComplete's override blockage: a settling transition stays open
|
|
3209
3130
|
* while one of its optimistic nodes holds an active override that is still
|
|
@@ -3216,11 +3137,7 @@ function transitionBlocked(transition) {
|
|
|
3216
3137
|
hasActiveOverride(node) &&
|
|
3217
3138
|
"_statusFlags" in node &&
|
|
3218
3139
|
node._statusFlags & STATUS_PENDING &&
|
|
3219
|
-
node._error instanceof NotReadyError
|
|
3220
|
-
// Mark-sourced pending never blocks settlement: affects() releases AT
|
|
3221
|
-
// settle, so counting its sentinel here would deadlock the window it
|
|
3222
|
-
// is scoped to.
|
|
3223
|
-
!node._error.source?._affectsFor
|
|
3140
|
+
node._error instanceof NotReadyError
|
|
3224
3141
|
) {
|
|
3225
3142
|
return true;
|
|
3226
3143
|
}
|
|
@@ -3382,11 +3299,9 @@ function installOptimisticEngine() {
|
|
|
3382
3299
|
if (GlobalQueue._optimisticWrite !== null) return;
|
|
3383
3300
|
GlobalQueue._optimisticWrite = optimisticWrite;
|
|
3384
3301
|
GlobalQueue._resolveOptimistic = resolveOptimisticNodes;
|
|
3385
|
-
GlobalQueue._stashOptimistic = stashOptimistic;
|
|
3386
3302
|
GlobalQueue._transitionBlocked = transitionBlocked;
|
|
3387
3303
|
GlobalQueue._cleanupLanes = cleanupCompletedLanes;
|
|
3388
3304
|
GlobalQueue._runLaneEffects = runLaneEffects;
|
|
3389
|
-
GlobalQueue._readStashed = readStashed;
|
|
3390
3305
|
GlobalQueue._gatedRead = gatedRead;
|
|
3391
3306
|
GlobalQueue._laneSuspends = laneSuspends;
|
|
3392
3307
|
GlobalQueue._laneReadsCommitted = laneReadsCommitted;
|
|
@@ -3435,6 +3350,46 @@ function collectPendingSources(el) {
|
|
|
3435
3350
|
function witnessAffects(node) {
|
|
3436
3351
|
pendingProbe?.sources.add(node);
|
|
3437
3352
|
}
|
|
3353
|
+
/**
|
|
3354
|
+
* The affects() coverage walk — the read half of the dedicated mark channel.
|
|
3355
|
+
* A node is covered by a live mark iff it carries one (`_affectsCount`) or
|
|
3356
|
+
* derives, through its CURRENT deps (hopping store firewalls), from a node
|
|
3357
|
+
* that does. Pull-based coverage means graph rewires, mid-window recomputes,
|
|
3358
|
+
* and probe-triggered recomputes can never strand or strip a mark — there is
|
|
3359
|
+
* nothing stored downstream to corrupt. Probe-created links
|
|
3360
|
+
* (`_pendingObserver`) are skipped so an `isPending` wrapper memo never
|
|
3361
|
+
* inherits the coverage it reports on.
|
|
3362
|
+
*/
|
|
3363
|
+
function markWalk(el, seen) {
|
|
3364
|
+
if (el._affectsCount) return true;
|
|
3365
|
+
// A real error outranks an inherited mark (A16/A24c): an errored node
|
|
3366
|
+
// answers probes with its error, not a coverage verdict, and coverage does
|
|
3367
|
+
// not flow through it — matching the rails' behavior, where propagation
|
|
3368
|
+
// stopped at errored nodes. A DIRECT mark on an errored node still reads
|
|
3369
|
+
// pending (the count check above), also matching.
|
|
3370
|
+
if (el._statusFlags & STATUS_ERROR) return false;
|
|
3371
|
+
if (seen.has(el)) return false;
|
|
3372
|
+
seen.add(el);
|
|
3373
|
+
const firewall = el._firewall;
|
|
3374
|
+
if (firewall && markWalk(firewall, seen)) return true;
|
|
3375
|
+
// Mid-recompute (the clearStatus companion poke runs before
|
|
3376
|
+
// trimStaleDeps), only the validated prefix [_deps.._depsTail] is this
|
|
3377
|
+
// pass's dependency set — walking past it would read dropped deps and
|
|
3378
|
+
// latch a stale verdict on the companion.
|
|
3379
|
+
const comp = el;
|
|
3380
|
+
const tail = comp._flags & REACTIVE_RECOMPUTING_DEPS ? comp._depsTail : undefined;
|
|
3381
|
+
if (tail !== null) {
|
|
3382
|
+
for (let d = comp._deps ?? null; d !== null; d = d._nextDep) {
|
|
3383
|
+
if (!d._pendingObserver && markWalk(d._dep, seen)) return true;
|
|
3384
|
+
if (d === tail) break;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
return false;
|
|
3388
|
+
}
|
|
3389
|
+
/** Gated entry: apps with no live mark pay one integer compare. */
|
|
3390
|
+
function markCovered(el) {
|
|
3391
|
+
return activeAffectsMarks !== 0 && markWalk(el, new Set());
|
|
3392
|
+
}
|
|
3438
3393
|
function quietPending(el) {
|
|
3439
3394
|
if (el._pendingSources) {
|
|
3440
3395
|
for (const source of el._pendingSources) if (!source._reask) return false;
|
|
@@ -3452,11 +3407,13 @@ function newQuestionInFlight(comp) {
|
|
|
3452
3407
|
function computePendingState(el) {
|
|
3453
3408
|
const comp = el;
|
|
3454
3409
|
if (comp._flags & REACTIVE_DISPOSED) return false;
|
|
3455
|
-
|
|
3410
|
+
// Mark coverage is transitive by dep-graph reachability: a latest() shadow
|
|
3411
|
+
// reaches its owner (and a store leaf its firewall) through its own deps,
|
|
3412
|
+
// so the one walk covers direct marks, derivation, and companion chains.
|
|
3413
|
+
if (markCovered(el)) return true;
|
|
3456
3414
|
const firewall = el._firewall;
|
|
3457
3415
|
if (el._parentSource) {
|
|
3458
3416
|
const parentNode = el._parentSource;
|
|
3459
|
-
if (parentNode._affectsCount) return true;
|
|
3460
3417
|
const parent = parentNode._firewall || parentNode;
|
|
3461
3418
|
return newQuestionInFlight(parent);
|
|
3462
3419
|
}
|
|
@@ -3489,12 +3446,22 @@ function updateChildCompanions(el) {
|
|
|
3489
3446
|
if (child._pendingSignal || child._latestValueComputed) updatePendingSignal(child);
|
|
3490
3447
|
}
|
|
3491
3448
|
}
|
|
3492
|
-
|
|
3449
|
+
/**
|
|
3450
|
+
* Re-derive every verdict companion downstream of `el` (subs + firewall
|
|
3451
|
+
* children, dedup'd). The affects() channel's poke walk: registration and
|
|
3452
|
+
* re-ask flips use the live write path (companion setSignal — its own lane
|
|
3453
|
+
* lets the wake escape an incomplete transition's effect stash, #2887);
|
|
3454
|
+
* mark release passes `snap` because it runs inside queue finalization,
|
|
3455
|
+
* where companion writes must land committed (a setSignal there would open
|
|
3456
|
+
* a fresh override window that nothing settles).
|
|
3457
|
+
*/
|
|
3458
|
+
function repollDownstreamVerdicts(el, snap = false) {
|
|
3459
|
+
const update = snap ? snapCompanionsToState : updatePendingSignal;
|
|
3493
3460
|
const visited = new Set();
|
|
3494
3461
|
const visit = node => {
|
|
3495
3462
|
if (visited.has(node)) return;
|
|
3496
3463
|
visited.add(node);
|
|
3497
|
-
if (node._pendingSignal || node._latestValueComputed)
|
|
3464
|
+
if (node._pendingSignal || node._latestValueComputed) update(node);
|
|
3498
3465
|
for (let s = node._subs; s !== null; s = s._nextSub) visit(s._sub);
|
|
3499
3466
|
for (let child = node._child ?? null; child !== null; child = child._nextChild) {
|
|
3500
3467
|
visit(child);
|
|
@@ -3557,6 +3524,19 @@ function latestRead(el) {
|
|
|
3557
3524
|
: el._value;
|
|
3558
3525
|
let value;
|
|
3559
3526
|
try {
|
|
3527
|
+
// An untracked latest() read has no reading context, so read() never
|
|
3528
|
+
// performs its mid-tick pull — a plain write queued between two latest()
|
|
3529
|
+
// calls left a still-subscribed shadow at its previous speculative value
|
|
3530
|
+
// until the flush (#2922). Mirror the tracked-read pull here: mark the
|
|
3531
|
+
// queued staleness through the graph, then bring the shadow up to date.
|
|
3532
|
+
const queue = queueFor(pendingComputed);
|
|
3533
|
+
if (
|
|
3534
|
+
pendingComputed._height >= queue._min &&
|
|
3535
|
+
!(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
|
|
3536
|
+
) {
|
|
3537
|
+
markHeap(queue);
|
|
3538
|
+
prepareComputed(pendingComputed, true);
|
|
3539
|
+
}
|
|
3560
3540
|
value = read(pendingComputed);
|
|
3561
3541
|
} catch (e) {
|
|
3562
3542
|
if (e instanceof NotReadyError && (!context || !(el._statusFlags & STATUS_UNINITIALIZED)))
|
|
@@ -3573,6 +3553,16 @@ function latestRead(el) {
|
|
|
3573
3553
|
return visibleValue;
|
|
3574
3554
|
}
|
|
3575
3555
|
}
|
|
3556
|
+
// A shadow recomputed by the pull above (not at creation) holds its fresh
|
|
3557
|
+
// speculative value in _pendingValue; a contextless read() only surfaces
|
|
3558
|
+
// _value. Overrides stay authoritative (A17), and stale readers keep the
|
|
3559
|
+
// other transition's committed view, matching read()'s own selection.
|
|
3560
|
+
if (
|
|
3561
|
+
pendingComputed._pendingValue !== NOT_PENDING &&
|
|
3562
|
+
!hasActiveOverride(pendingComputed) &&
|
|
3563
|
+
!(stale && pendingComputed._transition && activeTransition !== pendingComputed._transition)
|
|
3564
|
+
)
|
|
3565
|
+
return pendingComputed._pendingValue;
|
|
3576
3566
|
return value;
|
|
3577
3567
|
}
|
|
3578
3568
|
/** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
|
|
@@ -3882,6 +3872,22 @@ function restoreTransition(transition, fn) {
|
|
|
3882
3872
|
return result;
|
|
3883
3873
|
}
|
|
3884
3874
|
/**
|
|
3875
|
+
* The primitive for mutations: imperative async workflows whose *writes span
|
|
3876
|
+
* an async gap* — optimistic write, server round-trip, reconciling write —
|
|
3877
|
+
* where intermediate state must not leak and failure must revert cleanly
|
|
3878
|
+
* (pair with `createOptimistic` / `createOptimisticStore`).
|
|
3879
|
+
*
|
|
3880
|
+
* Navigation-shaped updates do not need an action. A plain setter call is
|
|
3881
|
+
* enough: reads pull the async, and downstream async computeds hold their
|
|
3882
|
+
* previous values per-node until the new ones are ready (`isPending` /
|
|
3883
|
+
* `latest` expose the in-flight state). Reach for `action` only when writes
|
|
3884
|
+
* happen *after* async work, not merely upstream of it.
|
|
3885
|
+
*
|
|
3886
|
+
* Framework-level actions (router form actions, server actions) are
|
|
3887
|
+
* specializations of this primitive: they are actions in exactly this sense —
|
|
3888
|
+
* the same transactional semantics — with form binding, serialization, and
|
|
3889
|
+
* submission tracking layered on top. The shared name is deliberate.
|
|
3890
|
+
*
|
|
3885
3891
|
* Wraps a generator function so each invocation runs as a single transaction
|
|
3886
3892
|
* (a "transition") that batches every signal/store write between yields. The
|
|
3887
3893
|
* surrounding UI sees one atomic update per yielded step; nothing is committed
|
|
@@ -3980,11 +3986,33 @@ function action(genFn) {
|
|
|
3980
3986
|
};
|
|
3981
3987
|
const run = r => {
|
|
3982
3988
|
if (r.done) return done(r.value);
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3989
|
+
// Thenable assimilation can itself throw synchronously (a `then`
|
|
3990
|
+
// getter, or a `then()` method that throws — #2918). Match `await`
|
|
3991
|
+
// semantics: the failure is thrown back into the generator at the
|
|
3992
|
+
// yield point (catchable there); if uncaught, step()'s guard settles
|
|
3993
|
+
// the action so its iterator never leaks in the transition. The
|
|
3994
|
+
// settled flag implements A+ 2.3.3.3.4.1: a throw after the thenable
|
|
3995
|
+
// already called a callback is ignored.
|
|
3996
|
+
let settled = false;
|
|
3997
|
+
try {
|
|
3998
|
+
if (isThenable(r.value))
|
|
3999
|
+
return void r.value.then(
|
|
4000
|
+
v => {
|
|
4001
|
+
if (settled) return;
|
|
4002
|
+
settled = true;
|
|
4003
|
+
restoreTransition(ctx, () => step(v));
|
|
4004
|
+
},
|
|
4005
|
+
e => {
|
|
4006
|
+
if (settled) return;
|
|
4007
|
+
settled = true;
|
|
4008
|
+
restoreTransition(ctx, () => step(e, true));
|
|
4009
|
+
}
|
|
4010
|
+
);
|
|
4011
|
+
} catch (e) {
|
|
4012
|
+
if (settled) return;
|
|
4013
|
+
settled = true;
|
|
4014
|
+
return void restoreTransition(ctx, () => step(e, true));
|
|
4015
|
+
}
|
|
3988
4016
|
restoreTransition(ctx, () => step(r.value));
|
|
3989
4017
|
};
|
|
3990
4018
|
step();
|
|
@@ -4260,6 +4288,12 @@ function createReaction(effectFn, options) {
|
|
|
4260
4288
|
});
|
|
4261
4289
|
};
|
|
4262
4290
|
}
|
|
4291
|
+
/** Delivers effect applies on a microtask instead of queueing them (#2930). */
|
|
4292
|
+
class MicrotaskQueue extends Queue {
|
|
4293
|
+
enqueue(type, fn) {
|
|
4294
|
+
queueMicrotask(() => fn(type));
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4263
4297
|
/**
|
|
4264
4298
|
* Awaits a reactive expression and returns its first fully-settled value as a
|
|
4265
4299
|
* `Promise`. Pending async reads (`createMemo` returning a promise, etc.) are
|
|
@@ -4289,6 +4323,17 @@ function resolve(fn) {
|
|
|
4289
4323
|
}
|
|
4290
4324
|
return new Promise((res, rej) => {
|
|
4291
4325
|
createRoot(dispose => {
|
|
4326
|
+
// Deliver effect applies on a microtask instead of the owner queue: an
|
|
4327
|
+
// incomplete transition stashes its effect queues until it settles, but
|
|
4328
|
+
// an action yielding this promise is itself what keeps the transition
|
|
4329
|
+
// open — the stashed res() deadlocked the action (#2930). The compute
|
|
4330
|
+
// still runs in place (under the transaction's view when created inside
|
|
4331
|
+
// an action step), and status/boundary notifications keep their normal
|
|
4332
|
+
// route through the inherited queue.
|
|
4333
|
+
const owner = getOwner();
|
|
4334
|
+
const queue = new MicrotaskQueue();
|
|
4335
|
+
queue._parent = owner._queue; // notify() forwards up the normal chain
|
|
4336
|
+
owner._queue = queue;
|
|
4292
4337
|
// A user effect rather than a bare computed: computeds are pull-based and
|
|
4293
4338
|
// are only re-enqueued when a pending source *resolves* — a rejection just
|
|
4294
4339
|
// marks them errored, so nothing would re-run and the promise would never
|
|
@@ -6027,127 +6072,76 @@ function createStore(first, second, options) {
|
|
|
6027
6072
|
}
|
|
6028
6073
|
|
|
6029
6074
|
/**
|
|
6030
|
-
* The
|
|
6031
|
-
*
|
|
6032
|
-
*
|
|
6033
|
-
*
|
|
6034
|
-
*
|
|
6035
|
-
*
|
|
6036
|
-
* - `_reask` is permanently `false`: a mark is by definition a declared
|
|
6037
|
-
* value change, so `quietPending` never silences a window it participates
|
|
6038
|
-
* in — even when the mark rides over an otherwise-quiet `refresh()`
|
|
6039
|
-
* re-ask of the same node (the whole point of declaring one).
|
|
6040
|
-
* - A landing on the marked node settles only the node's OWN source entry;
|
|
6041
|
-
* the sentinel entry survives until the mark's transaction releases it.
|
|
6042
|
-
* - The sentinel itself never carries `STATUS_PENDING`, so
|
|
6043
|
-
* `transitionComplete` never counts a mark as a blocker of its own
|
|
6044
|
-
* transaction (release happens AT settle — self-blocking would deadlock),
|
|
6045
|
-
* and reads of the marked node never throw (marks are value-transparent
|
|
6046
|
-
* at the source; pendingness is what propagates).
|
|
6075
|
+
* The counting half of a mark, shared by direct registration and store-scope
|
|
6076
|
+
* inheritance (a node created inside a live keyless mark's identity scope).
|
|
6077
|
+
* A mark is ONLY this count: coverage of everything derived from the node is
|
|
6078
|
+
* pull-derived by the verdict layer's `markWalk` (dep-graph reachability), so
|
|
6079
|
+
* nothing is stored downstream and nothing can be stranded or stripped by
|
|
6080
|
+
* mid-window recomputes.
|
|
6047
6081
|
*/
|
|
6048
|
-
function
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
// Brand + backref: lets the scheduler's settlement checks recognize
|
|
6052
|
-
// mark-sourced pending (which must never block its own transaction —
|
|
6053
|
-
// release happens AT settle).
|
|
6054
|
-
_affectsFor: node,
|
|
6055
|
-
_flags: 0,
|
|
6056
|
-
_statusFlags: 0,
|
|
6057
|
-
_reask: false,
|
|
6058
|
-
_error: undefined,
|
|
6059
|
-
_subs: null,
|
|
6060
|
-
_deps: null
|
|
6061
|
-
});
|
|
6082
|
+
function markAffects(node) {
|
|
6083
|
+
node._affectsCount = (node._affectsCount || 0) + 1;
|
|
6084
|
+
shiftAffectsMarks(1);
|
|
6062
6085
|
}
|
|
6063
6086
|
/**
|
|
6064
|
-
*
|
|
6065
|
-
*
|
|
6066
|
-
*
|
|
6067
|
-
*
|
|
6068
|
-
*
|
|
6069
|
-
* `
|
|
6070
|
-
* re-
|
|
6071
|
-
*
|
|
6072
|
-
*
|
|
6073
|
-
*
|
|
6074
|
-
* that merely share a downstream memo) would be captured and frozen until the
|
|
6075
|
-
* action settles.
|
|
6087
|
+
* Boundary visual channel: within a transaction, a live mark holds Loading
|
|
6088
|
+
* fallbacks / reveal ordering the way real in-flight async would — but the
|
|
6089
|
+
* notification is tagged visibility-only at the source (`_markVisual`), so
|
|
6090
|
+
* the root queue never registers reporters from it: marks are invisible to
|
|
6091
|
+
* ALL completion and settlement accounting by construction. Boundaries hold
|
|
6092
|
+
* the marked node in `_sources` while `_affectsCount` is live; the release
|
|
6093
|
+
* sweep (finalizePureQueue re-checks boundary children after mark release)
|
|
6094
|
+
* is the display-state update point. Ambient marks release inside the same
|
|
6095
|
+
* flush that would surface them, before effects run — verdict-only, netting
|
|
6096
|
+
* no visual change.
|
|
6076
6097
|
*/
|
|
6077
|
-
function
|
|
6098
|
+
function notifyMarkBoundaries(node) {
|
|
6078
6099
|
if (!node._subs && !node._child) return;
|
|
6079
|
-
const
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
* any sentinel entries it held). Called by `recompute` after the commit —
|
|
6091
|
-
* not before, because setting `_error` earlier would make the commit path
|
|
6092
|
-
* treat the node as errored and skip the value write.
|
|
6093
|
-
*/
|
|
6094
|
-
function applyAffectsReads(el, sources) {
|
|
6095
|
-
let applied = false;
|
|
6096
|
-
for (let i = 0; i < sources.length; i++) {
|
|
6097
|
-
const src = sources[i];
|
|
6098
|
-
if (!src._affectsCount) continue;
|
|
6099
|
-
const sentinel = getAffectsSentinel(src);
|
|
6100
|
-
if (addPendingSource(el, sentinel)) {
|
|
6101
|
-
el._statusFlags |= STATUS_PENDING;
|
|
6102
|
-
setPendingError(el, sentinel);
|
|
6103
|
-
applied = true;
|
|
6100
|
+
const error = new NotReadyError(node);
|
|
6101
|
+
error._markVisual = true;
|
|
6102
|
+
const visited = new Set();
|
|
6103
|
+
const visit = sub => {
|
|
6104
|
+
if (visited.has(sub)) return;
|
|
6105
|
+
visited.add(sub);
|
|
6106
|
+
// Display consumers (render effects, boundary computeds) act on the
|
|
6107
|
+
// notification; descent stops there, exactly like the status rails.
|
|
6108
|
+
if (sub._notifyStatus) {
|
|
6109
|
+
sub._notifyStatus(STATUS_PENDING, error);
|
|
6110
|
+
return;
|
|
6104
6111
|
}
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
/**
|
|
6109
|
-
* The counting half of a mark, shared by direct registration and store-scope
|
|
6110
|
-
* inheritance (a node created inside a live keyless mark's identity scope):
|
|
6111
|
-
* bumps the refcount and pokes the node's verdict companions so an
|
|
6112
|
-
* already-materialized `false` flips reactively.
|
|
6113
|
-
*/
|
|
6114
|
-
function markAffects(node) {
|
|
6115
|
-
node._affectsCount = (node._affectsCount || 0) + 1;
|
|
6116
|
-
shiftAffectsMarks(1);
|
|
6117
|
-
// Companions only exist once the verdict layer (isPending/latest) loaded;
|
|
6118
|
-
// without them there is no materialized verdict to flip.
|
|
6119
|
-
if (node._affectsCount === 1 && GlobalQueue._updatePendingSignal !== null)
|
|
6120
|
-
GlobalQueue._updatePendingSignal(node);
|
|
6112
|
+
forEachDependent(sub, visit);
|
|
6113
|
+
};
|
|
6114
|
+
forEachDependent(node, visit);
|
|
6121
6115
|
}
|
|
6122
6116
|
/**
|
|
6123
6117
|
* Registers one `affects()` mark on a node: counts it, records the
|
|
6124
6118
|
* registration with the current transaction (after initTransition the queue's
|
|
6125
|
-
* batch IS the active transition, mirroring `_optimisticNodes`),
|
|
6126
|
-
*
|
|
6127
|
-
*
|
|
6128
|
-
* registration (not just the first): subscribers gained since an
|
|
6129
|
-
* overlapping registration get covered, and dedup stops re-descent
|
|
6119
|
+
* batch IS the active transition, mirroring `_optimisticNodes`), re-derives
|
|
6120
|
+
* every downstream verdict companion (the mark channel's only push — verdict
|
|
6121
|
+
* pokes, not state), and notifies boundary display state. Both walks run on
|
|
6122
|
+
* every registration (not just the first): subscribers gained since an
|
|
6123
|
+
* earlier overlapping registration get covered, and dedup stops re-descent.
|
|
6130
6124
|
*/
|
|
6131
6125
|
function registerAffectsMark(node) {
|
|
6132
6126
|
markAffects(node);
|
|
6133
6127
|
globalQueue._batch._affectsNodes.push(node);
|
|
6134
|
-
|
|
6128
|
+
// Companions only exist once the verdict layer (isPending/latest) loaded;
|
|
6129
|
+
// without them there is no materialized verdict to poke.
|
|
6130
|
+
GlobalQueue._repollVerdicts !== null && GlobalQueue._repollVerdicts(node);
|
|
6131
|
+
notifyMarkBoundaries(node);
|
|
6135
6132
|
schedule();
|
|
6136
6133
|
}
|
|
6137
6134
|
/**
|
|
6138
|
-
* Releases one registration. When the node's last mark drops,
|
|
6139
|
-
*
|
|
6140
|
-
*
|
|
6141
|
-
*
|
|
6142
|
-
* can't open a fresh override window that would itself need settlement.
|
|
6135
|
+
* Releases one registration. When the node's last mark drops, re-derives
|
|
6136
|
+
* every downstream verdict through the settlement snap (committed, not
|
|
6137
|
+
* transition-scoped — release runs inside queue finalization, where a
|
|
6138
|
+
* setSignal would open a fresh override window that nothing settles).
|
|
6143
6139
|
*/
|
|
6144
6140
|
function releaseAffectsMark(node) {
|
|
6145
6141
|
shiftAffectsMarks(-1);
|
|
6146
6142
|
node._affectsCount--;
|
|
6147
6143
|
if (!node._affectsCount) {
|
|
6148
|
-
|
|
6149
|
-
if (sentinel) settlePendingSource(node, sentinel, true);
|
|
6150
|
-
GlobalQueue._snapCompanions !== null && GlobalQueue._snapCompanions(node);
|
|
6144
|
+
GlobalQueue._repollVerdicts !== null && GlobalQueue._repollVerdicts(node, true);
|
|
6151
6145
|
GlobalQueue._releaseAffectsScope?.(node);
|
|
6152
6146
|
}
|
|
6153
6147
|
}
|
|
@@ -6159,53 +6153,14 @@ function releaseAffectsMarks(nodes) {
|
|
|
6159
6153
|
for (let i = 0; i < nodes.length; i++) releaseAffectsMark(nodes[i]);
|
|
6160
6154
|
nodes.length = 0;
|
|
6161
6155
|
}
|
|
6162
|
-
/**
|
|
6163
|
-
* True when a node's pending status comes ONLY from affects() sentinels. A
|
|
6164
|
-
* mark is a promise of change, not an absence of value: reads of mark-pended
|
|
6165
|
-
* derived nodes stay value-transparent (verdicts report pending; the read
|
|
6166
|
-
* path must not suspend). Any real async source among the pending sources
|
|
6167
|
-
* keeps normal suspension semantics. Core's read path reaches this through
|
|
6168
|
-
* `GlobalQueue._onlyMarkPending`, gated on the `activeAffectsMarks` counter.
|
|
6169
|
-
*/
|
|
6170
|
-
function onlyMarkPending(el) {
|
|
6171
|
-
const sources = el._pendingSources;
|
|
6172
|
-
if (sources) {
|
|
6173
|
-
for (const s of sources) if (!s._affectsFor) return false;
|
|
6174
|
-
return true;
|
|
6175
|
-
}
|
|
6176
|
-
return false;
|
|
6177
|
-
}
|
|
6178
|
-
/**
|
|
6179
|
-
* Collect the still-live marked nodes behind a pended owner's sentinel
|
|
6180
|
-
* sources into a recompute's `affectsReads`. This is the transitive half of
|
|
6181
|
-
* read-path re-establishment (#2893): real async re-establishes at every
|
|
6182
|
-
* derivation level because its re-throw on read re-registers the source, but
|
|
6183
|
-
* mark-pended reads are value-transparent — without this, pendingness dies on
|
|
6184
|
-
* the first mid-window recompute past depth one, and the isPending() probe
|
|
6185
|
-
* itself (whose prepare step recomputes retryable NotReady holders) strips
|
|
6186
|
-
* the very status it reports on. Reached through
|
|
6187
|
-
* `GlobalQueue._collectMarkSources`, gated on `activeAffectsMarks`.
|
|
6188
|
-
*/
|
|
6189
|
-
function collectMarkSources(el, into) {
|
|
6190
|
-
if (el._pendingSources) {
|
|
6191
|
-
for (const s of el._pendingSources) {
|
|
6192
|
-
const marked = s._affectsFor;
|
|
6193
|
-
if (marked && marked._affectsCount) into.push(marked);
|
|
6194
|
-
}
|
|
6195
|
-
}
|
|
6196
|
-
}
|
|
6197
6156
|
// Late installation (same pattern as `GlobalQueue._update`): the mark engine
|
|
6198
6157
|
// lives with the feature so graphs that never declare a mark never ship it.
|
|
6199
|
-
// Each call site is gated by state only this module creates (
|
|
6200
|
-
//
|
|
6201
|
-
//
|
|
6202
|
-
// before the first time any of them can fire.
|
|
6203
|
-
GlobalQueue._applyAffectsReads = applyAffectsReads;
|
|
6158
|
+
// Each call site is gated by state only this module creates (a non-empty
|
|
6159
|
+
// `_affectsNodes` batch, a live scope in the store's `affectsScopes`), so the
|
|
6160
|
+
// hooks are installed before the first time any of them can fire.
|
|
6204
6161
|
GlobalQueue._releaseAffectsMarks = releaseAffectsMarks;
|
|
6205
6162
|
GlobalQueue._markAffects = markAffects;
|
|
6206
6163
|
GlobalQueue._releaseAffectsMark = releaseAffectsMark;
|
|
6207
|
-
GlobalQueue._onlyMarkPending = onlyMarkPending;
|
|
6208
|
-
GlobalQueue._collectMarkSources = collectMarkSources;
|
|
6209
6164
|
function affects(target, key) {
|
|
6210
6165
|
if (arguments.length > 2) {
|
|
6211
6166
|
const message =
|
|
@@ -7436,9 +7391,14 @@ class CollectionQueue extends Queue {
|
|
|
7436
7391
|
}
|
|
7437
7392
|
_checkSources() {
|
|
7438
7393
|
for (const source of this._sources) {
|
|
7394
|
+
// A source with a live affects() mark holds display state for the
|
|
7395
|
+
// mark's lifetime (the visual channel): the marked node carries no
|
|
7396
|
+
// status of its own, so the count is the liveness test. The release
|
|
7397
|
+
// sweep (finalizePureQueue after mark release) re-runs this check.
|
|
7439
7398
|
if (
|
|
7440
7399
|
source._flags & REACTIVE_DISPOSED ||
|
|
7441
|
-
(!
|
|
7400
|
+
(!source._affectsCount &&
|
|
7401
|
+
!(source._statusFlags & this._collectionType) &&
|
|
7442
7402
|
!(this._collectionType & STATUS_ERROR && source._statusFlags & STATUS_PENDING))
|
|
7443
7403
|
)
|
|
7444
7404
|
this._sources.delete(source);
|