@solidjs/signals 2.0.0-rc.1 → 2.0.0-rc.11
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-shared.js +7155 -0
- package/dist/dev.attribution.js +3307 -0
- package/dist/dev.js +5765 -4496
- package/dist/observe/affects.js +129 -0
- package/dist/observe/attribution.js +7 -0
- package/dist/observe/boundaries.js +793 -0
- package/dist/observe/core/action.js +177 -0
- package/dist/observe/core/async.js +849 -0
- package/dist/observe/core/attribution-costs.js +66 -0
- package/dist/observe/core/attribution-feedback.js +280 -0
- package/dist/observe/core/attribution-hooks.js +83 -0
- package/dist/observe/core/attribution-queries.js +43 -0
- package/dist/observe/core/attribution.js +2904 -0
- package/dist/observe/core/constants.js +283 -0
- package/dist/{types-cjs/core/context.d.cts → observe/core/context.js} +37 -12
- package/dist/observe/core/core.js +1999 -0
- package/dist/observe/core/dev.js +343 -0
- package/dist/observe/core/effect.js +200 -0
- package/dist/observe/core/error-hooks.js +71 -0
- package/dist/observe/core/error.js +80 -0
- package/dist/observe/core/external.js +98 -0
- package/dist/observe/core/graph.js +155 -0
- package/dist/observe/core/heap.js +147 -0
- package/dist/observe/core/invariants.js +45 -0
- package/dist/observe/core/lanes.js +205 -0
- package/dist/observe/core/optimistic.js +669 -0
- package/dist/observe/core/owner.js +353 -0
- package/dist/observe/core/scheduler.js +1561 -0
- package/dist/observe/core/verdict.js +674 -0
- package/dist/observe/index.js +59 -0
- package/dist/observe/map.js +507 -0
- package/dist/observe/signals.js +731 -0
- package/dist/observe/store/index.js +44 -0
- package/dist/observe/store/next/optimistic.js +717 -0
- package/dist/observe/store/next/projection.js +266 -0
- package/dist/observe/store/next/reconcile.js +375 -0
- package/dist/observe/store/next/store.js +2480 -0
- package/dist/observe/store/next/target.js +51 -0
- package/dist/observe/store/store.js +331 -0
- package/dist/observe/store/storePath.js +103 -0
- package/dist/observe/store/utils.js +1070 -0
- package/dist/prod/affects.js +10 -9
- package/dist/prod/attribution.js +45 -0
- package/dist/prod/boundaries.js +388 -178
- package/dist/prod/core/action.js +65 -37
- package/dist/prod/core/async.js +378 -163
- package/dist/prod/core/attribution-hooks.js +3 -0
- package/dist/prod/core/constants.js +186 -1
- package/dist/prod/core/context.js +10 -16
- package/dist/prod/core/core.js +1546 -416
- package/dist/prod/core/dev.js +18 -1
- package/dist/prod/core/effect.js +119 -79
- package/dist/prod/core/error-hooks.js +71 -0
- package/dist/prod/core/error.js +13 -1
- package/dist/prod/core/external.js +4 -4
- package/dist/prod/core/graph.js +105 -54
- package/dist/prod/core/heap.js +72 -65
- package/dist/prod/core/invariants.js +3 -2
- package/dist/prod/core/lanes.js +117 -52
- package/dist/prod/core/optimistic.js +500 -104
- package/dist/prod/core/owner.js +131 -84
- package/dist/prod/core/scheduler.js +1012 -241
- package/dist/prod/core/verdict.js +417 -112
- package/dist/prod/index.js +20 -8
- package/dist/prod/map.js +355 -171
- package/dist/prod/signals.js +363 -32
- package/dist/prod/store/index.js +2 -0
- package/dist/prod/store/next/optimistic.js +530 -197
- package/dist/prod/store/next/projection.js +142 -57
- package/dist/prod/store/next/reconcile.js +149 -103
- package/dist/prod/store/next/store.js +1470 -346
- package/dist/prod/store/next/target.js +40 -9
- package/dist/prod/store/store.js +23 -19
- package/dist/prod/store/utils.js +997 -130
- package/dist/types/attribution.d.ts +19 -0
- package/dist/types/attribution.prod.d.ts +22 -0
- package/dist/types/boundaries.d.ts +78 -5
- package/dist/types/core/action.d.ts +12 -5
- package/dist/types/core/async.d.ts +3 -1
- package/dist/types/core/attribution-costs.d.ts +35 -0
- package/dist/types/core/attribution-feedback.d.ts +133 -0
- package/dist/types/core/attribution-hooks.d.ts +304 -0
- package/dist/types/core/attribution-queries.d.ts +24 -0
- package/dist/types/core/attribution.d.ts +892 -0
- package/dist/types/core/constants.d.ts +179 -0
- package/dist/types/core/core.d.ts +299 -25
- package/dist/types/core/dev.d.ts +421 -8
- package/dist/types/core/effect.d.ts +8 -1
- package/dist/types/core/error-hooks.d.ts +71 -0
- package/dist/types/core/error.d.ts +9 -0
- package/dist/types/core/graph.d.ts +23 -0
- package/dist/types/core/heap.d.ts +5 -3
- package/dist/types/core/index.d.ts +6 -3
- package/dist/types/core/invariants.d.ts +5 -1
- package/dist/types/core/lanes.d.ts +51 -19
- package/dist/types/core/owner.d.ts +15 -3
- package/dist/types/core/scheduler.d.ts +252 -1
- package/dist/types/core/types.d.ts +124 -41
- package/dist/types/index.d.ts +18 -5
- package/dist/types/map.d.ts +2 -0
- package/dist/types/signals.d.ts +145 -13
- package/dist/types/store/index.d.ts +7 -6
- package/dist/types/store/next/optimistic.d.ts +18 -13
- package/dist/types/store/next/projection.d.ts +5 -2
- package/dist/types/store/next/reconcile.d.ts +7 -0
- package/dist/types/store/next/store.d.ts +83 -11
- package/dist/types/store/next/target.d.ts +108 -11
- package/dist/types/store/store.d.ts +20 -11
- package/dist/types/store/utils.d.ts +188 -0
- package/package.json +31 -26
- package/dist/node.cjs +0 -8137
- package/dist/types/store/optimistic.d.ts +0 -45
- package/dist/types/store/projection.d.ts +0 -70
- package/dist/types/store/reconcile.d.ts +0 -46
- package/dist/types-cjs/affects.d.cts +0 -47
- package/dist/types-cjs/boundaries.d.cts +0 -175
- package/dist/types-cjs/core/action.d.cts +0 -64
- package/dist/types-cjs/core/async.d.cts +0 -21
- package/dist/types-cjs/core/constants.d.cts +0 -71
- package/dist/types-cjs/core/core.d.cts +0 -156
- package/dist/types-cjs/core/dev.d.cts +0 -64
- package/dist/types-cjs/core/effect.d.cts +0 -30
- package/dist/types-cjs/core/error.d.cts +0 -47
- package/dist/types-cjs/core/external.d.cts +0 -15
- package/dist/types-cjs/core/graph.d.cts +0 -5
- package/dist/types-cjs/core/heap.d.cts +0 -22
- package/dist/types-cjs/core/index.d.cts +0 -13
- package/dist/types-cjs/core/invariants.d.cts +0 -59
- package/dist/types-cjs/core/lanes.d.cts +0 -56
- package/dist/types-cjs/core/optimistic.d.cts +0 -6
- package/dist/types-cjs/core/owner.d.cts +0 -124
- package/dist/types-cjs/core/scheduler.d.cts +0 -172
- package/dist/types-cjs/core/types.d.cts +0 -148
- package/dist/types-cjs/core/verdict.d.cts +0 -2
- package/dist/types-cjs/index.d.cts +0 -10
- package/dist/types-cjs/map.d.cts +0 -69
- package/dist/types-cjs/package.json +0 -3
- package/dist/types-cjs/signals.d.cts +0 -624
- package/dist/types-cjs/store/index.d.cts +0 -19
- package/dist/types-cjs/store/next/optimistic.d.cts +0 -20
- package/dist/types-cjs/store/next/projection.d.cts +0 -24
- package/dist/types-cjs/store/next/reconcile.d.cts +0 -3
- package/dist/types-cjs/store/next/store.d.cts +0 -71
- package/dist/types-cjs/store/next/target.d.cts +0 -99
- package/dist/types-cjs/store/optimistic.d.cts +0 -45
- package/dist/types-cjs/store/projection.d.cts +0 -70
- package/dist/types-cjs/store/reconcile.d.cts +0 -46
- package/dist/types-cjs/store/store.d.cts +0 -138
- package/dist/types-cjs/store/storePath.d.cts +0 -58
- package/dist/types-cjs/store/utils.d.cts +0 -74
package/dist/prod/core/core.js
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
|
-
import { handleAsync, clearStatus, parkLoadingWindow, notifyStatus, settleErroredDependents } from "./async.js";
|
|
1
|
+
import { releaseFlightTeardown, handleAsync, clearStatus, parkLoadingWindow, notifyStatus, settlePendingSource, settleErroredDependents } from "./async.js";
|
|
2
2
|
|
|
3
|
-
import { EFFECT_TRACKED, REACTIVE_OPTIMISTIC_DIRTY,
|
|
3
|
+
import { NOT_PENDING, EFFECT_TRACKED, EFFECT_USER, REACTIVE_OPTIMISTIC_DIRTY, CONFIG_DERIVED_OVERRIDE, CONFIG_HELD_CHILDREN, CONFIG_LANE_FRAME, CONFIG_OPTIMISTIC, STATUS_UNINITIALIZED, STATUS_ERROR, STATUS_PENDING, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, REACTIVE_ZOMBIE, CONFIG_SYNC, CONFIG_HAS_LANE, REACTIVE_MISSED_WAKE, REACTIVE_DISPOSED, REACTIVE_SNAPSHOT_STALE, unwrapOverride, CONFIG_DIRECT_COMMIT, CONFIG_HAS_COMPANIONS, CONFIG_PROMOTED, CONFIG_ADOPTED_UNFLUSHED, CONFIG_CHILDREN_FORBIDDEN, CONFIG_FRESH_READ, CONFIG_INPUTS_PUBLISHED, CONFIG_AUTO_DISPOSE, REACTIVE_LAZY, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_MANUAL_WRITE, CONFIG_AUTHORITATIVE_READ, CONFIG_OVERRIDE_SUPERSEDED, CONFIG_AUTHORITATIVE_OBSERVED, defaultContext, REACTIVE_NONE, CONFIG_HELD_TRUTH, CONFIG_TRANSPARENT, CONFIG_OWNED_WRITE, CONFIG_NO_SNAPSHOT, CONFIG_IN_SNAPSHOT_SCOPE, NO_SNAPSHOT, CONFIG_HAS_SNAPSHOT, CONFIG_FW_CHILDREN, CONFIG_SLOT_NODE, STORE_SNAPSHOT_PROPS } from "./constants.js";
|
|
4
4
|
|
|
5
5
|
import { NotReadyError } from "./error.js";
|
|
6
6
|
|
|
7
|
-
import { trimStaleDeps, link,
|
|
7
|
+
import { clearDeps, trimStaleDeps, link, dormantNodes } from "./graph.js";
|
|
8
8
|
|
|
9
|
-
import { deleteFromHeap, queueFor, insertIntoHeapHeight, markNode, markHeap, insertIntoHeap } from "./heap.js";
|
|
9
|
+
import { deleteFromHeap, queueFor, insertIntoHeapHeight, enqueueSub, markNode, markHeap, insertIntoHeap } from "./heap.js";
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { findLane } from "./lanes.js";
|
|
12
12
|
|
|
13
13
|
import "./invariants.js";
|
|
14
14
|
|
|
15
|
-
import { disposeChildren, markDisposal, inheritId } from "./owner.js";
|
|
15
|
+
import { disposeChildren, markDisposal, linkChild, inheritId } from "./owner.js";
|
|
16
16
|
|
|
17
|
-
GlobalQueue
|
|
17
|
+
import { GlobalQueue, bumpNotifyEpoch, activeTransition, globalQueue, clock, currentTransition, insertSubs, queuePendingNode, wakeParked, heldTrims, runInTransition, schedule, batchJoins, notifyEpoch, dirtyQueue, projectionWriteActive, reaskArmed, armReaskClear } from "./scheduler.js";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// The heap's per-node step. A tracked effect's heap visit is its compute
|
|
20
|
+
// phase — empty, like a user effect whose compute reads nothing — and hands
|
|
21
|
+
// the callback to the user queue. Routing the wake through the heap, rather
|
|
22
|
+
// than straight into the queue at notify time, is what orders the run after
|
|
23
|
+
// the commit regardless of which phase the write came from: a write in a
|
|
24
|
+
// render-effect callback stages its value for the next pass, but a wake pushed
|
|
25
|
+
// directly into the user queue ran in the SAME pass, read the old value, and
|
|
26
|
+
// nothing re-notified it when the value landed (#3291).
|
|
27
|
+
GlobalQueue.Ke = e => {
|
|
28
|
+
if (e.je === EFFECT_TRACKED) {
|
|
29
|
+
deleteFromHeap(e, queueFor(e));
|
|
30
|
+
e.Xe = true;
|
|
31
|
+
e.T.enqueue(EFFECT_USER, e.$e);
|
|
32
|
+
} else recompute(e);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
GlobalQueue.en = disposeChildren;
|
|
20
36
|
|
|
21
37
|
let tracking = false;
|
|
22
38
|
|
|
@@ -42,14 +58,29 @@ let context = null;
|
|
|
42
58
|
|
|
43
59
|
let currentOptimisticLane = null;
|
|
44
60
|
|
|
61
|
+
/** Notify `node`'s subscribers on `lane`'s channel: they recompute as the
|
|
62
|
+
* lane's passes — a memo publishes a derived override (lanes stage, #3479),
|
|
63
|
+
* an effect runs from the lane's queue, at the park, ahead of the
|
|
64
|
+
* transaction — the display-ahead view. The write itself is already staged
|
|
65
|
+
* (setSignal) and commits with the frame; this walk shows it now. Used by a
|
|
66
|
+
* boundary re-armed from a lane pass (boundaries.ts `_swap`, #3540). */ function notifyOnLane(e, n) {
|
|
67
|
+
const t = currentOptimisticLane;
|
|
68
|
+
currentOptimisticLane = n;
|
|
69
|
+
try {
|
|
70
|
+
insertSubs(e, true);
|
|
71
|
+
} finally {
|
|
72
|
+
currentOptimisticLane = t;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
let snapshotCaptureActive = false;
|
|
46
77
|
|
|
47
78
|
let snapshotSources = null;
|
|
48
79
|
|
|
49
80
|
function ownerInSnapshotScope(e) {
|
|
50
81
|
while (e) {
|
|
51
|
-
if (e.
|
|
52
|
-
e = e.
|
|
82
|
+
if (e.nn) return true;
|
|
83
|
+
e = e._parent;
|
|
53
84
|
}
|
|
54
85
|
return false;
|
|
55
86
|
}
|
|
@@ -60,44 +91,49 @@ function setSnapshotCapture(e) {
|
|
|
60
91
|
}
|
|
61
92
|
|
|
62
93
|
function markSnapshotScope(e) {
|
|
63
|
-
e.
|
|
94
|
+
e.nn = true;
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
function releaseSnapshotScope(e) {
|
|
67
|
-
e.
|
|
98
|
+
e.nn = false;
|
|
68
99
|
releaseSubtree(e);
|
|
69
100
|
schedule();
|
|
70
101
|
}
|
|
71
102
|
|
|
72
103
|
function releaseSubtree(e) {
|
|
73
|
-
let
|
|
74
|
-
while (
|
|
75
|
-
if (
|
|
76
|
-
|
|
104
|
+
let n = e.tn;
|
|
105
|
+
while (n) {
|
|
106
|
+
if (n.nn) {
|
|
107
|
+
n = n.un;
|
|
77
108
|
continue;
|
|
78
109
|
}
|
|
79
|
-
if (
|
|
80
|
-
const e =
|
|
81
|
-
e.
|
|
82
|
-
if (e.
|
|
83
|
-
e.
|
|
84
|
-
e.
|
|
85
|
-
if (dirtyQueue.
|
|
110
|
+
if (n.he) {
|
|
111
|
+
const e = n;
|
|
112
|
+
e.C &= ~CONFIG_IN_SNAPSHOT_SCOPE;
|
|
113
|
+
if (e.oe & REACTIVE_SNAPSHOT_STALE) {
|
|
114
|
+
e.oe &= ~REACTIVE_SNAPSHOT_STALE;
|
|
115
|
+
e.oe |= REACTIVE_DIRTY;
|
|
116
|
+
if (dirtyQueue.ln > e.rn) dirtyQueue.ln = e.rn;
|
|
86
117
|
insertIntoHeap(e, dirtyQueue);
|
|
87
118
|
}
|
|
88
119
|
}
|
|
89
|
-
releaseSubtree(
|
|
90
|
-
|
|
120
|
+
releaseSubtree(n);
|
|
121
|
+
n = n.un;
|
|
91
122
|
}
|
|
92
123
|
}
|
|
93
124
|
|
|
94
125
|
function clearSnapshots() {
|
|
95
126
|
if (snapshotSources) {
|
|
96
127
|
for (const e of snapshotSources) {
|
|
97
|
-
|
|
128
|
+
// The extension is a fixed-shape object with `_snapshotValue`
|
|
129
|
+
// pre-initialized to undefined (see ext()), and every reader tests
|
|
130
|
+
// `!== undefined` — assign, don't `delete`: deleting a field pushes the
|
|
131
|
+
// object to dictionary mode for every later read of every field.
|
|
132
|
+
const n = e.o;
|
|
133
|
+
if (n != null) n.sn = undefined;
|
|
98
134
|
// StoreNode targets share one pre-initialized hidden class (see
|
|
99
|
-
// createStoreProxy) —
|
|
100
|
-
//
|
|
135
|
+
// createStoreProxy) — same rule, and only when present so signal-node
|
|
136
|
+
// sources don't grow the field.
|
|
101
137
|
if (e[STORE_SNAPSHOT_PROPS] !== undefined) e[STORE_SNAPSHOT_PROPS] = undefined;
|
|
102
138
|
}
|
|
103
139
|
snapshotSources = null;
|
|
@@ -105,47 +141,140 @@ function clearSnapshots() {
|
|
|
105
141
|
snapshotCaptureActive = false;
|
|
106
142
|
}
|
|
107
143
|
|
|
108
|
-
function recompute(e,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
function recompute(e, n = false) {
|
|
145
|
+
// §12d: any recompute can clean a marked subscriber — invalidate skips.
|
|
146
|
+
bumpNotifyEpoch();
|
|
147
|
+
const t = e.je;
|
|
148
|
+
// Lane posture is resolved BEFORE the previous frame is parked below: a
|
|
149
|
+
// lane pass on an effect direct-commits (#3662, see the parking site), so
|
|
150
|
+
// the decision must be known there. `lane` is applied to
|
|
151
|
+
// `currentOptimisticLane` further down, once the previous posture is saved.
|
|
152
|
+
let i = !!(e.oe & REACTIVE_OPTIMISTIC_DIRTY);
|
|
153
|
+
let u = null;
|
|
154
|
+
if (i) {
|
|
155
|
+
u = GlobalQueue.an(e, true);
|
|
156
|
+
// `false` = wake-only lane demotion: recompute plain so a mid-tick
|
|
157
|
+
// latest()/isPending() pull stages instead of direct-committing (#3009).
|
|
158
|
+
// The predicate lives with the engine (recomputeLane).
|
|
159
|
+
if (u === false) i = false;
|
|
160
|
+
} else if (e.C & CONFIG_DERIVED_OVERRIDE) {
|
|
161
|
+
// Lanes stage (#3479): a pass over a live lane member carrying a derived
|
|
162
|
+
// override is the lane's pass whatever channel dirtied it (a boundary
|
|
163
|
+
// reset, an unrelated sync write) — its inputs serve the lane's view, so
|
|
164
|
+
// its result is the lane's and belongs in the override slot. Run plain,
|
|
165
|
+
// A18's sync twin below read that re-derived lane view as a differing
|
|
166
|
+
// truth (a fresh array), superseded the override and demoted the lane;
|
|
167
|
+
// the lane's next pass then dropped the staged "truth" and left the node
|
|
168
|
+
// flagged superseded with nothing to serve (fuzzer latest-1 #2481). A
|
|
169
|
+
// demoted node resolves no lane and stays plain: its pass IS the truth.
|
|
170
|
+
u = GlobalQueue.an(e, true);
|
|
171
|
+
if (u) i = true;
|
|
172
|
+
} else if (activeTransition && !n && activeTransition.cn.length) {
|
|
173
|
+
// Lane adoption: parent-deeper-than-owned-child can run before its OPT-dirty
|
|
174
|
+
// child propagates. Walk deps once and inherit the OPT lane so this node
|
|
175
|
+
// recomputes under the right posture and propagates correctly.
|
|
176
|
+
u = GlobalQueue.an(e, false);
|
|
177
|
+
if (u) i = true;
|
|
178
|
+
}
|
|
179
|
+
if (!n) {
|
|
180
|
+
// A stamped memo re-enters its hold: its value is that transaction's work.
|
|
181
|
+
// An effect's pass belongs to whatever dirtied it (A15 corollary: effects
|
|
182
|
+
// don't entangle parallel transactions); it joins its stamp only when the
|
|
183
|
+
// pass observes the held flight — queue notification (#3407).
|
|
184
|
+
if (e.we && !t && activeTransition !== e.we) globalQueue.initTransition(e.we);
|
|
112
185
|
deleteFromHeap(e, queueFor(e));
|
|
113
|
-
e.
|
|
114
|
-
|
|
115
|
-
|
|
186
|
+
if (e.o !== null) {
|
|
187
|
+
e.o.Ce = null;
|
|
188
|
+
// Supersede is where an iterator flight dies (#3122): close it now.
|
|
189
|
+
// Its cleanup(close) registration may sit in a zombie-deferred
|
|
190
|
+
// disposal list that a held transition only drains when the
|
|
191
|
+
// SUPERSEDING flight settles — cancellation must not wait for the
|
|
192
|
+
// work that replaced it. Idempotent with the cleanup-channel close.
|
|
193
|
+
releaseFlightTeardown(e);
|
|
194
|
+
}
|
|
195
|
+
// Tracked effects run after finalizePureQueue, so dispose immediately
|
|
196
|
+
// instead of deferring. Children built by an uncommitted recompute
|
|
197
|
+
// (CONFIG_HELD_CHILDREN) die immediately too: no frame ever showed them.
|
|
198
|
+
// Everything else is the committed frame's and is deferred as zombies
|
|
199
|
+
// until this node's commit — a transaction-owned node included (#3404):
|
|
200
|
+
// a parked node's children predate the hold, and tearing them down when
|
|
201
|
+
// the source lands ran cleanups before the transaction's atomic reveal.
|
|
202
|
+
// A lane pass on an effect parks a LANE frame instead (CONFIG_LANE_FRAME,
|
|
203
|
+
// #3662; A15 lanes corollary): the frame it replaces leaves the screen
|
|
204
|
+
// when the lane's queue applies this run (A30) — not at the action's
|
|
205
|
+
// commit — and a held lane defers that with the frame still displayed.
|
|
206
|
+
// The drain is the lane's first render entry for this pass, pushed before
|
|
207
|
+
// the pass builds the new frame: cleanups before side effects — the
|
|
208
|
+
// retired frame's `onCleanup`s run ahead of every effect callback of the
|
|
209
|
+
// new frame. While the frame waits, the live children were never shown:
|
|
210
|
+
// a superseding pass disposes them here like held children, and the
|
|
211
|
+
// parked frame stays.
|
|
212
|
+
if (t === EFFECT_TRACKED || e.C & (CONFIG_HELD_CHILDREN | CONFIG_LANE_FRAME)) disposeChildren(e); else if (e.tn !== null || e.qe !== null) {
|
|
116
213
|
markDisposal(e);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
e.
|
|
121
|
-
e.
|
|
214
|
+
const n = ext(e);
|
|
215
|
+
n.fn = e.qe;
|
|
216
|
+
n._n = e.tn;
|
|
217
|
+
e.qe = null;
|
|
218
|
+
e.tn = null;
|
|
219
|
+
e.En = 0;
|
|
220
|
+
if (t && u) {
|
|
221
|
+
e.C |= CONFIG_LANE_FRAME;
|
|
222
|
+
findLane(u).dn[0].push(() => {
|
|
223
|
+
e.C &= ~CONFIG_LANE_FRAME;
|
|
224
|
+
disposeChildren(e, false, true);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
122
227
|
} else ;
|
|
123
228
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
|
|
229
|
+
// A derived override (lanes stage, #3479) is override-covered like a written
|
|
230
|
+
// one: a plain pass over it — its source superseded (A18) — stages the truth
|
|
231
|
+
// and supersedes the override through the sync twin below.
|
|
232
|
+
const l = (e.C & (CONFIG_OPTIMISTIC | CONFIG_DERIVED_OVERRIDE)) !== 0 && e.o?.Fe !== NOT_PENDING && e.o?.Fe !== undefined;
|
|
233
|
+
const r = !!(e.h & STATUS_UNINITIALIZED);
|
|
234
|
+
// Capture both error and pending status before the compute clears them.
|
|
235
|
+
// A conditional can drop its pending source and recover to an unchanged
|
|
236
|
+
// value, leaving blocked dependents outside that source’s settle walk.
|
|
237
|
+
const o = e.h & STATUS_ERROR ? e.o?._ : undefined;
|
|
238
|
+
const s = (e.h & STATUS_PENDING) !== 0;
|
|
239
|
+
const a = s ? e.o?.ue : undefined;
|
|
240
|
+
// Pending SOURCE-hood, captured before the compute clears status: a node
|
|
241
|
+
// whose own flight parked dependents self-registers in _pendingSources
|
|
242
|
+
// (notifyStatus, isSource). If this recompute supersedes that flight and
|
|
243
|
+
// settles synchronously, those dependents settle HERE — asyncWrite's
|
|
244
|
+
// settlePendingSource walk never runs for a landing that was preempted
|
|
245
|
+
// (#3181).
|
|
246
|
+
const c = e.o?.ue?.has(e);
|
|
131
247
|
// Re-ask classification lives in the verdict module; capture the flag before
|
|
132
248
|
// the recompute wipes _flags below.
|
|
133
|
-
const
|
|
249
|
+
const f = (e.oe & REACTIVE_REASK) !== 0;
|
|
134
250
|
// Captured before the compute clears it on a sync landing: if that landing
|
|
135
251
|
// is transition-held below, the window must stay open until the hold
|
|
136
252
|
// commits (commitPendingNode) — a closed window plus a held value reads as
|
|
137
253
|
// a pending frame to live observers of the verdict (#2990).
|
|
138
|
-
const
|
|
139
|
-
|
|
254
|
+
const _ = e.ve;
|
|
255
|
+
// Creation-time A29 (see enterStagedRead): a pass outside a flush that is
|
|
256
|
+
// served a live transaction's staged value records the transaction here
|
|
257
|
+
// and is staged INTO it below — "born held" — instead of committing.
|
|
258
|
+
const E = stagedEntry;
|
|
259
|
+
stagedEntry = null;
|
|
260
|
+
const d = context;
|
|
140
261
|
context = e;
|
|
141
|
-
e.
|
|
142
|
-
e.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
262
|
+
e.Nn = null;
|
|
263
|
+
e.Ye++;
|
|
264
|
+
// REACTIVE_ZOMBIE is position, not scheduling state: it says the node sits
|
|
265
|
+
// on its owner's `_pendingFirstChild` chain, and `disposeChildren` keys its
|
|
266
|
+
// parent-chain splice off it. A zombie reruns for mainline writes until the
|
|
267
|
+
// commit that disposes it (#3463), so the per-pass wipe here — and in the
|
|
268
|
+
// finally below and in updateIfNecessary — must carry it (#3543): a
|
|
269
|
+
// de-flagged zombie spliced itself out of the LIVE chain at disposal,
|
|
270
|
+
// orphaning the owner's current child, which stayed subscribed forever.
|
|
271
|
+
e.oe = REACTIVE_RECOMPUTING_DEPS | e.oe & REACTIVE_ZOMBIE;
|
|
272
|
+
e.be = clock;
|
|
273
|
+
let N = e._e === NOT_PENDING ? e.ce : e._e;
|
|
274
|
+
let I = e.rn;
|
|
275
|
+
let T = false;
|
|
276
|
+
let S = tracking;
|
|
277
|
+
let O = currentOptimisticLane;
|
|
149
278
|
tracking = true;
|
|
150
279
|
// A computed's fn establishes its OWN dependencies, so it must never run
|
|
151
280
|
// inside a latest() read window: read() short-circuits through the
|
|
@@ -153,65 +282,72 @@ function recompute(e, t = false) {
|
|
|
153
282
|
// computed) inside latest(fn) came out permanently dependency-less (#2926).
|
|
154
283
|
// latestRead() already suspends the flag for its pull-recomputes; this
|
|
155
284
|
// covers creation-time computes and flushes that run inside the window.
|
|
156
|
-
const
|
|
285
|
+
const A = latestReadActive;
|
|
157
286
|
latestReadActive = false;
|
|
287
|
+
// A memo computes under its OWN lane posture, never the puller's (A31,
|
|
288
|
+
// #3442): its value is one shared slot every reader sees, so a pull from a
|
|
289
|
+
// lane-carrying reader (a probe effect on its companion lane pulling a sync
|
|
290
|
+
// memo) must not run it with that lane's read carve-outs — under a lane, a
|
|
291
|
+
// pending node on no lane serves its committed value instead of throwing,
|
|
292
|
+
// and the memo then published a stale "settled" value, dropped its
|
|
293
|
+
// pending status, and stopped holding its transaction. The branches below
|
|
294
|
+
// re-establish the posture the memo itself owns (OPT-dirty, or adopted
|
|
295
|
+
// through its deps). Effects keep the ambient lane: their runs are the
|
|
296
|
+
// lane's own view.
|
|
297
|
+
if (!t) currentOptimisticLane = null;
|
|
158
298
|
// Lane posture lives with the engine: OPTIMISTIC_DIRTY is only ever set by
|
|
159
299
|
// engine-driven paths, and _optimisticNodes is only pushed by
|
|
160
300
|
// _optimisticWrite, so the hook is installed whenever either gate holds.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (t) {
|
|
174
|
-
i = true;
|
|
175
|
-
currentOptimisticLane = t;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const T = n && n !== EFFECT_USER;
|
|
179
|
-
const I = stale;
|
|
180
|
-
if (T) stale = true;
|
|
301
|
+
// (Resolved at the top of this pass, ahead of the parking site.)
|
|
302
|
+
if (u) currentOptimisticLane = u;
|
|
303
|
+
const C = t && t !== EFFECT_USER;
|
|
304
|
+
const p = stale;
|
|
305
|
+
if (C) stale = true;
|
|
306
|
+
// An effect recorded for this transaction's commit replay (it once read a
|
|
307
|
+
// node the transaction held and showed the committed value) and now
|
|
308
|
+
// recomputing UNDER the transaction sees its staged view: the value this
|
|
309
|
+
// run produces is applied by the commit itself, and the stale recording
|
|
310
|
+
// would publish the frame a second time. Drop it; the reads below re-record
|
|
311
|
+
// if they are served the committed view again (a lane's committed read).
|
|
312
|
+
if (t && activeTransition !== null && activeTransition.In.size) activeTransition.In.delete(e);
|
|
181
313
|
try {
|
|
182
|
-
if (!false && e.
|
|
183
|
-
|
|
184
|
-
e.
|
|
185
|
-
e.
|
|
314
|
+
if (!false && e.C & CONFIG_SYNC) {
|
|
315
|
+
N = e.he(N);
|
|
316
|
+
if (e.o !== null) e.o.Ce = null;
|
|
317
|
+
e.ve = false;
|
|
186
318
|
} else {
|
|
187
319
|
// Snapshot `_inFlight` so we can detect whether `_fn` self-registered an async
|
|
188
320
|
// subscription (e.g. `createProjection` calls `handleAsync` from inside its body
|
|
189
321
|
// with a setter callback). In that case, the outer `handleAsync` call below would
|
|
190
322
|
// clobber the fresh subscription, so we skip it and let the internally-registered
|
|
191
323
|
// iteration drive updates.
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const i = typeof
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
if (!
|
|
198
|
-
e.
|
|
324
|
+
const n = e.o?.Ce;
|
|
325
|
+
const t = e.he(N);
|
|
326
|
+
const i = typeof t === "object" && t !== null;
|
|
327
|
+
const u = e.o?.Ce !== n;
|
|
328
|
+
N = u || !i ? t : handleAsync(e, t);
|
|
329
|
+
if (!u && !i) {
|
|
330
|
+
if (e.o !== null) e.o.Ce = null;
|
|
199
331
|
// A sync (non-object) return is the first real answer; async-shaped
|
|
200
332
|
// results clear inside handleAsync at their own landing points, and a
|
|
201
333
|
// self-registered flight (inFlightChanged — projections) clears when
|
|
202
334
|
// its internal handleAsync lands.
|
|
203
|
-
e.
|
|
335
|
+
e.ve = false;
|
|
204
336
|
}
|
|
205
337
|
}
|
|
206
|
-
// On a status-free node clearStatus is a guaranteed no-op: every
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
338
|
+
// On a status-free node clearStatus is a guaranteed no-op: every field
|
|
339
|
+
// its body gates on is either _statusFlags or lives in the cold
|
|
340
|
+
// extension — no extension, no status to clear. (_x from an unrelated
|
|
341
|
+
// installer just makes clearStatus a cheap re-verified no-op.)
|
|
342
|
+
// A node born held keeps STATUS_UNINITIALIZED until its transaction
|
|
343
|
+
// commits: it has no committed value, and read() holds its readers.
|
|
344
|
+
if (e.h !== 0 || e.o !== null) clearStatus(e, n && stagedEntry === null);
|
|
345
|
+
// _optimisticLane is only ever assigned by engine paths (CONFIG_HAS_LANE
|
|
346
|
+
// is their sticky presence mark).
|
|
347
|
+
if (e.C & CONFIG_HAS_LANE && e.o?.me) GlobalQueue.Tn(e);
|
|
348
|
+
} catch (n) {
|
|
349
|
+
const t = n instanceof NotReadyError;
|
|
350
|
+
if (t && e.ve) {
|
|
215
351
|
// Loading window with an unready sync dependency: register for the
|
|
216
352
|
// source's settle (the settlePendingSource walk runs off
|
|
217
353
|
// _pendingSources + _blocked alone) but take NO read-visible pending
|
|
@@ -219,176 +355,459 @@ function recompute(e, t = false) {
|
|
|
219
355
|
// registration — the committed loading value keeps serving. If the
|
|
220
356
|
// node is currently errored the error stays the answer until this
|
|
221
357
|
// retry can actually run.
|
|
222
|
-
parkLoadingWindow(e,
|
|
358
|
+
parkLoadingWindow(e, n);
|
|
223
359
|
} else {
|
|
224
360
|
// Track pending async in the lane (not the lane's source — it creates the lane
|
|
225
361
|
// but doesn't belong to it). Set lane BEFORE notifyStatus for downstream propagation.
|
|
226
|
-
if (
|
|
362
|
+
if (t && currentOptimisticLane) GlobalQueue.Sn(e);
|
|
227
363
|
let i = false;
|
|
228
|
-
if (
|
|
229
|
-
e.
|
|
230
|
-
if (GlobalQueue
|
|
364
|
+
if (t) {
|
|
365
|
+
ext(e).Pe = true;
|
|
366
|
+
if (GlobalQueue.On !== null) i = GlobalQueue.On(e, f);
|
|
231
367
|
}
|
|
232
|
-
notifyStatus(e,
|
|
368
|
+
notifyStatus(e, t ? STATUS_PENDING : STATUS_ERROR, n, undefined, t ? e.o?.me : undefined);
|
|
369
|
+
// The replacement source is fully propagated now. If no new flight
|
|
370
|
+
// re-owned self, retire the superseded flight and its dependent copies.
|
|
371
|
+
if (t && c && !e.o?.Ce) settlePendingSource(e);
|
|
372
|
+
// A re-park drops what the earlier pass carried (#3456): a source this
|
|
373
|
+
// pass no longer reaches — its branch switched, or a fresh flight
|
|
374
|
+
// replaced the inputs' pending with its own — stays copied onto
|
|
375
|
+
// dependents that reached it only through here, and its landing walk
|
|
376
|
+
// stops at this node (nothing left to retire) before it finds them. A
|
|
377
|
+
// dependent then waits forever on a flight it has no path to. The
|
|
378
|
+
// re-park twin of the unchanged-value recovery sweep below; dependents
|
|
379
|
+
// with another path keep the source (retryReaches).
|
|
380
|
+
if (t && a) for (const n of a) if (n !== e && !e.o?.ue?.has(n)) settlePendingSource(e, n);
|
|
233
381
|
if (i) GlobalQueue.k(e);
|
|
234
382
|
}
|
|
235
383
|
} finally {
|
|
236
|
-
tracking =
|
|
237
|
-
latestReadActive =
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
384
|
+
tracking = S;
|
|
385
|
+
latestReadActive = A;
|
|
386
|
+
if (C) stale = p;
|
|
387
|
+
// Consume the missed-wake latch (#3037, set by insertSubs): a dep write
|
|
388
|
+
// landed beneath this pass on a link it had already validated. The wipe
|
|
389
|
+
// below must not key off DIRTY/CHECK — the read-time pull protocol
|
|
390
|
+
// (markNode(c) in read()) marks the running node as part of ordinary
|
|
391
|
+
// bookkeeping, and those marks are correctly discarded here.
|
|
392
|
+
T = (e.oe & REACTIVE_MISSED_WAKE) !== 0;
|
|
393
|
+
// REACTIVE_DISPOSED survives too (#3621): the pass may have disposed its
|
|
394
|
+
// own owner (a memo calling its root's `dispose()`, a cleanup doing so
|
|
395
|
+
// #3601/#3606), and `disposeChildren` set the flag on this node
|
|
396
|
+
// reentrantly. Dropped, the node read as live — `refresh()` re-ran it
|
|
397
|
+
// and `isDisposed()` lied.
|
|
398
|
+
e.oe = e.oe & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED) | (n ? e.oe & REACTIVE_SNAPSHOT_STALE : 0);
|
|
399
|
+
context = d;
|
|
400
|
+
}
|
|
401
|
+
// The cast re-widens: TS narrowed `stagedEntry` to `null` at the reset
|
|
402
|
+
// above and does not invalidate that across the compute call that
|
|
403
|
+
// `enterStagedRead` runs under. No emitted code.
|
|
404
|
+
const R = stagedEntry;
|
|
405
|
+
stagedEntry = E;
|
|
406
|
+
// A node that died during its own pass (#3621) is dead at the end of it,
|
|
407
|
+
// and the pass is void. Its owner's teardown already unlinked its deps,
|
|
408
|
+
// removed it from the heap and ran its cleanups; what remains is what the
|
|
409
|
+
// body did AFTER the `dispose()` call: reads that re-linked the dead node
|
|
410
|
+
// to its sources (unlinked here — the leak that kept it re-running in a
|
|
411
|
+
// torn-down tree), a flight it may have started (retired: the landing
|
|
412
|
+
// checks `_inFlight` identity), and the value it returned. That value is
|
|
413
|
+
// NOT published: a dead node freezes at its last committed value (#3024),
|
|
414
|
+
// so nothing is staged, committed, or propagated to subscribers, and an
|
|
415
|
+
// effect's run is not enqueued (runEffect would refuse it anyway). The
|
|
416
|
+
// attribution frame opened at the top is still closed.
|
|
417
|
+
if (e.oe & REACTIVE_DISPOSED) {
|
|
418
|
+
clearDeps(e);
|
|
419
|
+
if (e.o !== null) e.o.Ce = null;
|
|
420
|
+
currentOptimisticLane = O;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (!e.o?._) {
|
|
424
|
+
// INV-11 (#3330): the equality gate compares against the slot this run
|
|
425
|
+
// publishes to. An override-covered node publishes the override; a lane
|
|
426
|
+
// recompute (OPT-dirty) direct-commits `_value` — the lane's own reveal
|
|
427
|
+
// schedule — so a transaction-held `_pendingValue` that already equals
|
|
428
|
+
// the new result is not "unchanged": the screen still shows `_value`.
|
|
429
|
+
// Only a transaction-staged run compares against `_pendingValue`.
|
|
430
|
+
const u = l ? unwrapOverride(e.o?.Fe) : i || e._e === NOT_PENDING ? e.ce : e._e;
|
|
431
|
+
let s = false;
|
|
246
432
|
try {
|
|
247
|
-
|
|
248
|
-
} catch (
|
|
433
|
+
s = !t && r || !e.xe || !e.xe(u, N);
|
|
434
|
+
} catch (n) {
|
|
249
435
|
// A throwing user comparator is an error of this node's computation.
|
|
250
436
|
// Route it through the same status path as a compute-phase throw so
|
|
251
437
|
// error boundaries contain it; otherwise it unwinds the scheduler
|
|
252
438
|
// flush, bypassing every boundary and wedging the queue (#2837).
|
|
253
|
-
notifyStatus(e, STATUS_ERROR,
|
|
439
|
+
notifyStatus(e, STATUS_ERROR, n);
|
|
254
440
|
}
|
|
255
441
|
// Effects use `_equals: false` (no per-effect closure). The side effects that
|
|
256
442
|
// the equals closure used to perform — flagging the effect dirty and enqueueing
|
|
257
443
|
// its runner — happen here instead. `!create` matches the previous `initialized`
|
|
258
444
|
// gate: the explicit recompute(node, true) inside effect() does not enqueue, so
|
|
259
445
|
// effect() can call its runner synchronously for the first run.
|
|
260
|
-
if (
|
|
261
|
-
e.
|
|
446
|
+
if (t && s) {
|
|
447
|
+
e.Xe = !e.o?._;
|
|
262
448
|
// Reuse one bound runner per effect — runEffect no-ops on a stale
|
|
263
449
|
// `_modified`, so re-enqueueing the same function is harmless.
|
|
264
|
-
if (!
|
|
450
|
+
if (!n) {
|
|
451
|
+
e.T.enqueue(t, e.An ??= GlobalQueue.Cn.bind(null, e));
|
|
452
|
+
// Contested effect (#3322). Effects don't entangle transactions (a
|
|
453
|
+
// shared effect is not shared state), yet they have one value slot:
|
|
454
|
+
// when this write commits under a different transaction
|
|
455
|
+
// (`activeTransition`, null = mainline) than the one that produced the
|
|
456
|
+
// previous value (`_valueTransition`), that view is gone. Rather than
|
|
457
|
+
// merge the two, each commit re-derives the effect against its own
|
|
458
|
+
// committed world (Transition._contested, re-dirtied by
|
|
459
|
+
// finalizePureQueue ahead of the heap run). The previous owner always
|
|
460
|
+
// needs it if still live: its commit is silent (staging already
|
|
461
|
+
// notified) and the value it computed is gone. The new owner needs it
|
|
462
|
+
// too, for the same reason, unless it is mainline — mainline publishes
|
|
463
|
+
// what it computes. A previous owner that was mainline, or already
|
|
464
|
+
// committed, left nothing to protect.
|
|
465
|
+
let n = e.pn;
|
|
466
|
+
if (n !== activeTransition) {
|
|
467
|
+
e.pn = activeTransition;
|
|
468
|
+
if (n !== null && (n = currentTransition(n)) !== activeTransition && !n.Rn) {
|
|
469
|
+
(n.Dn ??= []).push(e);
|
|
470
|
+
if (activeTransition !== null) (activeTransition.Dn ??= []).push(e);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
265
474
|
}
|
|
266
|
-
if (e._) ; else if (
|
|
267
|
-
const u = l ? e.
|
|
268
|
-
if (
|
|
475
|
+
if (e.o?._) ; else if (s) {
|
|
476
|
+
const u = l ? e.o?.Fe : undefined;
|
|
477
|
+
if (n && R === null ||
|
|
269
478
|
// Plain sync flush (no transition on either side) commits effect
|
|
270
479
|
// values directly — the pending round-trip (queuePendingNode +
|
|
271
480
|
// commitPendingNodes) exists to sequence transition reveals, and
|
|
272
481
|
// paying it per effect on the plain path is pure overhead.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
482
|
+
// DIRECT_COMMIT effects (resolve/until) commit directly even under
|
|
483
|
+
// their own held transition: their applies deliver on a microtask,
|
|
484
|
+
// not the stashed queues, so a staged value would hand the immediate
|
|
485
|
+
// apply stale state — see CONFIG_DIRECT_COMMIT.
|
|
486
|
+
t && R === null && (activeTransition !== e.we || activeTransition === null || e.C & CONFIG_DIRECT_COMMIT) || i) {
|
|
487
|
+
// Lanes stage (#3479): a lane pass on a memo publishes its speculative
|
|
488
|
+
// result as an OVERRIDE — `_value` stays the committed truth, so a
|
|
489
|
+
// reader off the lane (A17's committed view, #3460) sees a whole
|
|
490
|
+
// committed frame: the source's shadow and its derivations together,
|
|
491
|
+
// never a committed shadow beside a speculative memo. The lane's own
|
|
492
|
+
// readers and untracked reads see the override (A17); the revert
|
|
493
|
+
// drops it and re-derives (resolveOptimisticNodes). Effects keep the
|
|
494
|
+
// direct commit — their `_value` is a run result the lane's queues
|
|
495
|
+
// already sequence. Either way the lane pass drops any superseded
|
|
496
|
+
// older hold so its queued commit can't clobber the fresh frame: a
|
|
497
|
+
// hold staged on an earlier, lane-free pass of the SAME transaction
|
|
498
|
+
// is superseded — left in place, the commit published the older
|
|
499
|
+
// frame over the fresh one (#3377). A reversion pass (OPT-dirty with
|
|
500
|
+
// no lane — the override dropped) commits directly: it IS the truth.
|
|
501
|
+
if (i && !t && currentOptimisticLane !== null) GlobalQueue.Me(e, N, currentOptimisticLane); else e.ce = N;
|
|
502
|
+
if (i) e._e = NOT_PENDING;
|
|
283
503
|
} else {
|
|
284
|
-
e._e =
|
|
504
|
+
e._e = N;
|
|
505
|
+
if (R !== null) {
|
|
506
|
+
// Born held: the pass ran from mainline and derived from this
|
|
507
|
+
// transaction's staged world (enterStagedRead). Its value is the
|
|
508
|
+
// transaction's — staged into it directly, stamped, and committed
|
|
509
|
+
// with it; mainline's batch never sees it. A born-held effect
|
|
510
|
+
// skips its synchronous first run (effect()) and is replayed by
|
|
511
|
+
// the commit like a stale reader that showed the committed frame.
|
|
512
|
+
// (A re-pass under a fresh boundary — the in-flush form — is
|
|
513
|
+
// already the transaction's: restaged, not re-queued.)
|
|
514
|
+
if (e.we !== R) {
|
|
515
|
+
e.we = R;
|
|
516
|
+
R.Gn.push(e);
|
|
517
|
+
}
|
|
518
|
+
if (t) R.In.add(e);
|
|
519
|
+
// Under a boundary that has not revealed, a held first value is
|
|
520
|
+
// something not ready under it (#3540): the boundary collects this
|
|
521
|
+
// node as a source and shows its fallback; the source stays
|
|
522
|
+
// collected while born held (CollectionQueue._checkSources) and
|
|
523
|
+
// is released by the commit that initializes it. A boundary that
|
|
524
|
+
// already shows content is not told — it holds like any reader.
|
|
525
|
+
if (underFreshLoadingBoundary(e)) e.T.notify(e, STATUS_PENDING, STATUS_PENDING, new NotReadyError(e));
|
|
526
|
+
}
|
|
285
527
|
// A window landing that gets held re-opens the window until the hold
|
|
286
528
|
// commits — the verdict's held-value branch is window-gated (#2990).
|
|
287
|
-
if (
|
|
288
|
-
//
|
|
529
|
+
if (_) e.ve = true;
|
|
530
|
+
// A staged sync recompute is a write path like setSignal/asyncWrite,
|
|
289
531
|
// so sync derivations of held sources stay visible to isPending()/latest()
|
|
290
532
|
// (#2831). Both companion writes are transition-scoped (optimistic) and
|
|
291
|
-
// auto-revert/re-derive at commit.
|
|
292
|
-
//
|
|
293
|
-
|
|
533
|
+
// auto-revert/re-derive at commit. Not gated on an active transition:
|
|
534
|
+
// a plain flush can still become a hold after this recompute — an
|
|
535
|
+
// async memo downstream pends and the batch is adopted into a
|
|
536
|
+
// transaction (scheduler.enterTransition) — and nothing re-derives
|
|
537
|
+
// the companion at adoption, so a memo held that way read
|
|
538
|
+
// isPending() false while its held source read true (#3413).
|
|
539
|
+
if (e.C & CONFIG_HAS_COMPANIONS && GlobalQueue.Ve !== null) GlobalQueue.Ve(e, N);
|
|
294
540
|
}
|
|
295
541
|
// insertSubs only walks _subs (no scheduling of its own), so a
|
|
296
542
|
// subscriber-less node has nothing to notify.
|
|
297
|
-
if (e.
|
|
543
|
+
if (e.u !== null && (!l || i || e.o?.Fe !== u)) insertSubs(e, i || l);
|
|
544
|
+
// A18 supersession, sync twin of asyncWrite's override branch (#3331):
|
|
545
|
+
// this pass published truth that differs from the override (the gate
|
|
546
|
+
// compared against it) into the transaction-held slot. Whether the
|
|
547
|
+
// source is this node's own async or an upstream node it derives from
|
|
548
|
+
// synchronously makes no difference — "the source recomputed". The
|
|
549
|
+
// override stays displayed until the commit; the graph moves to the
|
|
550
|
+
// staged truth now (plain channel, lane demoted). Ordering: "a new
|
|
551
|
+
// value from the source" postdates the override — an override written
|
|
552
|
+
// in this same tick (optimisticWrite stamps `_overrideTime`) is the
|
|
553
|
+
// newer intent over whatever this pass derives from the batch's staged
|
|
554
|
+
// inputs, and is not superseded by it.
|
|
555
|
+
else if (l && !i && e.o.hn !== clock) GlobalQueue.ke(e, N);
|
|
298
556
|
} else if (l) {
|
|
299
557
|
// Unchanged value (equals the override) recomputed while the override
|
|
300
558
|
// is active: _value may still be stale, so hold the authoritative value
|
|
301
559
|
// for commit on its own transition's schedule — invisibly (A17/A18).
|
|
302
560
|
if (e._e === NOT_PENDING) queuePendingNode(e);
|
|
303
|
-
e._e =
|
|
304
|
-
if (
|
|
561
|
+
e._e = N;
|
|
562
|
+
if (_) e.ve = true;
|
|
305
563
|
// see the held branch above (#2990)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
564
|
+
// A confirmation after a supersession restores the override as the
|
|
565
|
+
// graph's value and notifies; a plain confirmation wakes only an
|
|
566
|
+
// authoritative-view reader (until()'s predicate, refresh()'s waiter)
|
|
567
|
+
// that observed this node past its override — "authoritative arrival
|
|
568
|
+
// equal to the override" is exactly the acknowledgment it waits for;
|
|
569
|
+
// A17 silence holds for every ordinary subscriber. Both live in the
|
|
570
|
+
// engine's supersedeOverride.
|
|
571
|
+
GlobalQueue.ke(e, N);
|
|
572
|
+
} else if (e.rn != I) {
|
|
573
|
+
for (let n = e.u; n !== null; n = n.Ae) {
|
|
574
|
+
insertIntoHeapHeight(n.ge, queueFor(n.ge));
|
|
309
575
|
}
|
|
310
576
|
}
|
|
311
577
|
// Silent recovery: errored → unchanged value fires no notification, but
|
|
312
578
|
// dependents still holding the propagated error consumed their dirty flag
|
|
313
579
|
// in an errored run and may sit on stale commits (#2949). Changed-value
|
|
314
580
|
// recoveries ride insertSubs above; a comparator throw re-errored the node
|
|
315
|
-
// (el._error re-set), so this only runs on a genuinely clean recovery.
|
|
316
|
-
if (s
|
|
581
|
+
// (el._x?._error re-set), so this only runs on a genuinely clean recovery.
|
|
582
|
+
if (!s && !e.o?._) {
|
|
583
|
+
if (o !== undefined) settleErroredDependents(e, o);
|
|
584
|
+
// Self-registration (this node's own superseded flight) is the #3181
|
|
585
|
+
// sweep's business below — retiring it here too would walk twice.
|
|
586
|
+
if (a) for (const n of a) if (n !== e) settlePendingSource(e, n);
|
|
587
|
+
}
|
|
588
|
+
// #3181: a synchronous settle supersedes the old landing callback, so
|
|
589
|
+
// recompute owns its pending-source sweep. An uninitialized node without
|
|
590
|
+
// a replacement source still has no truth to reveal and must stay parked.
|
|
591
|
+
if (c && !(e.h & (STATUS_PENDING | STATUS_UNINITIALIZED))) {
|
|
592
|
+
settlePendingSource(e);
|
|
593
|
+
// The superseded flight will not enter its waiting transactions at
|
|
594
|
+
// landing. Recheck them now so an unchanged value cannot leave their
|
|
595
|
+
// writes parked after the last pending source has settled.
|
|
596
|
+
wakeParked();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// A REPORTER whose pass stopped reading a source it reported on (a gate
|
|
600
|
+
// closed) stops counting for the transaction waiting on it (A15 / #3426:
|
|
601
|
+
// the hold lasts while a live reporter observes the flight). Nothing else
|
|
602
|
+
// re-judges a parked transaction (see wokenTransitions; the disposal
|
|
603
|
+
// (#3372) and boundary (#3375) twins of this site), so the writes it held
|
|
604
|
+
// stayed staged for as long as the flight stayed up — forever, for one
|
|
605
|
+
// that never lands (fuzzer #3446 P1, spec O3). The event is "this pass
|
|
606
|
+
// dropped a dep" — deps past `_depsTail` (trimmed below, or kept by A30
|
|
607
|
+
// for a staged pass), or a pass that read nothing — not "recovered from
|
|
608
|
+
// pending": a reporter registered by the stale-reader carve-out
|
|
609
|
+
// (heldFromStale, an INITIALIZED source refetching) displays the committed
|
|
610
|
+
// value and is never pending (fuzzer case 79). Every parked transaction,
|
|
611
|
+
// not the reporter's stamp: the transaction waiting on it registered it
|
|
612
|
+
// without stamping it. One idle pass per parked transaction; done ones
|
|
613
|
+
// return at re-entry. Effects only — reporters register from render-effect
|
|
614
|
+
// notification (INV-3).
|
|
615
|
+
// (`_depsTail` was reset at the top of the pass; TS keeps that narrowing.)
|
|
616
|
+
const D = e.Nn;
|
|
617
|
+
if (t && (s && !(e.h & STATUS_PENDING) || (D === null ? e.Ie !== null : D.Ne !== null))) wakeParked();
|
|
618
|
+
// Dependencies are the committed frame's until it is replaced (A30, #3410; the
|
|
619
|
+
// deps twin of the held children above): a pass that staged its value
|
|
620
|
+
// leaves the previous pass's tail linked for `commitPendingNode` to trim,
|
|
621
|
+
// so a write to a dependency the committed value still derives from
|
|
622
|
+
// reaches this node — and joins its hold if the stage is transaction-held
|
|
623
|
+
// by then (a plain flush decides nothing here: the transaction that holds
|
|
624
|
+
// the pass may open later in the same flush). A pass that published
|
|
625
|
+
// directly (a first pass, a lane's own reveal) trims now. An errored pass
|
|
626
|
+
// (a throw, NotReady included, or a comparator throw above) keeps its full
|
|
627
|
+
// list as before — `_depsTail` marks where it stopped — and the commit
|
|
628
|
+
// skips it by the same `_error`. An effect's frame is the run its value is
|
|
629
|
+
// applied by, not the value slot (#3438): a direct-committed pass that
|
|
630
|
+
// still owes a run (`_modified`) has not replaced what the last run
|
|
631
|
+
// published — the same flush may stash that run into a transaction it
|
|
632
|
+
// opens later — so its tail waits for `runEffect` to trim once the run
|
|
633
|
+
// applies. A pass that changed nothing replaced nothing either (#3469): the
|
|
634
|
+
// same flush may park with its inputs held, and the committed frame still
|
|
635
|
+
// derives from the tail — its trim waits on the flush's verdict (heldTrims).
|
|
636
|
+
// A tracked effect's pass IS its run (it bypasses the heap and runs after
|
|
637
|
+
// the commit): the frame is replaced, trim now.
|
|
638
|
+
if (!e.o?._ && e._e === NOT_PENDING && !(t && e.Xe)) {
|
|
639
|
+
if (n || i || t === EFFECT_TRACKED) trimStaleDeps(e); else if (e.Nn?.Ne ?? e.Ie) heldTrims.push(e);
|
|
317
640
|
}
|
|
318
|
-
currentOptimisticLane =
|
|
319
|
-
|
|
641
|
+
currentOptimisticLane = O;
|
|
642
|
+
// A parked LANE frame is not a hold (#3662): its drain is the effect's own
|
|
643
|
+
// run, not a commit — the node is neither queued nor stamped for it, and
|
|
644
|
+
// the release below (for transaction zombies) leaves it parked.
|
|
645
|
+
const G = (e.C & CONFIG_LANE_FRAME) !== 0;
|
|
646
|
+
const h = e._e !== NOT_PENDING || !G && e.o !== null && (e.o._n !== null || e.o.fn !== null) || (e.h & (STATUS_PENDING | STATUS_UNINITIALIZED)) !== 0;
|
|
320
647
|
// Override-covered holds (hasOverride) always queue: their commit belongs
|
|
321
648
|
// to their own transition's schedule (A18 re-rule) and is unobservable
|
|
322
649
|
// under the override (A17). Revert no longer commits anything, so an
|
|
323
650
|
// unqueued covered hold would leak (INV-7) once the revert clears
|
|
324
651
|
// _transition.
|
|
325
|
-
|
|
326
|
-
|
|
652
|
+
|
|
653
|
+
// While a pass's result waits on a commit, its children (and `_disposal`)
|
|
654
|
+
// wait with it, and a re-run may tear them down immediately
|
|
655
|
+
// (CONFIG_HELD_CHILDREN, #3404). One exception: a transaction-owned effect
|
|
656
|
+
// recomputed mainline (contested, #3322) published its value directly — a
|
|
657
|
+
// `_pendingValue` left from an earlier held pass is that transaction's,
|
|
658
|
+
// not this one's — so this pass's children are the frame's, and any
|
|
659
|
+
// zombies deferred at the top are superseded on that same frame. Its
|
|
660
|
+
// commit rides the transaction, not this flush: release them here rather
|
|
661
|
+
// than let two generations render at once. A LANE pass is the same case
|
|
662
|
+
// (#3662): it direct-commits ahead of its transaction, so its children are
|
|
663
|
+
// the frame's too. Left flagged for an OLDER frame's zombies, the stamped
|
|
664
|
+
// transaction's same-flush re-run below disposed the lane's freshly built
|
|
665
|
+
// children (an insert's inner effect, its run still queued in the lane)
|
|
666
|
+
// and held their replacements for a commit that never came.
|
|
667
|
+
let P = h && (!n || R !== null || (e.h & STATUS_PENDING) !== 0);
|
|
668
|
+
if (P && (!e.we || l)) queuePendingNode(e); else if (P && (activeTransition === null || i) && !(e.h & (STATUS_PENDING | STATUS_UNINITIALIZED))) {
|
|
669
|
+
P = false;
|
|
670
|
+
if (!G) disposeChildren(e, false, true);
|
|
671
|
+
}
|
|
672
|
+
if (P) e.C |= CONFIG_HELD_CHILDREN; else e.C &= ~CONFIG_HELD_CHILDREN;
|
|
673
|
+
// (A born-held pass IS the transaction's staged view — nothing to refresh.)
|
|
674
|
+
if (e.we && t && activeTransition !== e.we && R === null) {
|
|
675
|
+
// The re-run refreshes the transaction's STAGED view (_pendingValue); the
|
|
676
|
+
// value this pass published in _value belongs to the run that just
|
|
677
|
+
// finished. Keep that ownership, or the effect phase parks a
|
|
678
|
+
// mainline-computed value with the transaction (#3412).
|
|
679
|
+
const n = e.pn;
|
|
680
|
+
runInTransition(e.we, () => recompute(e));
|
|
681
|
+
e.pn = n;
|
|
682
|
+
}
|
|
683
|
+
// Missed-wake reschedule (see the finally above): values this pass read
|
|
684
|
+
// before the nested commit are stale, so run again now that the heap will
|
|
685
|
+
// accept the node. Equality gates stop same-value landings from cascading,
|
|
686
|
+
// and a re-run only latches again if another nested commit changes a dep
|
|
687
|
+
// beneath it — convergent unless deps genuinely keep changing.
|
|
688
|
+
if (T) {
|
|
689
|
+
enqueueSub(e);
|
|
690
|
+
schedule();
|
|
691
|
+
}
|
|
327
692
|
}
|
|
328
693
|
|
|
329
694
|
function updateIfNecessary(e) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
695
|
+
// Never re-enter a node that is currently computing: its dep bookkeeping
|
|
696
|
+
// (_depsTail/_depGen) is live, and a nested recompute would corrupt it.
|
|
697
|
+
// A mid-pass mark stays latched for recompute's own tail to reschedule
|
|
698
|
+
// (#3037); readers meanwhile serve the values the pass has so far.
|
|
699
|
+
// Never recompute a DISPOSED node either: recompute rewrites _flags and
|
|
700
|
+
// would resurrect it (#2983) — readers serve its last value.
|
|
701
|
+
if (e.oe & (REACTIVE_RECOMPUTING_DEPS | REACTIVE_DISPOSED)) return;
|
|
702
|
+
if (e.oe & REACTIVE_CHECK) {
|
|
703
|
+
for (let n = e.Ie; n; n = n.Ne) {
|
|
704
|
+
const t = n.Oe;
|
|
705
|
+
const i = t.De || t;
|
|
706
|
+
if (i.he) {
|
|
335
707
|
updateIfNecessary(i);
|
|
336
708
|
}
|
|
337
|
-
if (e.
|
|
709
|
+
if (e.oe & REACTIVE_DIRTY) {
|
|
338
710
|
break;
|
|
339
711
|
}
|
|
340
712
|
}
|
|
341
713
|
}
|
|
342
|
-
if (e.
|
|
714
|
+
if (e.oe & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY) || e.o?._ && e.be < clock && !e.o?.Ce) {
|
|
343
715
|
recompute(e);
|
|
344
716
|
}
|
|
345
|
-
|
|
717
|
+
// The guard above refused an already-disposed node; the recompute it just
|
|
718
|
+
// ran may have disposed it (#3621) — carry the flag, or it comes back alive.
|
|
719
|
+
// The manual-write mask is state, not scheduling (#3612): it says the
|
|
720
|
+
// node's staging is a PROPOSAL, and only the commit (or a later-tick
|
|
721
|
+
// refresh, #3026) lifts it — a pull that recomputed nothing must not, or
|
|
722
|
+
// an isPending()/latest() probe decided whether a later write to a held
|
|
723
|
+
// node was a second proposal or the derivation's `prev`.
|
|
724
|
+
e.oe = e.oe & (REACTIVE_SNAPSHOT_STALE | REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT | REACTIVE_ZOMBIE | REACTIVE_DISPOSED | REACTIVE_MANUAL_WRITE);
|
|
346
725
|
}
|
|
347
726
|
|
|
348
|
-
function computed(e,
|
|
349
|
-
const
|
|
727
|
+
function computed(e, n) {
|
|
728
|
+
const t = n?.transparent ?? false;
|
|
350
729
|
// `in` (not `!== undefined`): an explicit `loadingValue: undefined` on a
|
|
351
730
|
// `T | undefined` node is a real commit #0. The typeof guard tolerates
|
|
352
731
|
// non-object option values that older call shapes force through `as any`.
|
|
353
|
-
const i =
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
732
|
+
const i = n !== null && typeof n === "object" && "loadingValue" in n;
|
|
733
|
+
// Two literals, one per tier, selected at build time (the observe flag is
|
|
734
|
+
// a literal after replacement; the untaken branch is dead code). The observe
|
|
735
|
+
// literal is the prod literal plus its `_name` slot — a slot in the
|
|
736
|
+
// boilerplate, because a post-construction `self._name = …` forces a
|
|
737
|
+
// hidden-class transition and an out-of-object property store on EVERY node
|
|
738
|
+
// (measured: the whole of the observe tier's creation overhead). Keep the
|
|
739
|
+
// two in sync — the dist artifact test pins observe's key set to prod's
|
|
740
|
+
// plus `_name`.
|
|
741
|
+
const u = {
|
|
742
|
+
id: inheritId(n, t, context),
|
|
743
|
+
C: (t ? CONFIG_TRANSPARENT : 0) | (n?.ownedWrite ? CONFIG_OWNED_WRITE : 0) | (!context || n?.lazy ? CONFIG_AUTO_DISPOSE : 0) | (n?.sync ? CONFIG_SYNC : 0) | (n?.H ? CONFIG_NO_SNAPSHOT : 0) | (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
|
|
744
|
+
xe: n?.equals ?? isEqual,
|
|
745
|
+
qe: null,
|
|
746
|
+
T: context?.T ?? globalQueue,
|
|
747
|
+
Je: context?.Je ?? defaultContext,
|
|
748
|
+
En: 0,
|
|
749
|
+
he: e,
|
|
750
|
+
ce: i ? n.loadingValue : undefined,
|
|
751
|
+
rn: 0,
|
|
752
|
+
Pn: undefined,
|
|
753
|
+
Fn: null,
|
|
754
|
+
Ie: null,
|
|
755
|
+
Nn: null,
|
|
756
|
+
Ye: 0,
|
|
366
757
|
u: null,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
rt: null,
|
|
374
|
-
Fe: context,
|
|
375
|
-
He: null,
|
|
376
|
-
ct: null,
|
|
377
|
-
ke: null,
|
|
378
|
-
se: t?.lazy ? REACTIVE_LAZY : REACTIVE_NONE,
|
|
758
|
+
mn: null,
|
|
759
|
+
_parent: context,
|
|
760
|
+
un: null,
|
|
761
|
+
gn: null,
|
|
762
|
+
tn: null,
|
|
763
|
+
oe: n?.lazy ? REACTIVE_LAZY : REACTIVE_NONE,
|
|
379
764
|
// A loadingValue node is born committed: commit #0 is already in _value.
|
|
380
|
-
|
|
381
|
-
|
|
765
|
+
h: i ? 0 : STATUS_UNINITIALIZED,
|
|
766
|
+
be: clock,
|
|
382
767
|
_e: NOT_PENDING,
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
768
|
+
we: null,
|
|
769
|
+
vn: -1,
|
|
770
|
+
ve: i,
|
|
771
|
+
// Cold machinery (async/transition/optimistic/verdict slots) lives one
|
|
772
|
+
// hop away in the lazily-allocated extension — the core literal MUST
|
|
773
|
+
// stay under V8's in-object boundary (§12: past ~39 fields every
|
|
774
|
+
// allocation spills to a backing store and creation cost ~4x's).
|
|
775
|
+
o: null
|
|
776
|
+
};
|
|
777
|
+
if (n?.unobserved) ext(u).Hn = n.unobserved;
|
|
778
|
+
setupComputedNode(u, n);
|
|
779
|
+
return u;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Lazily allocate a node's cold extension (ONE shape for signals and
|
|
783
|
+
* computeds — `_x` access stays monomorphic). Installers write through
|
|
784
|
+
* this; hot paths read `el._x?._field` gated by the _config presence bits.
|
|
785
|
+
* Never call ext() just to store a field's default. */ function ext(e) {
|
|
786
|
+
return e.o ??= {
|
|
787
|
+
Fe: undefined,
|
|
788
|
+
Ln: undefined,
|
|
789
|
+
hn: 0,
|
|
790
|
+
bn: NOT_PENDING,
|
|
791
|
+
Vn: 0,
|
|
792
|
+
me: undefined,
|
|
793
|
+
Ze: undefined,
|
|
794
|
+
He: undefined,
|
|
795
|
+
kn: undefined,
|
|
796
|
+
t: 0,
|
|
797
|
+
Ce: null,
|
|
798
|
+
ye: null,
|
|
799
|
+
_: undefined,
|
|
800
|
+
Pe: undefined,
|
|
801
|
+
ue: undefined,
|
|
802
|
+
S: undefined,
|
|
803
|
+
Le: false,
|
|
804
|
+
i: null,
|
|
805
|
+
Hn: undefined,
|
|
806
|
+
sn: undefined,
|
|
807
|
+
fn: null,
|
|
808
|
+
_n: null,
|
|
809
|
+
Un: undefined
|
|
389
810
|
};
|
|
390
|
-
setupComputedNode(l, t);
|
|
391
|
-
return l;
|
|
392
811
|
}
|
|
393
812
|
|
|
394
813
|
/**
|
|
@@ -396,117 +815,237 @@ function computed(e, t) {
|
|
|
396
815
|
* so V8 sees the full hidden class shape at construction time. Effects always run in lazy
|
|
397
816
|
* mode (recompute is called explicitly by `effect()`), so we hardcode the lazy bits and skip
|
|
398
817
|
* the auto-dispose CONFIG bit (effect() previously cleared it post-construction).
|
|
399
|
-
*/ function createEffectNode(e,
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
818
|
+
*/ function createEffectNode(e, n, t, i, u) {
|
|
819
|
+
const l = u?.transparent ?? false;
|
|
820
|
+
// Prod and observe boilerplates — see computed() for why the observe tier
|
|
821
|
+
// gets its `_name` as a literal slot rather than a write after the fact.
|
|
822
|
+
// The default label is the node kind (tracked effects relabel their computed
|
|
823
|
+
// in trackedEffect); the wrappers in signals.ts no longer spread a name into
|
|
824
|
+
// the options to get it.
|
|
825
|
+
const r = {
|
|
826
|
+
id: inheritId(u, l, context),
|
|
827
|
+
C: (l ? CONFIG_TRANSPARENT : 0) | (u?.ownedWrite ? CONFIG_OWNED_WRITE : 0) | (u?.sync ? CONFIG_SYNC : 0) | (u?.yn ?? 0) | (snapshotCaptureActive && ownerInSnapshotScope(context) ? CONFIG_IN_SNAPSHOT_SCOPE : 0),
|
|
828
|
+
xe: false,
|
|
829
|
+
qe: null,
|
|
830
|
+
T: context?.T ?? globalQueue,
|
|
831
|
+
Je: context?.Je ?? defaultContext,
|
|
832
|
+
En: 0,
|
|
833
|
+
he: e,
|
|
834
|
+
ce: undefined,
|
|
835
|
+
rn: 0,
|
|
836
|
+
Pn: undefined,
|
|
837
|
+
Fn: null,
|
|
838
|
+
Ie: null,
|
|
839
|
+
Nn: null,
|
|
840
|
+
Ye: 0,
|
|
413
841
|
u: null,
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
He: null,
|
|
423
|
-
ct: null,
|
|
424
|
-
ke: null,
|
|
425
|
-
se: REACTIVE_LAZY,
|
|
426
|
-
S: STATUS_UNINITIALIZED,
|
|
427
|
-
de: clock,
|
|
842
|
+
mn: null,
|
|
843
|
+
_parent: context,
|
|
844
|
+
un: null,
|
|
845
|
+
gn: null,
|
|
846
|
+
tn: null,
|
|
847
|
+
oe: REACTIVE_LAZY,
|
|
848
|
+
h: STATUS_UNINITIALIZED,
|
|
849
|
+
be: clock,
|
|
428
850
|
_e: NOT_PENDING,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
Re: i,
|
|
441
|
-
i: l
|
|
851
|
+
we: null,
|
|
852
|
+
vn: -1,
|
|
853
|
+
ve: false,
|
|
854
|
+
Xe: false,
|
|
855
|
+
xn: undefined,
|
|
856
|
+
Qn: n,
|
|
857
|
+
Mn: t,
|
|
858
|
+
wn: undefined,
|
|
859
|
+
je: i,
|
|
860
|
+
pn: null,
|
|
861
|
+
o: null
|
|
442
862
|
};
|
|
443
|
-
|
|
444
|
-
|
|
863
|
+
// Effects dispatch status through the SHARED notifier (statusNotifierOf,
|
|
864
|
+
// keyed off _type) — storing it per node forced a full NodeExtension
|
|
865
|
+
// allocation on EVERY effect at creation (an alloc + 19 field stores,
|
|
866
|
+
// +23% effect creation, caught by the creation benches). Only genuinely
|
|
867
|
+
// per-node channels (boundaries) live on _x.
|
|
868
|
+
if (u?.unobserved) ext(r).Hn = u.unobserved;
|
|
869
|
+
setupComputedNode(r, lazyOptions);
|
|
870
|
+
return r;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* The shared status notifier for effect nodes, installed once by effect.ts
|
|
875
|
+
* at module evaluation (`this`-dispatched — one function serves every
|
|
876
|
+
* effect, so nodes never store it). Boundary computeds keep their own
|
|
877
|
+
* per-node channel on `_x._notifyStatus`, which takes precedence.
|
|
878
|
+
*/ let effectStatusNotify = null;
|
|
879
|
+
|
|
880
|
+
function setEffectStatusNotify(e) {
|
|
881
|
+
effectStatusNotify = e;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Resolve a node's status notifier: an own `_x` channel (boundaries) wins;
|
|
885
|
+
* effect nodes (`_type` — EFFECT_PURE is 0, and only effect literals carry
|
|
886
|
+
* the field) fall back to the shared notifier. Presence doubles as the
|
|
887
|
+
* "display consumer" membership test in the status walks, exactly as the
|
|
888
|
+
* per-node field did when every effect carried one. */ function statusNotifierOf(e) {
|
|
889
|
+
const n = e.o?.S;
|
|
890
|
+
if (n !== undefined) return n;
|
|
891
|
+
return e.je ? effectStatusNotify ?? undefined : undefined;
|
|
445
892
|
}
|
|
446
893
|
|
|
447
894
|
const lazyOptions = {
|
|
448
895
|
lazy: true
|
|
449
896
|
};
|
|
450
897
|
|
|
451
|
-
function setupComputedNode(e,
|
|
452
|
-
e.
|
|
453
|
-
const
|
|
454
|
-
if (context)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (n) e.Le = n.Le + 1;
|
|
465
|
-
if (GlobalQueue.dt !== null) GlobalQueue.dt(e);
|
|
466
|
-
!t?.lazy && recompute(e, true);
|
|
467
|
-
if (snapshotCaptureActive && !t?.lazy) {
|
|
468
|
-
if (!(e.S & STATUS_PENDING) && !(e.T & CONFIG_NO_SNAPSHOT)) {
|
|
469
|
-
e.xe = e.be === undefined ? NO_SNAPSHOT : e.be;
|
|
898
|
+
function setupComputedNode(e, n) {
|
|
899
|
+
e.Fn = e;
|
|
900
|
+
const t = context?.Wn ? context.qn : context;
|
|
901
|
+
if (context) linkChild(context, e);
|
|
902
|
+
if (t) e.rn = t.rn + 1;
|
|
903
|
+
if (GlobalQueue.Zn !== null) GlobalQueue.Zn(e);
|
|
904
|
+
!n?.lazy && recompute(e, true);
|
|
905
|
+
if (snapshotCaptureActive && !n?.lazy) {
|
|
906
|
+
if (!(e.h & STATUS_PENDING) && !(e.C & CONFIG_NO_SNAPSHOT)) {
|
|
907
|
+
ext(e).sn = e.ce === undefined ? NO_SNAPSHOT : e.ce;
|
|
908
|
+
e.C |= CONFIG_HAS_SNAPSHOT;
|
|
470
909
|
snapshotSources.add(e);
|
|
471
910
|
}
|
|
472
911
|
}
|
|
473
912
|
}
|
|
474
913
|
|
|
475
|
-
function signal(e,
|
|
914
|
+
function signal(e, n, t = null) {
|
|
915
|
+
// Prod and observe boilerplates — see computed(). The observe literal adds
|
|
916
|
+
// `_name` and `_owner` (the creating owner, stamped by registerGraph for
|
|
917
|
+
// createSignal nodes so ownerPath can locate signal subjects; null here,
|
|
918
|
+
// and staying null on internal signals — one shape either way).
|
|
476
919
|
const i = {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
_e: NOT_PENDING
|
|
920
|
+
xe: n?.equals ?? isEqual,
|
|
921
|
+
C: (n?.ownedWrite ? CONFIG_OWNED_WRITE : 0) | (n?.H ? CONFIG_NO_SNAPSHOT : 0),
|
|
922
|
+
ce: e,
|
|
923
|
+
u: null,
|
|
924
|
+
mn: null,
|
|
925
|
+
be: clock,
|
|
926
|
+
De: t,
|
|
927
|
+
Ue: t?.o?.i || null,
|
|
928
|
+
Yn: null,
|
|
929
|
+
_e: NOT_PENDING,
|
|
930
|
+
// Signal-literal diet (§12e): NO _time/_fn/_statusFlags slots. Stores
|
|
931
|
+
// materialize one signal per touched leaf, so signal bytes are store
|
|
932
|
+
// bytes. _time is write-only on signals (every read site is computed-
|
|
933
|
+
// typed error-retry gating); _fn/_statusFlags read falsy-identically as
|
|
934
|
+
// missing properties on the shared paths (undefined masks to 0).
|
|
935
|
+
we: null,
|
|
936
|
+
vn: -1,
|
|
937
|
+
o: null
|
|
487
938
|
};
|
|
488
|
-
n
|
|
489
|
-
if (
|
|
490
|
-
|
|
939
|
+
if (n?.unobserved) ext(i).Hn = n.unobserved;
|
|
940
|
+
if (t) linkFirewallChild(t, i);
|
|
941
|
+
if (snapshotCaptureActive && !(i.C & CONFIG_NO_SNAPSHOT) && !((t?.h ?? 0) & STATUS_PENDING)) {
|
|
942
|
+
ext(i).sn = e === undefined ? NO_SNAPSHOT : e;
|
|
943
|
+
i.C |= CONFIG_HAS_SNAPSHOT;
|
|
491
944
|
snapshotSources.add(i);
|
|
492
945
|
}
|
|
493
946
|
return i;
|
|
494
947
|
}
|
|
495
948
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
949
|
+
// ---------------------------------------------------------------------------
|
|
950
|
+
// SLOT SIGNALS (store leaves) — the create-floor diet. Store mounts
|
|
951
|
+
// materialize one signal per touched leaf (~13 × rows on dbmon), so per-node
|
|
952
|
+
// allocations are mount bytes: the generic path costs an options object, an
|
|
953
|
+
// equals closure, an unobserved closure, a NodeExtension to hold it, and
|
|
954
|
+
// three post-construction expandos (acc/px/pxv → hidden-class transitions).
|
|
955
|
+
// slotSignal bakes everything into ONE literal: `_host`/`_key` backrefs
|
|
956
|
+
// replace the closures (equals is a method call — `this` is the node; the
|
|
957
|
+
// unobserved sweep dispatches CONFIG_SLOT_NODE to one shared hook), and the
|
|
958
|
+
// store's wrap-cache fields are pre-shaped.
|
|
959
|
+
/** The shared slot-node unobserved handler — a live binding read directly by
|
|
960
|
+
* the sweep sites (no wrapper frame, no null check: a CONFIG_SLOT_NODE node
|
|
961
|
+
* existing implies the store module loaded and registered the hook). */ let slotUnobservedHook;
|
|
962
|
+
|
|
963
|
+
/** Install the shared slot-node unobserved handler (store module, once). */ function setSlotUnobserved(e) {
|
|
964
|
+
slotUnobservedHook = e;
|
|
500
965
|
}
|
|
501
966
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
967
|
+
/** Push a new node onto its firewall's child chain (the literal already
|
|
968
|
+
* points `_nextChild` at the old head). Doubly linked so a released leaf
|
|
969
|
+
* unlinks in O(1) — the chain is walked per mark of the projection and
|
|
970
|
+
* would otherwise grow by one node per leaf ever read (#3351). */ function linkFirewallChild(e, n) {
|
|
971
|
+
const t = n.Ue;
|
|
972
|
+
if (t !== null) t.Yn = n;
|
|
973
|
+
ext(e).i = n;
|
|
974
|
+
e.C |= CONFIG_FW_CHILDREN;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** Release a firewall child the store no longer addresses (unobserved sweep
|
|
978
|
+
* dropped it from its target's cache): unlink it from the chain so the
|
|
979
|
+
* projection stops retaining it and its last value. The node keeps its own
|
|
980
|
+
* `_nextChild` so a walk that is mid-chain on it still terminates. It also
|
|
981
|
+
* leaves `_companionChildren` (#3503): the companions themselves are
|
|
982
|
+
* permanent on the node, but the node is unreachable through the store, so
|
|
983
|
+
* the set would only retain it and its last value. */ function unlinkFirewallChild(e) {
|
|
984
|
+
const n = e;
|
|
985
|
+
const t = n.De;
|
|
986
|
+
if (!t) return;
|
|
987
|
+
const i = n.Yn;
|
|
988
|
+
const u = n.Ue;
|
|
989
|
+
if (i !== null) i.Ue = u; else if (t.o.i === n) t.o.i = u;
|
|
990
|
+
if (u !== null) u.Yn = i;
|
|
991
|
+
n.Yn = null;
|
|
992
|
+
// #3503: the companion set is the last projection-side reference to a
|
|
993
|
+
// released leaf; leaving it there kept the leaf and its last value alive
|
|
994
|
+
// for the projection's lifetime after every latest()/isPending() reader
|
|
995
|
+
// was disposed.
|
|
996
|
+
t.o.Un?.delete(n);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function slotSignal(e, n, t, i, u, l = null) {
|
|
1000
|
+
// Prod and observe boilerplates — see computed(). The store relabels the
|
|
1001
|
+
// observe slot (`store.<key>`) when the attribution engine is installed.
|
|
1002
|
+
const r = {
|
|
1003
|
+
xe: n,
|
|
1004
|
+
C: CONFIG_OWNED_WRITE | CONFIG_SLOT_NODE,
|
|
1005
|
+
ce: e,
|
|
1006
|
+
u: null,
|
|
1007
|
+
mn: null,
|
|
1008
|
+
be: clock,
|
|
1009
|
+
De: l,
|
|
1010
|
+
Ue: l?.o?.i || null,
|
|
1011
|
+
Yn: null,
|
|
1012
|
+
_e: NOT_PENDING,
|
|
1013
|
+
we: null,
|
|
1014
|
+
vn: -1,
|
|
1015
|
+
o: null,
|
|
1016
|
+
// Slot backrefs: what the equals/unobserved closures used to capture.
|
|
1017
|
+
Bn: t,
|
|
1018
|
+
Kn: i,
|
|
1019
|
+
// Store read-path caches, pre-shaped (were post-construction expandos).
|
|
1020
|
+
acc: u,
|
|
1021
|
+
px: undefined,
|
|
1022
|
+
pxv: undefined
|
|
1023
|
+
};
|
|
1024
|
+
if (l) linkFirewallChild(l, r);
|
|
1025
|
+
if (snapshotCaptureActive && !((l?.h ?? 0) & STATUS_PENDING)) {
|
|
1026
|
+
ext(r).sn = e === undefined ? NO_SNAPSHOT : e;
|
|
1027
|
+
r.C |= CONFIG_HAS_SNAPSHOT;
|
|
1028
|
+
snapshotSources.add(r);
|
|
1029
|
+
}
|
|
1030
|
+
return r;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function optimisticSignal(e, n) {
|
|
1034
|
+
const t = signal(e, n);
|
|
1035
|
+
ext(t).Fe = NOT_PENDING;
|
|
1036
|
+
t.C |= CONFIG_OPTIMISTIC;
|
|
1037
|
+
return t;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function optimisticComputed(e, n) {
|
|
1041
|
+
const t = computed(e, n);
|
|
1042
|
+
ext(t).Fe = NOT_PENDING;
|
|
1043
|
+
t.C |= CONFIG_OPTIMISTIC;
|
|
1044
|
+
return t;
|
|
506
1045
|
}
|
|
507
1046
|
|
|
508
|
-
function isEqual(e,
|
|
509
|
-
return e ===
|
|
1047
|
+
function isEqual(e, n) {
|
|
1048
|
+
return e === n;
|
|
510
1049
|
}
|
|
511
1050
|
|
|
512
1051
|
/**
|
|
@@ -530,15 +1069,41 @@ function isEqual(e, t) {
|
|
|
530
1069
|
* }
|
|
531
1070
|
* );
|
|
532
1071
|
* ```
|
|
533
|
-
*/ function untrack(e,
|
|
534
|
-
if (GlobalQueue.
|
|
535
|
-
const
|
|
1072
|
+
*/ function untrack(e, n) {
|
|
1073
|
+
if (GlobalQueue.jn === null && !tracking && true) return e();
|
|
1074
|
+
const t = tracking;
|
|
536
1075
|
tracking = false;
|
|
537
1076
|
try {
|
|
538
|
-
if (GlobalQueue.
|
|
1077
|
+
if (GlobalQueue.jn !== null) return GlobalQueue.jn(e);
|
|
539
1078
|
return e();
|
|
540
1079
|
} finally {
|
|
541
|
-
tracking =
|
|
1080
|
+
tracking = t;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Set while runtime bookkeeping reads a node inside another node's pass (a
|
|
1086
|
+
* loading boundary priming its tree at creation, from whatever pass is
|
|
1087
|
+
* mounting it). `context` is that node, but the read is nobody's: the value
|
|
1088
|
+
* is probed, never derived from, so nothing the read would normally record
|
|
1089
|
+
* on `context` may be recorded — not the untracked-pending re-run link
|
|
1090
|
+
* (`read`, `!tracking`; #3528: a boundary's `on` key read this way from
|
|
1091
|
+
* `notify` linked the key's source into an unrelated async memo, a cycle that
|
|
1092
|
+
* never converged), and not a transaction entry (`enterStagedRead`; #3540: a
|
|
1093
|
+
* born-held tree would otherwise pull the mounting pass into the hold).
|
|
1094
|
+
*/ let spectating = false;
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Evaluates `fn` untracked, recording nothing on the current `context`: no
|
|
1098
|
+
* untracked-pending re-run link, no transaction entry. For bookkeeping reads
|
|
1099
|
+
* made on behalf of no node — see `spectating`.
|
|
1100
|
+
*/ function spectate(e) {
|
|
1101
|
+
const n = spectating;
|
|
1102
|
+
spectating = true;
|
|
1103
|
+
try {
|
|
1104
|
+
return untrack(e);
|
|
1105
|
+
} finally {
|
|
1106
|
+
spectating = n;
|
|
542
1107
|
}
|
|
543
1108
|
}
|
|
544
1109
|
|
|
@@ -546,13 +1111,33 @@ function isEqual(e, t) {
|
|
|
546
1111
|
* Bring a computed to a readable state: lazy/disposed nodes are (re)computed;
|
|
547
1112
|
* an isPending() probe (`refresh`) additionally pulls the node fully up to
|
|
548
1113
|
* date so its status flags reflect the current graph.
|
|
549
|
-
*/ function prepareComputed(e,
|
|
550
|
-
if (e.
|
|
551
|
-
e.
|
|
552
|
-
recompute(e, true);
|
|
553
|
-
} else if (e.se & REACTIVE_DISPOSED) {
|
|
1114
|
+
*/ function prepareComputed(e, n) {
|
|
1115
|
+
if (e.oe & REACTIVE_LAZY) {
|
|
1116
|
+
e.oe &= ~REACTIVE_LAZY;
|
|
554
1117
|
recompute(e, true);
|
|
555
|
-
} else if (
|
|
1118
|
+
} else if (e.oe & REACTIVE_DISPOSED) {
|
|
1119
|
+
// Two disposal lifecycles share the flag (#3024). Observation-lifecycle
|
|
1120
|
+
// nodes (CONFIG_AUTO_DISPOSE) are dormant — torn down by unobserved()
|
|
1121
|
+
// when the last subscriber left — and reads reawaken them; that is the
|
|
1122
|
+
// pay-for-use contract. Owner-lifecycle nodes are dead: recomputing would
|
|
1123
|
+
// re-run user code in a torn-down tree (and discard manual writes on
|
|
1124
|
+
// derived-writable signals), so reads return the last committed value.
|
|
1125
|
+
if (e.C & CONFIG_AUTO_DISPOSE) {
|
|
1126
|
+
const n = e._parent;
|
|
1127
|
+
if (n !== null) {
|
|
1128
|
+
// A dormant node was off the chain when its owner died, so the strip
|
|
1129
|
+
// in disposeChildren missed it: freeze here instead (#3024).
|
|
1130
|
+
if (n.oe & REACTIVE_DISPOSED) {
|
|
1131
|
+
e.C &= ~CONFIG_AUTO_DISPOSE;
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
// A zombie never left its chain: settleAutodispose releases without
|
|
1135
|
+
// the !ZOMBIE check its siblings have, so this guard is load-bearing.
|
|
1136
|
+
if (!(e.oe & REACTIVE_ZOMBIE)) linkChild(n, e);
|
|
1137
|
+
}
|
|
1138
|
+
recompute(e, true);
|
|
1139
|
+
}
|
|
1140
|
+
} else if (n) {
|
|
556
1141
|
updateIfNecessary(e);
|
|
557
1142
|
}
|
|
558
1143
|
}
|
|
@@ -570,16 +1155,330 @@ function isEqual(e, t) {
|
|
|
570
1155
|
* fast path, `_fn` nodes need prepareComputed, and firewall / override /
|
|
571
1156
|
* snapshot / transition / lane / dev-strictRead state all take the full
|
|
572
1157
|
* resolution. Anything slow returns READ_SLOW; the caller then calls read().
|
|
573
|
-
*/
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1158
|
+
*/
|
|
1159
|
+
/**
|
|
1160
|
+
* Wake only authoritative-view readers (until() predicates) subscribed to `el`.
|
|
1161
|
+
* The A17-silent ack paths — an authoritative arrival equal to the active
|
|
1162
|
+
* override — use this so the predicate re-evaluates without re-firing
|
|
1163
|
+
* ordinary subscribers whose visible (override) value did not change.
|
|
1164
|
+
* Pay-for-use: reached through GlobalQueue._notifyAuthoritativeObservers,
|
|
1165
|
+
* installed at first until() call — apps that never use until() shake it.
|
|
1166
|
+
*/ function notifyAuthoritativeObservers(e) {
|
|
1167
|
+
for (let n = e.u; n !== null; n = n.Ae) {
|
|
1168
|
+
const e = n.ge;
|
|
1169
|
+
if (!(e.C & CONFIG_AUTHORITATIVE_READ)) continue;
|
|
1170
|
+
// Missed-wake latch (#3037), same contract as insertSubs: the reader may
|
|
1171
|
+
// itself have pulled this recompute (updateIfNecessary from its own
|
|
1172
|
+
// read), and the heap refuses RECOMPUTING nodes — latch so recompute's
|
|
1173
|
+
// tail reschedules it with the staged value visible.
|
|
1174
|
+
if (e.oe & REACTIVE_RECOMPUTING_DEPS && n.Be === e.Ye && n !== e.Nn) e.oe |= REACTIVE_MISSED_WAKE;
|
|
1175
|
+
enqueueSub(e);
|
|
1176
|
+
}
|
|
1177
|
+
schedule();
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/** Installs the authoritative-reader wakeup hook. Idempotent; called by every
|
|
1181
|
+
* creator of a CONFIG_AUTHORITATIVE_READ computation — until() and refresh() —
|
|
1182
|
+
* before its first read (same late-binding contract as the optimistic engine;
|
|
1183
|
+
* the gating bit is only ever set by such a read, so the `!` call sites are
|
|
1184
|
+
* safe once every setter installs, #3303). */ function installAuthoritativeRead() {
|
|
1185
|
+
if (GlobalQueue.zn === null) GlobalQueue.zn = notifyAuthoritativeObservers;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Stale-reader term of the value selections below: a render effect reading a
|
|
1190
|
+
* node some OTHER live transaction has staged sees the committed value. The
|
|
1191
|
+
* commit is silent — the staging walk was the notification — so a reader
|
|
1192
|
+
* that linked AFTER that walk (an effect created during the hold, a store
|
|
1193
|
+
* key first read under it) would show the old value past the reveal: record
|
|
1194
|
+
* it for the transaction's commit replay (the `_gatedSubs` contract lanes
|
|
1195
|
+
* already use). An effect the transaction itself computed re-derives at its
|
|
1196
|
+
* commit on its own (parked run, or the contested re-derive, #3322) and is
|
|
1197
|
+
* not recorded — replaying it too would publish the frame twice.
|
|
1198
|
+
*
|
|
1199
|
+
* Flight twin (the pending-branch carve-out): the reader is served the
|
|
1200
|
+
* node's committed, pre-flight value and now observes that flight — A15:
|
|
1201
|
+
* async work observed by a reader settles as one unit with the writes that
|
|
1202
|
+
* asked it — so it joins the transaction's reporters for the node. The
|
|
1203
|
+
* reporter the transaction recorded when the flight started may be gone (a
|
|
1204
|
+
* keyed remount disposed it, #3374); a completion check that found no live
|
|
1205
|
+
* reporter committed the writes ahead of the answer, tearing the new
|
|
1206
|
+
* reader's frame (`Count: 1` beside `Details: 0`). Joins an entry the
|
|
1207
|
+
* transaction already holds; a flight nobody had observed yet has none (the
|
|
1208
|
+
* reader is its first observer — a conditional that just revealed it, #3458)
|
|
1209
|
+
* and is notified up the reader's own queue chain under that transaction,
|
|
1210
|
+
* the one sanctioned registration site (INV-3): a collecting boundary above
|
|
1211
|
+
* the reader consumes it as it would any pending, an unboundaried reader
|
|
1212
|
+
* opens the entry — and the transaction, judged complete on its other
|
|
1213
|
+
* flights, revealed the inputs beside the reader's pre-flight value
|
|
1214
|
+
* otherwise (`Count: 1 | A: 1` beside `B: 0`). A staged signal or a settled
|
|
1215
|
+
* node registers nothing. Every reporter dies with its reader
|
|
1216
|
+
* (reporterBlocksSource: the read linked it as a dep). The node's own entry
|
|
1217
|
+
* is the only one that can matter: a chain's intermediate memo is re-pulled
|
|
1218
|
+
* by the read (updateIfNecessary's retry) and enters the transaction, so the
|
|
1219
|
+
* reader holds through the normal path; a node with its own flight that is
|
|
1220
|
+
* also pending on an upstream re-ask blocks through that flight until it
|
|
1221
|
+
* lands, and its landing re-runs the reader into the normal path.
|
|
1222
|
+
*/
|
|
1223
|
+
/** The replay half of the stale-of-foreign clause (A15 / A26): a stale reader
|
|
1224
|
+
* served the committed value because `txn` holds what it read re-runs at
|
|
1225
|
+
* txn's commit, when the value it was denied becomes the frame — unless its
|
|
1226
|
+
* own last value already came from that transaction. One registration for
|
|
1227
|
+
* the node path (heldFromStale) and the store's backing paths, which have
|
|
1228
|
+
* no node to carry the hold (heldFromReader, the adoption hold view). */ function recordStaleReplay(e, n) {
|
|
1229
|
+
const t = n.pn;
|
|
1230
|
+
if (t == null || currentTransition(t) !== e) e.In.add(n);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* The ownership relation (DESIGN-CONSOLIDATION §6, ruled 2026-09-17): is
|
|
1235
|
+
* `hold` part of the running pass's world? A plain reader's world is the
|
|
1236
|
+
* transaction it runs under, through merges. A lane reader's world is its
|
|
1237
|
+
* lane AND the transition that owns the lane — the one asymmetry between a
|
|
1238
|
+
* lane and a separate transaction (a lane sees what lands from its parent as
|
|
1239
|
+
* its own; a separate transaction would wait for the parent to settle) —
|
|
1240
|
+
* see `ownsLane` in lanes.ts, built on this. One relation for the
|
|
1241
|
+
* stale-of-foreign clause (heldFromStale), the lane arm (readsHeldCommitted)
|
|
1242
|
+
* and the store's backing holds (foreignHold); `serve` has no lane arm of
|
|
1243
|
+
* its own, the lane's extra visibility lives here.
|
|
1244
|
+
*/ function ownsHold(e) {
|
|
1245
|
+
return activeTransition !== null && currentTransition(e) === currentTransition(activeTransition);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function heldFromStale(e, n) {
|
|
1249
|
+
const t = e.we;
|
|
1250
|
+
if (t === null || ownsHold(t)) return false;
|
|
1251
|
+
const i = currentTransition(t);
|
|
1252
|
+
recordStaleReplay(i, n);
|
|
1253
|
+
const u = i.le.get(e);
|
|
1254
|
+
if (u) u.add(n); else if (e.h & STATUS_PENDING) runInTransition(i, () => n.T.notify(n, STATUS_PENDING, STATUS_PENDING, e.o._));
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* A tracked computation about to be served a live transaction's staged value
|
|
1260
|
+
* derives from that transaction's world, so it enters the transaction and
|
|
1261
|
+
* this pass's result is held with it (A29, #3408). The write side (`setSignal`)
|
|
1262
|
+
* and a stamped node's recompute already enter; a reader that only now
|
|
1263
|
+
* starts reading the held node — a conditional memo whose branch flipped —
|
|
1264
|
+
* was the gap: it published a value derived from the staged world into the
|
|
1265
|
+
* mainline frame. Not for a probe (`isPending(() => x())` observes, it does
|
|
1266
|
+
* not derive) and a no-op for the ambient batch (`_transition` null) or the
|
|
1267
|
+
* transaction already active.
|
|
1268
|
+
*/
|
|
1269
|
+
/** The transaction a pass running OUTSIDE a flush was served a staged value
|
|
1270
|
+
* from (A29, creation-time form). Inside a flush the pass enters through
|
|
1271
|
+
* initTransition; outside one — a memo or effect created from mainline code
|
|
1272
|
+
* while a hold is live — entering would leave `activeTransition` and the
|
|
1273
|
+
* batch pointed at the transaction for the rest of the synchronous block,
|
|
1274
|
+
* so an unrelated write made after the mount was held with someone else's
|
|
1275
|
+
* action. The entry is the pass's alone: recompute stages the node into the
|
|
1276
|
+
* transaction (born held) and mainline is never touched.
|
|
1277
|
+
*
|
|
1278
|
+
* A pass under a FRESH loading boundary takes this path inside a flush too
|
|
1279
|
+
* (#3540). Born held is right for a plain memo or effect: published, its
|
|
1280
|
+
* value would tear the frame. A boundary that has not revealed yet is the
|
|
1281
|
+
* exception by definition — its job is to catch what is not ready under it
|
|
1282
|
+
* rather than let it hold: the pass is staged into the transaction as
|
|
1283
|
+
* above, and the boundary is told (recompute's born-held arm notifies it
|
|
1284
|
+
* with a `NotReadyError` sourced at the node) so it shows its fallback now
|
|
1285
|
+
* and reveals the staged result at the commit.
|
|
1286
|
+
* Entering instead adopted the whole flush into the hold — a `Show` that
|
|
1287
|
+
* flipped mainline to mount a `Loading` over a held value waited for the
|
|
1288
|
+
* hold with it. A boundary that already shows content keeps the entering
|
|
1289
|
+
* path: it has content to keep, and holds like any reader. */ let stagedEntry = null;
|
|
1290
|
+
|
|
1291
|
+
/** Is `el` routed to a loading boundary that still shows its fallback — the
|
|
1292
|
+
* nearest pending-collecting queue up its chain is uninitialized? (Measured
|
|
1293
|
+
* 2026-09-18: relocating the walk behind a `GlobalQueue` slot installed by
|
|
1294
|
+
* boundaries.ts saved ~8 B on the core floor and cost boundary-using apps
|
|
1295
|
+
* 50–70 B — the indirection's tokens outweigh the walk. Kept inline.) */ function underFreshLoadingBoundary(e) {
|
|
1296
|
+
for (let n = e.T; n !== null; n = n._parent) if (n.te & STATUS_PENDING) return !n.q;
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function enterStagedRead(e, n = e.we) {
|
|
1301
|
+
if (!n || n === activeTransition || pendingCheckActive) return;
|
|
1302
|
+
// A companion (the latest() shadow, the isPending() verdict signal) is the
|
|
1303
|
+
// engine's mirror of the flushed world — reading it, or being it, is an
|
|
1304
|
+
// observation, not a derivation from the hold: latest(x) never enters x's
|
|
1305
|
+
// transaction, and the shadow's own pass never enters either (it would
|
|
1306
|
+
// flip activeTransition under the reader that pulled it). (`el` is null for
|
|
1307
|
+
// a store backing served under a hold — no node, the transaction is the
|
|
1308
|
+
// fold's.)
|
|
1309
|
+
if (e?.o?.kn || context?.o?.kn) return;
|
|
1310
|
+
// A bookkeeping read (`spectate`) is nobody's derivation: it compares or
|
|
1311
|
+
// probes, and enters nothing — the boundary priming read of a born-held
|
|
1312
|
+
// tree must not enter the creator's pass into the hold (#3540).
|
|
1313
|
+
if (spectating) return;
|
|
1314
|
+
// Verdict machinery (GlobalQueue._verdictPull: companion creation and the
|
|
1315
|
+
// latest()/isPending() pulls — the latest() shadow is created before it is
|
|
1316
|
+
// marked optimistic, so the bit alone cannot tell) and optimistic nodes
|
|
1317
|
+
// (own lane posture, A31) read staged truth by design; they keep the
|
|
1318
|
+
// entering path.
|
|
1319
|
+
// (`context` is non-null here: every caller selected a value for a reader.)
|
|
1320
|
+
const t = context;
|
|
1321
|
+
const i = activeTransition === null && !globalQueue.Jn;
|
|
1322
|
+
// Verdict pulls are observations, not derivations: a latest() /
|
|
1323
|
+
// isPending() call from mainline must never enter a transaction (it
|
|
1324
|
+
// would capture the rest of the caller's synchronous block).
|
|
1325
|
+
if (i && GlobalQueue.Xn) return;
|
|
1326
|
+
if (t.oe & REACTIVE_RECOMPUTING_DEPS && !(t.C & CONFIG_OPTIMISTIC) && (stagedEntry === null || stagedEntry === n) && (i || !GlobalQueue.Xn && underFreshLoadingBoundary(t))) {
|
|
1327
|
+
stagedEntry = n;
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
globalQueue.initTransition(n);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Rule 1 (value selection), the full arm: does this reader see a STAGED
|
|
1335
|
+
* node's COMMITTED value? One implementation of the rule the fast paths
|
|
1336
|
+
* (readNodeFast, read's fast block) carry as their trivial ternary and that
|
|
1337
|
+
* every slow site — read's tail, the store's backing selection, the lane and
|
|
1338
|
+
* verdict arms — used to restate by hand (docs/DESIGN-CONSOLIDATION.md, move 3b). In order:
|
|
1339
|
+
* - no reader at all (an untracked read) — the committed frame;
|
|
1340
|
+
* - a reader under an optimistic lane the engine says reads committed
|
|
1341
|
+
* (laneReadsCommitted: another lane's hold, #3460);
|
|
1342
|
+
* - nothing staged;
|
|
1343
|
+
* - a children-forbidden reader (createTrackedEffect / onSettled: the frame,
|
|
1344
|
+
* never the graph — A32);
|
|
1345
|
+
* - a stale reader (render effect) of a FOREIGN transaction's staged write —
|
|
1346
|
+
* committed, no entanglement (heldFromStale registers the replay; a node
|
|
1347
|
+
* born held has no committed frame to fall back to, `noCommitted`);
|
|
1348
|
+
* - HELD truth (#3164, CONFIG_HELD_TRUTH) read by a LANE pass: staged
|
|
1349
|
+
* confirming truth — fold-staged onto an armed family, or entangle-stolen
|
|
1350
|
+
* by an awaited until() — is masked from lane passes only, owning
|
|
1351
|
+
* transaction or not. A lane applies its frame display-ahead at the park,
|
|
1352
|
+
* so a lane pass served the truth would paint the confirmation beside the
|
|
1353
|
+
* optimism it confirms (`saving=true` beside the saved row — the #3164
|
|
1354
|
+
* tear); it keeps committed and is re-run by the reveal's post-revert
|
|
1355
|
+
* wake. Every other deriving reader falls through to A29 below: the truth
|
|
1356
|
+
* is a staged value like any other, and the pass that derives from it is
|
|
1357
|
+
* held with it — including the retaining transaction's own passes, which
|
|
1358
|
+
* a superseded override already hands the truth (#3568: masking them
|
|
1359
|
+
* composed the landed `length` with rows still masked to committed).
|
|
1360
|
+
* latest() and authoritative readers (until()'s predicate) tunnel
|
|
1361
|
+
* through — the tunnel that keeps the hold deadlock-free.
|
|
1362
|
+
* False means the reader derives from the staged value and enters its
|
|
1363
|
+
* transaction (enterStagedRead, A29).
|
|
1364
|
+
*/ function readerSeesCommitted(e, n, t, i) {
|
|
1365
|
+
return !!(!n || currentOptimisticLane !== null && GlobalQueue.$n(e, t, n) || e._e === NOT_PENDING || n.C & CONFIG_CHILDREN_FORBIDDEN || stale && !i && heldFromStale(e, n) || e.C & CONFIG_HELD_TRUTH && currentOptimisticLane !== null && !latestReadActive && !(n.C & CONFIG_AUTHORITATIVE_READ));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/** A28 — set when a node is staged (queuePendingNode) or a held node rewritten
|
|
1369
|
+
* (stashHeldRewrite) OUTSIDE a flush; cleared when the next flush begins. The
|
|
1370
|
+
* read sites test this one module boolean instead of `globalQueue._running`:
|
|
1371
|
+
* inside a flush it is false and the A28 arm costs nothing; outside, only a
|
|
1372
|
+
* tick with unflushed writes pays the staged-node check. */ let unflushedStaged = false;
|
|
1373
|
+
|
|
1374
|
+
function markUnflushedStaged() {
|
|
1375
|
+
unflushedStaged = true;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/** A28 — a write becomes visible at flush. Outside a flush, a node holding an
|
|
1379
|
+
* AMBIENT staged value (no transaction stamp) was written since the last
|
|
1380
|
+
* flush: ambient staging commits at flush end, so nothing else leaves a node
|
|
1381
|
+
* in this state; a stamped value is the flushed held world, which A28 says
|
|
1382
|
+
* latest() serves. Inside a flush the rule does not apply (A28 (4): promoted
|
|
1383
|
+
* within the round). Structural — no marker on the write path. */ function unflushed(e) {
|
|
1384
|
+
return unflushedValue(e) !== NOT_PENDING;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/** The value an unflushed node serves — the committed value for an ambient
|
|
1388
|
+
* write, the flushed staged value for a rewrite of a held node — or
|
|
1389
|
+
* NOT_PENDING when nothing is unflushed. Exempt: owned-write nodes (A28 (4):
|
|
1390
|
+
* a write issued inside a recompute is promoted at that recompute's end —
|
|
1391
|
+
* boundary and loading machinery, until()'s internals, signals declared for
|
|
1392
|
+
* in-computation writes) and engine companions (the isPending() verdict
|
|
1393
|
+
* signal, the latest() shadow: the system's own writes, made at the source's
|
|
1394
|
+
* write to mirror it, installing eagerly — A28, A8). */ function unflushedValue(e, n = e.ce) {
|
|
1395
|
+
if (globalQueue.Jn || e._e === NOT_PENDING || e.C & CONFIG_PROMOTED || e.o?.kn) return NOT_PENDING;
|
|
1396
|
+
// Ambient, or adopted by a transaction before any flush carried the staging
|
|
1397
|
+
// (CONFIG_ADOPTED_UNFLUSHED): nothing flushed is staged — the committed
|
|
1398
|
+
// value answers (the caller's notion of committed: a store node's backing).
|
|
1399
|
+
// A held node: unflushed only if rewritten since the last flush (stash).
|
|
1400
|
+
if (e.we === null || e.C & CONFIG_ADOPTED_UNFLUSHED) return n;
|
|
1401
|
+
return e.o === null ? NOT_PENDING : e.o.bn;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** Held nodes rewritten since the last flush (setSignal); the flush clears
|
|
1405
|
+
* their stash — from then on latest() answers with the rewrite. */ const unflushedRewrites = [];
|
|
1406
|
+
|
|
1407
|
+
/** Nodes written inside a creation-time recompute (CONFIG_PROMOTED). */ const promotedWrites = [];
|
|
1408
|
+
|
|
1409
|
+
/** A28 (5): an optimistic write becomes the ACTIVE override at the flush that
|
|
1410
|
+
* carries it. `_overrideTime` is stamped with `clock` at the write and `clock`
|
|
1411
|
+
* advances after every flush, so "this tick, outside a flush" is unflushed. */ function unflushedOverride(e) {
|
|
1412
|
+
// Companions are optimistic signals written by the engine (see unflushed).
|
|
1413
|
+
return !globalQueue.Jn && e.o?.hn === clock && !e.o?.kn;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** Active optimistic override on an armed node (an armed slot idles at
|
|
1417
|
+
* NOT_PENDING; undefined = unarmed plain node). The writer's own channels —
|
|
1418
|
+
* the draft, `in`/keys inside the setter — compose on this regardless of
|
|
1419
|
+
* flush state. */ function hasActiveOverride(e) {
|
|
1420
|
+
const n = e.o;
|
|
1421
|
+
return n !== null && n.Fe !== undefined && n.Fe !== NOT_PENDING;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/** The override a READER sees: installed, and carried by a flush (A28 (5) —
|
|
1425
|
+
* an optimistic write is a write; until its flush no reader sees it). One
|
|
1426
|
+
* implementation for read()'s override arm, the verdict channels
|
|
1427
|
+
* (latestRead, computePendingState) and the store's selection
|
|
1428
|
+
* (docs/DESIGN-CONSOLIDATION.md, move 3b). */ function visibleOverride(e) {
|
|
1429
|
+
return hasActiveOverride(e) && !unflushedOverride(e);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** A derivation served the committed value because of an unflushed write
|
|
1433
|
+
* (A28) must run again in the flush that carries it — the late-linker case
|
|
1434
|
+
* (#3337's reason to defer the walk): it linked after the write walked. */ function markLateLinker(e) {
|
|
1435
|
+
// The pass's own tail re-enqueues on this latch (recompute's finally) —
|
|
1436
|
+
// a direct enqueue here would be wiped by the pass's flag reset.
|
|
1437
|
+
e.oe |= REACTIVE_MISSED_WAKE;
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/** Companion-bearing nodes written outside a flush (setSignal); the flush
|
|
1442
|
+
* that carries their writes re-syncs their companions (A28). */ const unflushedCompanions = [];
|
|
1443
|
+
|
|
1444
|
+
function resyncUnflushedCompanions() {
|
|
1445
|
+
unflushedStaged = false;
|
|
1446
|
+
// Length-guarded: the common flush has nothing here, and must allocate nothing.
|
|
1447
|
+
// Length-guarded: the common flush has nothing here and allocates nothing.
|
|
1448
|
+
if (unflushedRewrites.length !== 0) {
|
|
1449
|
+
for (const e of unflushedRewrites) e.o.bn = NOT_PENDING;
|
|
1450
|
+
unflushedRewrites.length = 0;
|
|
1451
|
+
}
|
|
1452
|
+
if (promotedWrites.length !== 0) {
|
|
1453
|
+
for (const e of promotedWrites) e.C &= ~CONFIG_PROMOTED;
|
|
1454
|
+
promotedWrites.length = 0;
|
|
1455
|
+
}
|
|
1456
|
+
if (unflushedCompanions.length !== 0) {
|
|
1457
|
+
for (const e of unflushedCompanions) GlobalQueue.Ve(e, e._e !== NOT_PENDING ? e._e : e.ce);
|
|
1458
|
+
unflushedCompanions.length = 0;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function readNodeFast(e) {
|
|
1463
|
+
if (latestReadActive || pendingCheckActive || e.he || e.De || e.o?.Fe !== undefined || e.o?.sn !== undefined || activeTransition !== null || currentOptimisticLane !== null || snapshotCaptureActive ||
|
|
1464
|
+
// A28: a staged node read outside a flush may serve its committed value;
|
|
1465
|
+
// that arm lives on the slow path so this body stays inlinable.
|
|
1466
|
+
unflushedStaged && e._e !== NOT_PENDING || false) return READ_SLOW;
|
|
1467
|
+
let n = context;
|
|
1468
|
+
if (n?.Wn) n = n.qn;
|
|
1469
|
+
if (n && tracking) link(e, n);
|
|
578
1470
|
// Children-forbidden readers (createTrackedEffect / onSettled callbacks) get
|
|
579
1471
|
// committed visibility: like the effect half of createEffect and event
|
|
580
1472
|
// handlers, effect-phase code never observes its own unsettled write — the
|
|
581
1473
|
// write lands in the same flush's continuation (#3006).
|
|
582
|
-
|
|
1474
|
+
// A stale reader (render effect) recomputing with no transaction active is
|
|
1475
|
+
// mainline: a write staged by a live transaction (`_transition` stamped —
|
|
1476
|
+
// ambient staging never is) stays masked until that transaction's reveal,
|
|
1477
|
+
// the same rule the slow path applies (#3322). Without it a zombie
|
|
1478
|
+
// recompute, or a commit-time re-derive of a contested effect, published
|
|
1479
|
+
// another transaction's uncommitted value.
|
|
1480
|
+
return !n || e._e === NOT_PENDING || n.C & CONFIG_CHILDREN_FORBIDDEN || stale && heldFromStale(e, n) ? e.ce : (enterStagedRead(e),
|
|
1481
|
+
e._e);
|
|
583
1482
|
}
|
|
584
1483
|
|
|
585
1484
|
function read(e) {
|
|
@@ -587,83 +1486,182 @@ function read(e) {
|
|
|
587
1486
|
// Checked before isPending so that isPending(() => latest(x)) checks
|
|
588
1487
|
// the _pendingSignal of _latestValueComputed (async in flight) rather
|
|
589
1488
|
// than the original node (which stays "pending" while held in a transition).
|
|
590
|
-
if (latestReadActive) return GlobalQueue.
|
|
591
|
-
let
|
|
592
|
-
if (
|
|
593
|
-
const
|
|
594
|
-
const i = e.
|
|
595
|
-
const
|
|
1489
|
+
if (latestReadActive) return GlobalQueue.et(e);
|
|
1490
|
+
let n = context;
|
|
1491
|
+
if (n?.Wn) n = n.qn;
|
|
1492
|
+
const t = e;
|
|
1493
|
+
const i = e.De;
|
|
1494
|
+
const u = i || e;
|
|
596
1495
|
// Handle isPending() mode: collect pending state while preserving normal read semantics.
|
|
597
1496
|
// Probe mode is suspended while preparing the node so nested reads during a
|
|
598
1497
|
// recompute don't collect into the probe.
|
|
599
1498
|
if (pendingCheckActive) {
|
|
600
|
-
GlobalQueue.
|
|
601
|
-
} else if (typeof
|
|
1499
|
+
GlobalQueue.nt(e, n, u, i);
|
|
1500
|
+
} else if (typeof t.he === "function") {
|
|
602
1501
|
prepareComputed(e, false);
|
|
603
1502
|
}
|
|
604
|
-
if (!
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1503
|
+
if (!t.he && u === e && e.o?.Fe === undefined && e.o?.sn === undefined && activeTransition === null && currentOptimisticLane === null && !snapshotCaptureActive && (!unflushedStaged || e._e === NOT_PENDING) && // A28, see readNodeFast
|
|
1504
|
+
true) {
|
|
1505
|
+
if (n && tracking) link(e, n);
|
|
1506
|
+
// Committed visibility for children-forbidden readers and for stale
|
|
1507
|
+
// readers of a foreign transaction's staged write — see readNodeFast.
|
|
1508
|
+
return !n || e._e === NOT_PENDING || n.C & CONFIG_CHILDREN_FORBIDDEN || stale && heldFromStale(e, n) ? e.ce : (enterStagedRead(e),
|
|
1509
|
+
e._e);
|
|
1510
|
+
}
|
|
1511
|
+
if (n && tracking) {
|
|
1512
|
+
link(e, n, pendingCheckActive);
|
|
1513
|
+
if (u.he) {
|
|
1514
|
+
const t = queueFor(e);
|
|
1515
|
+
if (u.rn >= t.ln) {
|
|
1516
|
+
markNode(n);
|
|
1517
|
+
markHeap(t);
|
|
1518
|
+
updateIfNecessary(u);
|
|
617
1519
|
}
|
|
618
|
-
|
|
1520
|
+
// Fresh-pull readers (awaitable refresh's waiter) recompute a dirty
|
|
1521
|
+
// source inline even when the height gate defers to the flush: the
|
|
1522
|
+
// waiter must park on the re-ask's window (or serve its sync answer),
|
|
1523
|
+
// never read the PRE-re-ask value as settled. Self-guarded: a clean
|
|
1524
|
+
// node no-ops and updateIfNecessary refuses disposed nodes (#2983) —
|
|
1525
|
+
// a dead target serves its last value, which is already quiescent.
|
|
1526
|
+
else if (n.C & CONFIG_FRESH_READ) updateIfNecessary(u);
|
|
1527
|
+
const i = u.rn;
|
|
619
1528
|
// parent check is shallow, might need to be recursive
|
|
620
|
-
if (i >=
|
|
621
|
-
|
|
1529
|
+
if (i >= n.rn && e._parent !== n) {
|
|
1530
|
+
n.rn = i + 1;
|
|
622
1531
|
}
|
|
623
1532
|
}
|
|
624
1533
|
}
|
|
625
|
-
if (
|
|
626
|
-
|
|
1534
|
+
if (u.h & STATUS_PENDING) {
|
|
1535
|
+
// A reader landing on a pending node throws — the reveal that discovered
|
|
1536
|
+
// the flight holds on it (A15: observed async settles as one unit) — with
|
|
1537
|
+
// one carve-out: a stale (render) reader of a node pending in some OTHER
|
|
1538
|
+
// transaction keeps showing the node's committed value, no entanglement
|
|
1539
|
+
// (parallel transactions; the reader is recorded for that transaction's
|
|
1540
|
+
// commit replay, `heldFromStale`). The carve-out is sound only while the
|
|
1541
|
+
// committed value is coherent with the visible frame, i.e. while the
|
|
1542
|
+
// flight's inputs are themselves unpublished: the stamp alone does not
|
|
1543
|
+
// say so (it is pending-node bookkeeping), so it is refused when the
|
|
1544
|
+
// inputs are on screen — committed by a batch that left the flight in
|
|
1545
|
+
// the air (CONFIG_INPUTS_PUBLISHED, #3305) or revealed through a lane
|
|
1546
|
+
// (optimistic / latest, #3334) — and the reader holds instead. An
|
|
1547
|
+
// UNINITIALIZED node has no committed value to show and holds too
|
|
1548
|
+
// (firewall-backed store reads always did; plain memos since the #3043
|
|
1549
|
+
// port): falling through served `undefined` as if settled and stranded
|
|
1550
|
+
// the reader outside both transactions, so it never re-ran.
|
|
1551
|
+
if (n && !(stale && !(u.h & STATUS_UNINITIALIZED) && !(u.C & CONFIG_INPUTS_PUBLISHED) && !(u.C & CONFIG_HAS_LANE && GlobalQueue.tt(u)) && heldFromStale(u, n))) {
|
|
627
1552
|
// Per-lane suspension lives with the engine (a non-null lane implies it
|
|
628
1553
|
// is installed): under a lane, only same-lane pending async without an
|
|
629
|
-
// active override throws
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1554
|
+
// active override throws — plus uninitialized sources regardless of
|
|
1555
|
+
// lane (#3276); that check rides laneSuspends so floor bundles don't
|
|
1556
|
+
// pay for it.
|
|
1557
|
+
if (currentOptimisticLane === null || GlobalQueue.it(u)) {
|
|
1558
|
+
// An untracked read of a pending node still re-runs its reader when
|
|
1559
|
+
// the node settles — unless nobody is reading (`spectating`, #3528)
|
|
1560
|
+
// or the reader is the node itself.
|
|
1561
|
+
if (!tracking && !spectating && e !== n) link(e, n);
|
|
1562
|
+
throw u.o?._;
|
|
633
1563
|
}
|
|
634
|
-
} else if (
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1564
|
+
} else if (!n && u.h & STATUS_UNINITIALIZED &&
|
|
1565
|
+
// An armed DERIVED override shields the uninitialized state from a
|
|
1566
|
+
// read with no reader identity (#3648 follow-up, A18 (d)): a memo
|
|
1567
|
+
// whose first landing rode its lane (asyncWrite's lane branch →
|
|
1568
|
+
// `laneOverride`) has no `_value` — the flag stays set until the lane's
|
|
1569
|
+
// commit promotes the override — yet the override IS the displayed
|
|
1570
|
+
// value of a pending node (A18 (c), the screen keeps it until the
|
|
1571
|
+
// commit), exactly as an initialized node re-deriving under its override
|
|
1572
|
+
// displays it. "Uninitialized must suspend" (#3276, A19 exception 1) was
|
|
1573
|
+
// written for a node with NOTHING to show; this node has the override.
|
|
1574
|
+
// The arm fires in the body-end window: the source override superseded
|
|
1575
|
+
// (A18 body-end, #3427), the memo re-deriving on the plain channel and
|
|
1576
|
+
// held, its own slot still armed. Fall through to serve()'s override
|
|
1577
|
+
// arm. Readers WITH identity are untouched: a tracked lane reader keeps
|
|
1578
|
+
// the override (A17), an off-lane one suspends (#3651, `overrideRead`).
|
|
1579
|
+
!(hasActiveOverride(e) && e.C & CONFIG_DERIVED_OVERRIDE)) {
|
|
1580
|
+
throw u.o?._;
|
|
639
1581
|
}
|
|
640
1582
|
}
|
|
641
1583
|
// `owner` is the computed itself, or the firewall behind a store node —
|
|
642
1584
|
// firewall-backed reads follow the same rules (memo parity, #2897 ruling):
|
|
643
1585
|
// an errored derive throws for every late reader instead of silently
|
|
644
1586
|
// serving node values (the seed, or last-good data after a failed refetch).
|
|
645
|
-
if (
|
|
1587
|
+
if (u.he && u.h & STATUS_ERROR) {
|
|
646
1588
|
// Only a genuine reactive re-read may retry an errored async source:
|
|
647
1589
|
// - tracking: owned/tracked scope only (never events / `untrack` / effect side-effect phase)
|
|
648
1590
|
// - !pendingCheckActive: an `isPending` probe observes the error, never refetches
|
|
649
1591
|
// - owner._time < clock: only on a later cycle than the one the error was found
|
|
650
|
-
if (tracking && !pendingCheckActive &&
|
|
651
|
-
recompute(
|
|
1592
|
+
if (tracking && !pendingCheckActive && u.be < clock) {
|
|
1593
|
+
recompute(u);
|
|
652
1594
|
return read(e);
|
|
653
|
-
} else throw
|
|
654
|
-
}
|
|
655
|
-
if (snapshotCaptureActive &&
|
|
656
|
-
const
|
|
657
|
-
if (
|
|
658
|
-
const i =
|
|
659
|
-
const
|
|
660
|
-
if (
|
|
1595
|
+
} else throw u.o?._;
|
|
1596
|
+
}
|
|
1597
|
+
if (snapshotCaptureActive && n && n.C & CONFIG_IN_SNAPSHOT_SCOPE) {
|
|
1598
|
+
const t = e.o?.sn;
|
|
1599
|
+
if (t !== undefined) {
|
|
1600
|
+
const i = t === NO_SNAPSHOT ? undefined : t;
|
|
1601
|
+
const u = e._e !== NOT_PENDING ? e._e : e.ce;
|
|
1602
|
+
if (u !== i) n.oe |= REACTIVE_SNAPSHOT_STALE;
|
|
661
1603
|
return i;
|
|
662
1604
|
}
|
|
663
1605
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1606
|
+
const l = serve(e, n, u, e.ce);
|
|
1607
|
+
if (!n && u === e && typeof t.he === "function" && e.C & CONFIG_AUTO_DISPOSE && !(u.h & STATUS_PENDING) && !e.u &&
|
|
1608
|
+
// An untracked read served a visible override returned before this
|
|
1609
|
+
// sweep registration when the arm was inline; keep that.
|
|
1610
|
+
!visibleOverride(e)) {
|
|
1611
|
+
// Deferred, not inline (#3078): an inline unobserved() here made untracked
|
|
1612
|
+
// reads destructive — dispose on this read, full revival recompute on the
|
|
1613
|
+
// next — so consecutive reads could answer differently with no write in
|
|
1614
|
+
// between (the revival samples the ambient transition/lane context).
|
|
1615
|
+
// The sweep at flush finalization re-validates and reclaims; schedule()
|
|
1616
|
+
// guarantees that flush happens even if nothing else is queued.
|
|
1617
|
+
dormantNodes.add(e);
|
|
1618
|
+
schedule();
|
|
1619
|
+
}
|
|
1620
|
+
return l;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* Rule 1, the one slow implementation (DESIGN-CONSOLIDATION move 3b, step
|
|
1625
|
+
* 6c): the value a reader `c` (null = untracked, no pass) is served from
|
|
1626
|
+
* `el`, whose committed value is `committed` — the node's own `_value` for
|
|
1627
|
+
* a signal or memo, the BACKING for a store property node (single-home rule,
|
|
1628
|
+
* O6: committed truth lives in the backing and a node's `_value` is never
|
|
1629
|
+
* served for one). Called by read()'s slow tail and by the store's untracked
|
|
1630
|
+
* node path (nodeValue); the fast paths (readNodeFast, read's fast block)
|
|
1631
|
+
* keep their trivial ternary by design (perf, see the doc). Arms, in order:
|
|
1632
|
+
* - the override (A17), routed through the engine for a tracked reader under
|
|
1633
|
+
* a lane or a supersession (A18), an authoritative reader marked instead;
|
|
1634
|
+
* - the lane entanglement gate (committed, recorded for replay);
|
|
1635
|
+
* - a node born held has nothing for an untracked reader (A19 exception 1);
|
|
1636
|
+
* - an unflushed write serves committed and re-runs the reader in the
|
|
1637
|
+
* carrying flush (A28);
|
|
1638
|
+
* - readerSeesCommitted, else the staged value and the transaction (A29).
|
|
1639
|
+
*/ function serve(e, n, t, i) {
|
|
1640
|
+
if (hasActiveOverride(e)) {
|
|
1641
|
+
// A17: the override IS the value for every reader — except an authoritative
|
|
1642
|
+
// reader (until()'s predicate carries CONFIG_AUTHORITATIVE_READ): it must
|
|
1643
|
+
// observe independently-arriving truth, and serving it the caller's own
|
|
1644
|
+
// tentative write would trivially satisfy the predicate. The bit is checked
|
|
1645
|
+
// on the reading computation itself, so a shared computed the predicate
|
|
1646
|
+
// pulls recomputes as ITSELF (context = the memo, no bit) under the normal
|
|
1647
|
+
// view. Fall through to normal value selection (staged `_pendingValue` is
|
|
1648
|
+
// authoritative — optimism never lives there); the sticky mark makes the
|
|
1649
|
+
// A17-silent "landing equals override" paths notify this node's subs so
|
|
1650
|
+
// the reader re-runs when truth arrives.
|
|
1651
|
+
// A28 (5): an optimistic write written this tick, outside a flush, is not
|
|
1652
|
+
// yet the active override — fall through to the normal selection (the
|
|
1653
|
+
// authoritative mark below still applies: until() must wake on landing).
|
|
1654
|
+
if (!(n && n.C & CONFIG_AUTHORITATIVE_READ) && !unflushedOverride(e)) {
|
|
1655
|
+
// A tracked read of an override is the engine's selection (a lane or a
|
|
1656
|
+
// supersession implies the engine): a render effect OFF the override's
|
|
1657
|
+
// held lane sees the committed value (#3460, lanes mirror transitions);
|
|
1658
|
+
// a node whose own source answered with a DIFFERENT value hands a
|
|
1659
|
+
// tracked reader the staged truth (A18 supersession, #3331). Untracked
|
|
1660
|
+
// reads display the override.
|
|
1661
|
+
if (n && e.C & (CONFIG_HAS_LANE | CONFIG_OVERRIDE_SUPERSEDED)) return GlobalQueue.ut(e, n);
|
|
1662
|
+
return unwrapOverride(e.o?.Fe);
|
|
1663
|
+
}
|
|
1664
|
+
e.C |= CONFIG_AUTHORITATIVE_OBSERVED;
|
|
667
1665
|
}
|
|
668
1666
|
// Entanglement gate: a reader recomputing under an optimistic lane that reads
|
|
669
1667
|
// a pending mid-transition write sees the committed value. Projection-store
|
|
@@ -672,8 +1670,8 @@ function read(e) {
|
|
|
672
1670
|
// _pendingValue for correct fetching. The sub is recorded for replay at commit
|
|
673
1671
|
// so it re-runs with the new committed view. (Gate details live with the
|
|
674
1672
|
// engine — a non-null lane implies it is installed.)
|
|
675
|
-
if (currentOptimisticLane !== null && activeTransition !== null &&
|
|
676
|
-
return
|
|
1673
|
+
if (currentOptimisticLane !== null && activeTransition !== null && n !== null && GlobalQueue.lt(e, t, n)) {
|
|
1674
|
+
return i;
|
|
677
1675
|
}
|
|
678
1676
|
// In optimistic lane context, return _value for optimistic/lane-assigned signals
|
|
679
1677
|
// and for regular signals in stale mode (render effects). Non-stale readers (user
|
|
@@ -681,39 +1679,142 @@ function read(e) {
|
|
|
681
1679
|
// (The lane-context clause lives with the engine.) Children-forbidden readers
|
|
682
1680
|
// (createTrackedEffect / onSettled callbacks) get committed visibility — see
|
|
683
1681
|
// readNodeFast (#3006).
|
|
684
|
-
|
|
1682
|
+
// A node born held (recompute) has a staged value and no committed one: a
|
|
1683
|
+
// stale reader cannot fall back to the committed frame, so it takes the
|
|
1684
|
+
// staged value and enters like a tracked reader (A29); an untracked reader
|
|
1685
|
+
// has nothing to serve and holds (A19 exception 1) — a bookkeeping read
|
|
1686
|
+
// (`spectate`) likewise: it derives nothing, so it cannot enter, and a
|
|
1687
|
+
// held-only value is simply not ready to it.
|
|
1688
|
+
const u = e._e !== NOT_PENDING && (e.h & STATUS_UNINITIALIZED) !== 0;
|
|
1689
|
+
if (u && (!n || spectating)) throw new NotReadyError(null);
|
|
1690
|
+
const l = n && unflushedStaged ? unflushedValue(e, i) : NOT_PENDING;
|
|
1691
|
+
if (l !== NOT_PENDING) {
|
|
1692
|
+
markLateLinker(n);
|
|
1693
|
+
if (pendingCheckActive) GlobalQueue.rt(e, l);
|
|
1694
|
+
return l;
|
|
1695
|
+
}
|
|
1696
|
+
const r = readerSeesCommitted(e, n, t, u) ? i : (enterStagedRead(e), e._e);
|
|
685
1697
|
// Record that this isPending() probe observed the fresh pending value, so
|
|
686
1698
|
// the probe doesn't pair "pending" with the new value (#2831).
|
|
687
|
-
if (pendingCheckActive) GlobalQueue.
|
|
688
|
-
|
|
689
|
-
|
|
1699
|
+
if (pendingCheckActive) GlobalQueue.rt(e, r);
|
|
1700
|
+
return r;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/** A28 — a rewrite of a HELD node outside a flush keeps the staged value the
|
|
1704
|
+
* last flush left, for latest()/verdicts, until the next flush carries it
|
|
1705
|
+
* (stash, cleared at that flush). Cold: only stamped nodes reach here. */ function stashHeldRewrite(e) {
|
|
1706
|
+
if (globalQueue.Jn) return;
|
|
1707
|
+
const n = ext(e);
|
|
1708
|
+
if (n.bn === NOT_PENDING) {
|
|
1709
|
+
n.bn = e._e;
|
|
1710
|
+
unflushedRewrites.push(e);
|
|
1711
|
+
unflushedStaged = true;
|
|
690
1712
|
}
|
|
691
|
-
return u;
|
|
692
1713
|
}
|
|
693
1714
|
|
|
694
|
-
|
|
695
|
-
|
|
1715
|
+
/** A28 (4) — written inside a recompute that runs OUTSIDE a flush (a
|
|
1716
|
+
* creation-time compute): promoted at that pass's end, visible to the rest of
|
|
1717
|
+
* the block. Cold: only contextual writes reach here. */ function notePromotedWrite(e) {
|
|
1718
|
+
if (globalQueue.Jn || e.C & CONFIG_PROMOTED) return;
|
|
1719
|
+
e.C |= CONFIG_PROMOTED;
|
|
1720
|
+
promotedWrites.push(e);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** Cold half of setSignal's snapshot arm (see there): first write during
|
|
1724
|
+
* capture to a plain signal without a live snapshot records the pre-write
|
|
1725
|
+
* value. Only plain user signals qualify. Computeds reach setSignal from an
|
|
1726
|
+
* async landing (asyncWrite), and a value arriving from async during the pass
|
|
1727
|
+
* REVEALS — the creation-time arm skips pending computeds for the same
|
|
1728
|
+
* reason. Firewall leaves belong to a projection whose compute is the tree's
|
|
1729
|
+
* own work, captured (or deliberately not) at creation. */ function captureWriteSnapshot(e, n) {
|
|
1730
|
+
if (e.C & CONFIG_NO_SNAPSHOT || e.he !== undefined || e.De || e.o?.sn !== undefined) return;
|
|
1731
|
+
ext(e).sn = n === undefined ? NO_SNAPSHOT : n;
|
|
1732
|
+
e.C |= CONFIG_HAS_SNAPSHOT;
|
|
1733
|
+
snapshotSources.add(e);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function setSignal(e, n) {
|
|
1737
|
+
// A write to a held node is a second proposal on a contested node (A34, #3494):
|
|
1738
|
+
// the writer's tick reveals with the hold, the same value or another. Inside
|
|
1739
|
+
// a flush the round enters now; from mainline the entry waits for the next
|
|
1740
|
+
// flush's start (batchJoins) — entering here left activeTransition set for
|
|
1741
|
+
// the rest of the caller's block, so a memo created after the write became
|
|
1742
|
+
// the transaction's instead of mainline's (A28, A29). Before the equality
|
|
1743
|
+
// gate below: repeating the held value proposes it too — and that repeat
|
|
1744
|
+
// leaves through the gate, so the join schedules its own flush here; left
|
|
1745
|
+
// for the next flush to find, it adopted an unrelated tick (#3519 review).
|
|
1746
|
+
// A held DERIVATION is not a proposal (A34 amendment, #3612): the user
|
|
1747
|
+
// setters of derived nodes (setMemo, the derived store's setter) classify
|
|
1748
|
+
// the write before it reaches here — see heldDerivation — and re-derive
|
|
1749
|
+
// under the hold instead of masking; the join stands either way.
|
|
1750
|
+
if (e.we && activeTransition !== e.we) {
|
|
1751
|
+
if (globalQueue.Jn) globalQueue.initTransition(e.we); else {
|
|
1752
|
+
batchJoins.push(e.we);
|
|
1753
|
+
// dupes: a bare return in initTransition
|
|
1754
|
+
schedule();
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
696
1757
|
// The optimistic write path lives with the engine: only optimisticSignal /
|
|
697
1758
|
// optimisticComputed callers and optimistic store nodes carry an
|
|
698
|
-
// _overrideValue slot
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1759
|
+
// _overrideValue slot (flagged by CONFIG_OPTIMISTIC — a masked read of the
|
|
1760
|
+
// always-present config instead of a missing-property probe), and every
|
|
1761
|
+
// module that installs one installs the engine first.
|
|
1762
|
+
if (e.C & CONFIG_OPTIMISTIC) {
|
|
1763
|
+
if (!projectionWriteActive) return GlobalQueue.ot(e, n);
|
|
1764
|
+
// An authoritative store landing on an override-covered node: the store
|
|
1765
|
+
// twin of asyncWrite's override branch, decided by the engine (#3331).
|
|
1766
|
+
const t = e.o?.Fe;
|
|
1767
|
+
if (t !== undefined && t !== NOT_PENDING) return GlobalQueue.st(e, n);
|
|
1768
|
+
}
|
|
1769
|
+
const t = e._e === NOT_PENDING ? e.ce : e._e;
|
|
1770
|
+
if (typeof n === "function") n = n(t);
|
|
703
1771
|
// Uninitialized check first: the first commit has no previous value, so the
|
|
704
1772
|
// user comparator must not run against `undefined` (matches recompute).
|
|
705
|
-
const i = !!(e.
|
|
706
|
-
if (!i) return
|
|
707
|
-
|
|
708
|
-
|
|
1773
|
+
const i = !!(e.h & STATUS_UNINITIALIZED) || !e.xe || !e.xe(t, n);
|
|
1774
|
+
if (!i) return n;
|
|
1775
|
+
// A write during hydration's snapshot capture to a source that has no
|
|
1776
|
+
// snapshot — created BEFORE capture began (module-level state: an identity
|
|
1777
|
+
// minted from onSettled in the pass, a preference read from storage) —
|
|
1778
|
+
// captures the pre-write value now, so the write is held like any other:
|
|
1779
|
+
// in-scope readers keep serving what the server rendered with and replay
|
|
1780
|
+
// at release. Left uncaptured, the write cascades live through a claim
|
|
1781
|
+
// pass whose DOM writes are skipped, and a component rendered later in the
|
|
1782
|
+
// pass reads a value the server never had. Store leaves written here are
|
|
1783
|
+
// plain signals and qualify the same way.
|
|
1784
|
+
if (snapshotCaptureActive) captureWriteSnapshot(e, t);
|
|
1785
|
+
const u = e._e !== NOT_PENDING;
|
|
1786
|
+
if (!u) queuePendingNode(e);
|
|
1787
|
+
// A28 arms, gated on the loads the write already pays for (a plain ambient
|
|
1788
|
+
// rewrite outside a flush — the write-loop shape — costs `_transition` and
|
|
1789
|
+
// `context` here and nothing else; the arms themselves are cold helpers so
|
|
1790
|
+
// setSignal stays within every setter's inlining budget, ~300 B bytecode).
|
|
1791
|
+
else if (e.we !== null) stashHeldRewrite(e);
|
|
1792
|
+
e._e = n;
|
|
1793
|
+
if (context !== null) notePromotedWrite(e);
|
|
709
1794
|
// syncCompanions only pokes _pendingSignal/_latestValueComputed — with
|
|
710
1795
|
// neither companion present the call is a guaranteed no-op (companions are
|
|
711
|
-
// only ever created, never removed, and creating one installs the hook
|
|
712
|
-
|
|
713
|
-
|
|
1796
|
+
// only ever created, never removed, and creating one installs the hook and
|
|
1797
|
+
// sets CONFIG_HAS_COMPANIONS — one masked read replaces two optional-field
|
|
1798
|
+
// probes on every write).
|
|
1799
|
+
if (e.C & CONFIG_HAS_COMPANIONS && GlobalQueue.Ve !== null) {
|
|
1800
|
+
GlobalQueue.Ve(e, n);
|
|
1801
|
+
// A28 (2): the verdict computed here is the PRE-flush one (an unflushed
|
|
1802
|
+
// write is not pending); the flush that carries the write re-syncs so
|
|
1803
|
+
// the companions mirror the flushed world. Only companion nodes pay.
|
|
1804
|
+
if (!globalQueue.Jn) unflushedCompanions.push(e);
|
|
1805
|
+
}
|
|
1806
|
+
// _time is a computed-only slot (§12e): writing it on a signal would fork
|
|
1807
|
+
// the lean shape. Every read site is computed-typed.
|
|
1808
|
+
if (e.he !== undefined) e.be = clock;
|
|
1809
|
+
// Staged-rewrite fast path (§12d): a re-write to a node whose subscribers
|
|
1810
|
+
// were already walked — and where nothing has recomputed or linked since
|
|
1811
|
+
// (epoch) — re-stages the value and stops. The walk is idempotent (subs
|
|
1812
|
+
// marked, heap entries flag-guarded, effects queued once); lane and reask
|
|
1813
|
+
// contexts change what a walk MEANS, so they always walk.
|
|
1814
|
+
if (u && e.vn === notifyEpoch && currentOptimisticLane === null && !reaskArmed) return n;
|
|
714
1815
|
insertSubs(e);
|
|
715
1816
|
schedule();
|
|
716
|
-
return
|
|
1817
|
+
return n;
|
|
717
1818
|
}
|
|
718
1819
|
|
|
719
1820
|
/**
|
|
@@ -724,22 +1825,50 @@ function setSignal(e, t) {
|
|
|
724
1825
|
* cleanup point.
|
|
725
1826
|
*/ function suppressComputedRecompute(e) {
|
|
726
1827
|
deleteFromHeap(e, queueFor(e));
|
|
727
|
-
if (!(e.
|
|
1828
|
+
if (!(e.oe & REACTIVE_MANUAL_WRITE) && e._e === NOT_PENDING) {
|
|
728
1829
|
queuePendingNode(e);
|
|
729
1830
|
schedule();
|
|
730
1831
|
}
|
|
731
|
-
e.
|
|
1832
|
+
e.oe = e.oe & -4 | REACTIVE_MANUAL_WRITE;
|
|
1833
|
+
e.ct = clock;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/** A34 amendment (#3612) — is `el`'s staging a DERIVATION another transaction
|
|
1837
|
+
* holds: stamped by a transaction that is not the writer's, and not a manual
|
|
1838
|
+
* proposal (the mask, on the node or — a store leaf — its firewall)? #2692's
|
|
1839
|
+
* "manual write wins" is a rule for one synchronous frame; across a hold the
|
|
1840
|
+
* held pass result is nobody's proposal, and a user setter reaching it
|
|
1841
|
+
* composes on the committed frame it was written against and becomes `prev`
|
|
1842
|
+
* for the transaction's re-derivation (rederiveHeld) instead of replacing it.
|
|
1843
|
+
* Writes made under the holding transaction masked the node and keep
|
|
1844
|
+
* A34(1)'s last-write-wins. Only the user setters ask; an async landing or a
|
|
1845
|
+
* companion writing a stamped node is the transaction's own work. */ function heldDerivation(e) {
|
|
1846
|
+
return e.we !== null && activeTransition !== e.we && !((e.De || e).oe & REACTIVE_MANUAL_WRITE);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/** The held-derivation write's second half: the node re-derives under its
|
|
1850
|
+
* hold with the written staging as the pass's `prev`. Nothing is masked. The
|
|
1851
|
+
* write took the A34 join (setSignal), which scheduled the flush that drains
|
|
1852
|
+
* this; recompute re-enters the stamp. */ function rederiveHeld(e) {
|
|
1853
|
+
e.oe |= REACTIVE_DIRTY;
|
|
1854
|
+
// over CHECK: the heap visit recomputes, not re-checks
|
|
1855
|
+
enqueueSub(e);
|
|
732
1856
|
}
|
|
733
1857
|
|
|
734
1858
|
/**
|
|
735
1859
|
* User-facing setter for the memo form of `createSignal(fn)`. Behaves like
|
|
736
1860
|
* `setSignal`, but also cancels any pending recompute of the memo so the
|
|
737
1861
|
* manual value wins over a value that would otherwise be produced by an
|
|
738
|
-
* upstream change in the same tick.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1862
|
+
* upstream change in the same tick. Across a hold the write is not a
|
|
1863
|
+
* proposal: a memo another transaction holds as a pass result composes on the
|
|
1864
|
+
* committed value and re-derives under the hold with the write as `prev`
|
|
1865
|
+
* (A34 amendment, #3612; heldDerivation).
|
|
1866
|
+
*/ function setMemo(e, n) {
|
|
1867
|
+
const t = heldDerivation(e);
|
|
1868
|
+
if (t && typeof n === "function") n = n(e.ce);
|
|
1869
|
+
const i = setSignal(e, n);
|
|
1870
|
+
t ? rederiveHeld(e) : suppressComputedRecompute(e);
|
|
1871
|
+
return i;
|
|
743
1872
|
}
|
|
744
1873
|
|
|
745
1874
|
/**
|
|
@@ -760,54 +1889,55 @@ function setSignal(e, t) {
|
|
|
760
1889
|
* setTimeout(() => runWithOwner(owner, fn), ms);
|
|
761
1890
|
* }
|
|
762
1891
|
* ```
|
|
763
|
-
*/ function runWithOwner(e,
|
|
764
|
-
const
|
|
1892
|
+
*/ function runWithOwner(e, n) {
|
|
1893
|
+
const t = context;
|
|
765
1894
|
const i = tracking;
|
|
766
1895
|
context = e;
|
|
767
1896
|
tracking = false;
|
|
768
1897
|
try {
|
|
769
|
-
return
|
|
1898
|
+
return n();
|
|
770
1899
|
} finally {
|
|
771
|
-
context =
|
|
1900
|
+
context = t;
|
|
772
1901
|
tracking = i;
|
|
773
1902
|
}
|
|
774
1903
|
}
|
|
775
1904
|
|
|
776
|
-
function staleValues(e,
|
|
777
|
-
const
|
|
778
|
-
stale =
|
|
1905
|
+
function staleValues(e, n = true) {
|
|
1906
|
+
const t = stale;
|
|
1907
|
+
stale = n;
|
|
779
1908
|
try {
|
|
780
1909
|
return e();
|
|
781
1910
|
} finally {
|
|
782
|
-
stale =
|
|
1911
|
+
stale = t;
|
|
783
1912
|
}
|
|
784
1913
|
}
|
|
785
1914
|
|
|
786
1915
|
/**
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1916
|
+
* Core marking half of `refresh()` (the public wrapper lives in signals.ts —
|
|
1917
|
+
* it validates the target, marks through here, then builds the quiescence
|
|
1918
|
+
* promise on the resolve()/until() effect machinery). Flags the node's next
|
|
1919
|
+
* recompute as a quiet re-ask and schedules it; no-ops for non-derived or
|
|
1920
|
+
* disposed targets and for same-tick manual writes.
|
|
1921
|
+
*/ function markRefresh(e) {
|
|
1922
|
+
if (typeof e.he === "function" && !(e.oe & REACTIVE_DISPOSED)) {
|
|
1923
|
+
if (e.oe & REACTIVE_MANUAL_WRITE) {
|
|
1924
|
+
// A manual write in the CURRENT tick wins over the refresh (#2692).
|
|
1925
|
+
// A mask stamped in an earlier tick only survives because a
|
|
1926
|
+
// transaction (action) is holding the pending drain open; there the
|
|
1927
|
+
// refresh is a later, explicit re-ask and lifts the mask — otherwise
|
|
1928
|
+
// any setStore early in an action silently swallows every refresh()
|
|
1929
|
+
// for the rest of the transaction (#3026).
|
|
1930
|
+
if (e.ct === clock) return;
|
|
1931
|
+
e.oe &= ~REACTIVE_MANUAL_WRITE;
|
|
1932
|
+
// The lift falls through to the re-ask classification below. The held
|
|
1933
|
+
// write's value change already rides the transaction; the refetch it
|
|
1934
|
+
// asks for is the same question with unchanged inputs. Skipping the
|
|
1935
|
+
// mark here classified that refetch as a NEW question, which pends
|
|
1936
|
+
// every leaf (3.1) — an action doing setStore + yield + refresh(store)
|
|
1937
|
+
// lit up every sibling row, and affects() could not narrow it (a mark
|
|
1938
|
+
// only turns pending on). Same-question motion stays silent (3.4);
|
|
1939
|
+
// the written slot and any declared mark carry the pending instead.
|
|
1940
|
+
}
|
|
811
1941
|
// A refresh with no value-change dirt already queued is a re-ask of the
|
|
812
1942
|
// same question: mark it so the recompute classifies any resulting
|
|
813
1943
|
// pending window as quiet (not pending). If the node is already dirty
|
|
@@ -815,14 +1945,14 @@ function staleValues(e, t = true) {
|
|
|
815
1945
|
// REACTIVE_IN_HEAP counts as dirt: insertSubs schedules subscribers by
|
|
816
1946
|
// heap insertion alone (no DIRTY/CHECK flag), so a same-batch value
|
|
817
1947
|
// change followed by refresh() must not be laundered into a quiet re-ask.
|
|
818
|
-
|
|
819
|
-
|
|
1948
|
+
if (!(e.oe & (REACTIVE_DIRTY | REACTIVE_CHECK | REACTIVE_IN_HEAP))) {
|
|
1949
|
+
e.oe |= REACTIVE_REASK;
|
|
820
1950
|
armReaskClear();
|
|
821
1951
|
}
|
|
822
|
-
|
|
823
|
-
insertIntoHeap(
|
|
1952
|
+
e.oe = e.oe & ~REACTIVE_CHECK | REACTIVE_DIRTY;
|
|
1953
|
+
insertIntoHeap(e, queueFor(e));
|
|
824
1954
|
schedule();
|
|
825
1955
|
}
|
|
826
1956
|
}
|
|
827
1957
|
|
|
828
|
-
export { READ_SLOW, clearSnapshots, computed, context, createEffectNode, currentOptimisticLane, isEqual, latestReadActive, markSnapshotScope, optimisticComputed, optimisticSignal, pendingCheckActive, prepareComputed, read, readNodeFast, recompute,
|
|
1958
|
+
export { READ_SLOW, clearSnapshots, computed, context, createEffectNode, currentOptimisticLane, effectStatusNotify, enterStagedRead, ext, hasActiveOverride, heldDerivation, installAuthoritativeRead, isEqual, latestReadActive, markLateLinker, markRefresh, markSnapshotScope, markUnflushedStaged, notifyAuthoritativeObservers, notifyOnLane, optimisticComputed, optimisticSignal, ownsHold, pendingCheckActive, prepareComputed, read, readNodeFast, readerSeesCommitted, recompute, recordStaleReplay, rederiveHeld, releaseSnapshotScope, resyncUnflushedCompanions, runWithOwner, serve, setContextInternal, setEffectStatusNotify, setLatestReadActive, setMemo, setPendingCheckActive, setSignal, setSlotUnobserved, setSnapshotCapture, signal, slotSignal, slotUnobservedHook, snapshotCaptureActive, snapshotSources, spectate, spectating, stale, staleValues, statusNotifierOf, suppressComputedRecompute, tracking, unflushed, unflushedCompanions, unflushedOverride, unflushedStaged, unflushedValue, unlinkFirewallChild, untrack, visibleOverride };
|