@solidjs/signals 2.0.0-rc.16 → 2.0.0-rc.18
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 +1972 -1751
- package/dist/dev.attribution.js +16 -16
- package/dist/dev.js +672 -319
- package/dist/observe/boundaries.js +2 -2
- package/dist/observe/core/async.js +151 -116
- package/dist/observe/core/constants.js +3 -1
- package/dist/observe/core/context.js +2 -0
- package/dist/observe/core/core.js +51 -35
- package/dist/observe/core/effect.js +29 -30
- package/dist/observe/core/graph.js +13 -11
- package/dist/observe/core/lanes.js +25 -14
- package/dist/observe/core/owner.js +77 -51
- package/dist/observe/core/scheduler.js +202 -175
- package/dist/observe/core/verdict.js +5 -2
- package/dist/observe/deferred.js +16 -13
- package/dist/observe/from.js +248 -0
- package/dist/observe/index.js +2 -0
- package/dist/observe/map.js +2 -2
- package/dist/observe/signals.js +6 -1
- package/dist/observe/store/affects.js +13 -13
- package/dist/observe/store/optimistic.js +112 -71
- package/dist/observe/store/reconcile.js +137 -121
- package/dist/observe/store/store.js +612 -626
- package/dist/observe/store/target.js +1 -5
- package/dist/prod/boundaries.js +2 -2
- package/dist/prod/core/async.js +211 -176
- package/dist/prod/core/constants.js +3 -1
- package/dist/prod/core/context.js +2 -0
- package/dist/prod/core/core.js +63 -47
- package/dist/prod/core/effect.js +24 -25
- package/dist/prod/core/graph.js +12 -10
- package/dist/prod/core/heap.js +1 -1
- package/dist/prod/core/lanes.js +39 -28
- package/dist/prod/core/owner.js +70 -44
- package/dist/prod/core/scheduler.js +78 -51
- package/dist/prod/core/truth.js +4 -4
- package/dist/prod/core/verdict.js +11 -8
- package/dist/prod/deferred.js +17 -14
- package/dist/prod/from.js +248 -0
- package/dist/prod/index.js +2 -0
- package/dist/prod/map.js +2 -2
- package/dist/prod/signals.js +18 -13
- package/dist/prod/store/optimistic.js +112 -71
- package/dist/prod/store/reconcile.js +137 -121
- package/dist/prod/store/store.js +109 -121
- package/dist/prod/store/target.js +13 -17
- package/dist/types/core/async.d.ts +12 -2
- package/dist/types/core/constants.d.ts +5 -0
- package/dist/types/core/core.d.ts +29 -6
- package/dist/types/core/dev.d.ts +9 -1
- package/dist/types/core/error.d.ts +9 -1
- package/dist/types/core/owner.d.ts +4 -0
- package/dist/types/core/scheduler.d.ts +38 -7
- package/dist/types/from.d.ts +47 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
package/dist/dev-shared.js
CHANGED
|
@@ -61,11 +61,26 @@ class NoOwnerError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
class ContextNotFoundError extends Error {
|
|
63
63
|
constructor() {
|
|
64
|
-
super(
|
|
65
|
-
"Context must either be created with a default value or a value must be provided before accessing it."
|
|
66
|
-
);
|
|
64
|
+
super(contextNotFoundMessage(arguments[0], arguments[1]));
|
|
67
65
|
}
|
|
68
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Dev message for a context read with no value. `undefined` means "not
|
|
69
|
+
* provided" for context, from either side — no default, or a provider
|
|
70
|
+
* passing `undefined` — so the message says which happened and points at
|
|
71
|
+
* `null` as the explicit empty value.
|
|
72
|
+
*/
|
|
73
|
+
function contextNotFoundMessage(context, providedUndefined) {
|
|
74
|
+
if (!context)
|
|
75
|
+
return "Context must either be created with a default value or a value must be provided before accessing it.";
|
|
76
|
+
const description = context.id.description;
|
|
77
|
+
const name = description ? `Context "${description}"` : "Context";
|
|
78
|
+
if (providedUndefined)
|
|
79
|
+
return `${name} was provided with \`undefined\`, which counts as not provided: \`undefined\` means "no value" for context. To provide an empty value, create the context with a \`null\` default (\`createContext<T | null>(null)\`) and pass \`null\`.`;
|
|
80
|
+
if (context._undefinedDefault)
|
|
81
|
+
return `${name} has no provider above this read, and \`createContext(undefined)\` does not give it a default: \`undefined\` means "no value" for context. Mount a provider above this read, or use \`null\` as an explicit empty default: \`createContext<T | null>(null)\`.`;
|
|
82
|
+
return `${name} has no provider above this read and no default value. Mount a provider above this read, or create the context with a default; for an optional context, use \`null\` as the empty default (\`createContext<T | null>(null)\`), not \`undefined\`.`;
|
|
83
|
+
}
|
|
69
84
|
|
|
70
85
|
const REACTIVE_NONE = 0;
|
|
71
86
|
const REACTIVE_CHECK = 1 << 0;
|
|
@@ -291,6 +306,8 @@ const defaultContext = {};
|
|
|
291
306
|
*/
|
|
292
307
|
const $REFRESH = Symbol("refresh");
|
|
293
308
|
|
|
309
|
+
const NewSet = Set;
|
|
310
|
+
|
|
294
311
|
let attrHooks = null;
|
|
295
312
|
/**
|
|
296
313
|
* The installed engine, registered on `globalThis` as well (the records
|
|
@@ -688,6 +705,19 @@ function locationLine(loc) {
|
|
|
688
705
|
return `${location.origin}/${loc}`;
|
|
689
706
|
return loc;
|
|
690
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* The locating lines of a console report: `\n in <App> › <TodoRow> › effect`
|
|
710
|
+
* (the owner path) and `\n at src/App.tsx:18:3` (see `locationLine`), each
|
|
711
|
+
* when the entry carries it; `""` when it carries neither. Shared by
|
|
712
|
+
* `reportDiagnostic` and the sites that print their own console entry (the
|
|
713
|
+
* halt, which logs its cause beside the message).
|
|
714
|
+
*/
|
|
715
|
+
function locationLines(entry) {
|
|
716
|
+
let text = "";
|
|
717
|
+
if (entry.ownerPath) text += `\n in ${entry.ownerPath.join(" › ")}`;
|
|
718
|
+
if (entry.loc) text += `\n at ${locationLine(entry.loc)}`;
|
|
719
|
+
return text;
|
|
720
|
+
}
|
|
691
721
|
/**
|
|
692
722
|
* The console face of a diagnostic — ONE entry per finding: the message, the
|
|
693
723
|
* owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
|
|
@@ -704,9 +734,7 @@ function locationLine(loc) {
|
|
|
704
734
|
*/
|
|
705
735
|
function reportDiagnostic(entry) {
|
|
706
736
|
if (suppressedEvents.has(entry)) return;
|
|
707
|
-
let text = entry.message;
|
|
708
|
-
if (entry.ownerPath) text += `\n in ${entry.ownerPath.join(" › ")}`;
|
|
709
|
-
if (entry.loc) text += `\n at ${locationLine(entry.loc)}`;
|
|
737
|
+
let text = entry.message + locationLines(entry);
|
|
710
738
|
const footer = takeFooter(entry);
|
|
711
739
|
if (footer) text += `\n${footer}`;
|
|
712
740
|
const element = eventSubjects.get(entry)?._devElement;
|
|
@@ -989,9 +1017,10 @@ function checkPostAwaitRead(dep, holder, key, nodeName, throwsPending) {
|
|
|
989
1017
|
if (callbackDepth !== 0) return;
|
|
990
1018
|
if (disposalDepth !== 0) {
|
|
991
1019
|
// Teardown is synchronous, so a depth still raised on the next microtask
|
|
992
|
-
// was left behind by a
|
|
993
|
-
// try/finally, which would survive into prod
|
|
994
|
-
// later check would stay
|
|
1020
|
+
// was left behind by a throw that escaped it (owner.ts brackets it
|
|
1021
|
+
// without try/finally, which would survive into prod; a cleanup's own
|
|
1022
|
+
// throw is caught there). Clear it then, or every later check would stay
|
|
1023
|
+
// silent.
|
|
995
1024
|
if (!disposalHealQueued) {
|
|
996
1025
|
disposalHealQueued = true;
|
|
997
1026
|
queueMicrotask(healDisposalDepth);
|
|
@@ -1112,1647 +1141,1740 @@ function devCheckQuiescent(isQueuedForCommit) {
|
|
|
1112
1141
|
return;
|
|
1113
1142
|
}
|
|
1114
1143
|
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
1122
|
-
|
|
1123
|
-
let
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
let endLanesHook = null;
|
|
1130
|
-
let lanesBlockedHook = null;
|
|
1131
|
-
function installLaneHooks(
|
|
1132
|
-
read,
|
|
1133
|
-
stage,
|
|
1134
|
-
outcome,
|
|
1135
|
-
write,
|
|
1136
|
-
applyGuesses,
|
|
1137
|
-
seams,
|
|
1138
|
-
corrections,
|
|
1139
|
-
end,
|
|
1140
|
-
blocked
|
|
1141
|
-
) {
|
|
1142
|
-
laneReadHook = read;
|
|
1143
|
-
laneStageHook = stage;
|
|
1144
|
-
laneOutcomeHook = outcome;
|
|
1145
|
-
laneWriteHook = write;
|
|
1146
|
-
applyGuessesHook = applyGuesses;
|
|
1147
|
-
laneSeamsHook = seams;
|
|
1148
|
-
laneCorrectionsHook = corrections;
|
|
1149
|
-
endLanesHook = end;
|
|
1150
|
-
lanesBlockedHook = blocked;
|
|
1144
|
+
// L2 (step 2): the parked frame. A pass parks the previous frame's children
|
|
1145
|
+
// and `_disposal` list on `_x._pendingFirstChild` / `_x._pendingDisposal`
|
|
1146
|
+
// (`parkChildren`, core.ts); they keep reacting until the node's commit
|
|
1147
|
+
// disposes them (`zombie` walk below) or the owner dies. `markDisposal` flags
|
|
1148
|
+
// the parked subtree REACTIVE_ZOMBIE: the splice guard and the reawaken path
|
|
1149
|
+
// key off it, and the heap defers a zombie's recompute past the seam (no
|
|
1150
|
+
// second heap — the deferral list in scheduler.ts).
|
|
1151
|
+
function markDisposal(el) {
|
|
1152
|
+
let child = el._firstChild;
|
|
1153
|
+
while (child) {
|
|
1154
|
+
child._flags |= REACTIVE_ZOMBIE;
|
|
1155
|
+
markDisposal(child);
|
|
1156
|
+
child = child._nextSibling;
|
|
1157
|
+
}
|
|
1151
1158
|
}
|
|
1152
|
-
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
// `heldRun` — a queued run under a fallback-showing boundary waits for
|
|
1162
|
-
// the reveal (true: held; the boundary re-queues it by type). The
|
|
1163
|
-
// synchronous first render on creation builds the subtree, attached or
|
|
1164
|
-
// not; its updates and the user effects wait.
|
|
1165
|
-
let catchHook = null;
|
|
1166
|
-
let freshHook = null;
|
|
1167
|
-
let hiddenHook = null;
|
|
1168
|
-
let boundarySeamHook = null;
|
|
1169
|
-
let heldRunHook = null;
|
|
1170
|
-
function installBoundaryHooks(caught, fresh, hidden, seam, heldRun) {
|
|
1171
|
-
catchHook = caught;
|
|
1172
|
-
freshHook = fresh;
|
|
1173
|
-
hiddenHook = hidden;
|
|
1174
|
-
boundarySeamHook = seam;
|
|
1175
|
-
heldRunHook = heldRun;
|
|
1159
|
+
function dispose(node) {
|
|
1160
|
+
// Direct disposal is death, not dormancy: strip the observation lifecycle
|
|
1161
|
+
// so a later read freezes at the last committed value instead of
|
|
1162
|
+
// reawakening the node (#3024). The teardown itself (heap removal — a node
|
|
1163
|
+
// left queued would be recomputed and resurrected by the next flush (#2983)
|
|
1164
|
+
// — dep unlinking, child disposal) is exactly unobserved()'s body; only
|
|
1165
|
+
// this flag distinguishes death from dormancy.
|
|
1166
|
+
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
1167
|
+
unobserved(node);
|
|
1176
1168
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
let
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1169
|
+
// The first error a cleanup threw, boxed so a thrown `undefined` still
|
|
1170
|
+
// counts. A throwing cleanup stops neither its siblings nor the rest of the
|
|
1171
|
+
// tree: every cleanup runs. The error is rethrown once the teardown is over:
|
|
1172
|
+
// by a root's dispose call outside a flush (`disposeRootSelf`), otherwise when
|
|
1173
|
+
// the flush ends (`flush`; `runCleanup` schedules one), so a flush is never
|
|
1174
|
+
// left half-applied. A halt drops it: the error that halted is the one
|
|
1175
|
+
// reported, and a stale one must not surface from a later, unrelated dispose.
|
|
1176
|
+
let cleanupError;
|
|
1177
|
+
/** Runs a cleanup or `unobserved` hook; a throw is collected, not raised. @internal */
|
|
1178
|
+
function runCleanup(fn) {
|
|
1179
|
+
try {
|
|
1180
|
+
fn();
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
cleanupError ||= [e];
|
|
1183
|
+
schedule();
|
|
1184
|
+
}
|
|
1188
1185
|
}
|
|
1189
|
-
/**
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
releaseMarksHook = release;
|
|
1195
|
-
releaseAmbientMarksHook = releaseAmbient;
|
|
1186
|
+
/** Rethrows the error `runCleanup` collected, once; `drop` discards it. @internal */
|
|
1187
|
+
function rethrowCleanupError(drop) {
|
|
1188
|
+
const error = cleanupError;
|
|
1189
|
+
cleanupError = undefined;
|
|
1190
|
+
if (error && !drop) throw error[0];
|
|
1196
1191
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1192
|
+
function disposeChildren(node, self = false, zombie) {
|
|
1193
|
+
const flags = node._flags;
|
|
1194
|
+
if (flags & REACTIVE_DISPOSED) return;
|
|
1195
|
+
// A parked frame dies with its owner (#3024): the commit that would retire
|
|
1196
|
+
// it never comes once the node is DISPOSED, so it drains here or its
|
|
1197
|
+
// cleanups never run and it stays subscribed, rerunning in a torn-down
|
|
1198
|
+
// tree (#3561). Death only (`self`): a re-pass leaves the frame reacting
|
|
1199
|
+
// until the commit.
|
|
1200
|
+
if (
|
|
1201
|
+
self &&
|
|
1202
|
+
!zombie &&
|
|
1203
|
+
node._x !== null &&
|
|
1204
|
+
(node._x._pendingFirstChild !== null || node._x._pendingDisposal !== null)
|
|
1205
|
+
)
|
|
1206
|
+
disposeChildren(node, false, true);
|
|
1207
|
+
if (self) {
|
|
1208
|
+
node._flags = flags | REACTIVE_DISPOSED;
|
|
1209
|
+
if (node._parent === null && node._root) unregisterRoot(node);
|
|
1210
|
+
}
|
|
1211
|
+
if (self && true) clearSignals(node);
|
|
1212
|
+
if (self && node._fn) {
|
|
1213
|
+
if (node._x !== null) node._x._inFlight = null;
|
|
1214
|
+
// A death that can unblock the future (L2, `futureBlocked`): a held
|
|
1215
|
+
// flight's — a transaction's or a lane's (its `_x._transaction`) — or a
|
|
1216
|
+
// stale reader's (REACTIVE_FRAME_READ — a frame that derived from one).
|
|
1217
|
+
// Nothing else re-examines a dead node, so make sure a seam comes.
|
|
1218
|
+
if (
|
|
1219
|
+
(node._statusFlags & STATUS_PENDING && node._x?._transaction != null) ||
|
|
1220
|
+
flags & REACTIVE_FRAME_READ
|
|
1221
|
+
)
|
|
1222
|
+
schedule();
|
|
1223
|
+
}
|
|
1224
|
+
let child = zombie ? node._x._pendingFirstChild : node._firstChild;
|
|
1225
|
+
if (zombie) node._x._pendingFirstChild = null;
|
|
1226
|
+
else node._firstChild = null;
|
|
1227
|
+
while (child) {
|
|
1228
|
+
const n = child;
|
|
1229
|
+
// Owner teardown is death regardless of the child's own lifecycle
|
|
1230
|
+
// (#3024): strip AUTO_DISPOSE so a post-disposal read freezes at the
|
|
1231
|
+
// last committed value instead of reawakening in a torn-down tree.
|
|
1232
|
+
// Runs before the recursion so already-dormant children (whose
|
|
1233
|
+
// disposeChildren call early-returns on REACTIVE_DISPOSED) die too.
|
|
1234
|
+
// Only unobserved()'s own node keeps its dormancy — it is never in
|
|
1235
|
+
// this loop; its children are rebuilt fresh on reawaken.
|
|
1236
|
+
n._config &= ~CONFIG_AUTO_DISPOSE;
|
|
1237
|
+
// Heap removal must not be gated on `_deps`: a dependency-free
|
|
1238
|
+
// computation queued by refresh() has a null dep list but still sits in
|
|
1239
|
+
// the dirty heap, and left there the post-disposal flush recomputes it —
|
|
1240
|
+
// recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
|
|
1241
|
+
// comes back to life (post-unmount runs, leaked cleanups, #2983).
|
|
1242
|
+
// deleteFromHeap self-guards on the in-heap flags (and tolerates plain
|
|
1243
|
+
// Owners, whose _flags is undefined), so no gate here.
|
|
1244
|
+
deleteFromHeap(n, dirtyQueue);
|
|
1245
|
+
clearDeps(n);
|
|
1246
|
+
// The chain is detached above so a node a cleanup links mid-drain lands
|
|
1247
|
+
// on the fresh head and survives. Pointing each drained child's prev at
|
|
1248
|
+
// itself routes its later splice onto the detached chain, never the head,
|
|
1249
|
+
// and keeps the dev owner-chain-head invariant honest for those children.
|
|
1250
|
+
child._prevSibling = child;
|
|
1251
|
+
disposeChildren(child, true);
|
|
1252
|
+
// Read after, not before: a sibling this disposal made dormant spliced
|
|
1253
|
+
// itself out of the detached chain, and a cleanup may then have linked
|
|
1254
|
+
// it at the fresh head, which rewrote the `_nextSibling` a pre-read
|
|
1255
|
+
// would still be holding.
|
|
1256
|
+
child = child._nextSibling;
|
|
1257
|
+
}
|
|
1258
|
+
if (!zombie) node._childCount = 0;
|
|
1259
|
+
// O(1) splice out of parent's chain on individual dispose. Skipped during
|
|
1260
|
+
// batch dispose (parent already disposed) and for a zombie (it sits on the
|
|
1261
|
+
// parent's parked frame, drained whole by the commit). We leave
|
|
1262
|
+
// node._nextSibling intact so outer walks that already advanced past us
|
|
1263
|
+
// still reach later siblings.
|
|
1264
|
+
if (
|
|
1265
|
+
self &&
|
|
1266
|
+
!zombie &&
|
|
1267
|
+
!(flags & REACTIVE_ZOMBIE) &&
|
|
1268
|
+
node._parent !== null &&
|
|
1269
|
+
!(node._parent._flags & REACTIVE_DISPOSED)
|
|
1270
|
+
) {
|
|
1271
|
+
const prev = node._prevSibling;
|
|
1272
|
+
const next = node._nextSibling;
|
|
1273
|
+
// A node with no predecessor must be the chain's head; otherwise the
|
|
1274
|
+
// write below would clobber the head with a stale `_nextSibling`,
|
|
1275
|
+
// orphaning every live child ahead of it (#3543).
|
|
1276
|
+
assertInvariant(
|
|
1277
|
+
prev !== null || node._parent._firstChild === node,
|
|
1278
|
+
"owner-chain-head",
|
|
1279
|
+
"head node is not parent._firstChild — a node was spliced while flagged live but not in the chain (see #3543)"
|
|
1280
|
+
);
|
|
1281
|
+
if (prev !== null) prev._nextSibling = next;
|
|
1282
|
+
else node._parent._firstChild = next;
|
|
1283
|
+
if (next !== null) next._prevSibling = prev;
|
|
1284
|
+
node._prevSibling = null;
|
|
1285
|
+
}
|
|
1286
|
+
runDisposal(node, zombie);
|
|
1287
|
+
// Final effect-returned cleanup fires at true disposal, after `_disposal`
|
|
1288
|
+
// to mirror rerun ordering (compute-phase teardown first, cleanup last).
|
|
1289
|
+
if (self && node._cleanup) {
|
|
1290
|
+
const effectCleanup = node._cleanup;
|
|
1291
|
+
node._cleanup = undefined;
|
|
1292
|
+
enterDisposal();
|
|
1293
|
+
runCleanup(effectCleanup);
|
|
1294
|
+
exitDisposal();
|
|
1295
|
+
}
|
|
1201
1296
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
externalUntrackHook = untrack;
|
|
1297
|
+
function linkChild(parent, node) {
|
|
1298
|
+
const head = parent._firstChild;
|
|
1299
|
+
node._prevSibling = null;
|
|
1300
|
+
node._nextSibling = head;
|
|
1301
|
+
if (head !== null) head._prevSibling = node;
|
|
1302
|
+
parent._firstChild = node;
|
|
1209
1303
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
// Lanes (plan sec. 28, 2026-10-02; lanes.ts). A lane is "a new base of a transition":
|
|
1236
|
-
// the sub-frame an optimistic write opens in the transition of the frame it
|
|
1237
|
-
// is made in. It sees the screen plus its own guesses (plan sec. 19), breaks out of
|
|
1238
|
-
// the parent's hold — its effects run now — and holds itself if its own
|
|
1239
|
-
// derivations hit async. It ends when the parent lands: a guess reverts to
|
|
1240
|
-
// the truth it covered, or lands as the truth that superseded it. A frame
|
|
1241
|
-
// that does not park has nothing to be optimistic over: the write is void.
|
|
1242
|
-
//
|
|
1243
|
-
// The seat of a pass is its node's: a node carrying a lane's value runs as
|
|
1244
|
-
// the lane's whoever dirtied it; a derivation's tracked read of a lane's
|
|
1245
|
-
// value moves its pass into the lane; a leaf's never does (it reads the
|
|
1246
|
-
// screen). Lane work's values go to the lane slot, its runs to the lane's
|
|
1247
|
-
// queues; the rest of the frame is the flush's.
|
|
1248
|
-
/** The lane the running pass is work of; set at `recompute`'s head from the
|
|
1249
|
-
* node, moved by a derivation's lane read, saved and restored around the
|
|
1250
|
-
* pass. */
|
|
1251
|
-
let passLane = null;
|
|
1252
|
-
function setPassLane(l) {
|
|
1253
|
-
passLane = l;
|
|
1254
|
-
}
|
|
1255
|
-
/** The transaction this flush parks into: set by the first join, merged
|
|
1256
|
-
* with every further one. Consumed at the seam. A join outside a flush (a
|
|
1257
|
-
* mount's pending observer, a write to a held node, an action opening)
|
|
1258
|
-
* schedules the seam that consumes it: membership is the tick's — the same
|
|
1259
|
-
* synchronous frame — and the next tick's writes are not this one's. */
|
|
1260
|
-
let flushTransaction = null;
|
|
1261
|
-
/** The transaction the passes running OUTSIDE a flush this tick derive from
|
|
1262
|
-
* (a mainline mount, a mainline re-pass): the passes' own (A29, creation-
|
|
1263
|
-
* time form — "the entry is the pass's alone"): their results are staged
|
|
1264
|
-
* into it, and nothing else in the tick is — a write made after the mount
|
|
1265
|
-
* is a mainline write, a render effect mounted beside it a stale reader.
|
|
1266
|
-
* Inside a flush, or in a tick that already has its transaction (an
|
|
1267
|
-
* action's body), the frame joins instead (`flushTransaction`) — except a
|
|
1268
|
-
* first pass a loading boundary that has not shown content catches (`own`,
|
|
1269
|
-
* A29's boundary exemption, #3540): it is the boundary's, not the tick's,
|
|
1270
|
-
* and a flush that has joined nothing keeps it pass-scoped as outside one.
|
|
1271
|
-
* Cleared by the flush the join schedules, and at the end of a flush that
|
|
1272
|
-
* set it. */
|
|
1273
|
-
let passTx = null;
|
|
1274
|
-
function joinPassTx(t, own) {
|
|
1275
|
-
if ((globalQueue._running && !own) || flushTransaction !== null) return joinFuture(t);
|
|
1276
|
-
if (passTx === null) passTx = resolveTx(t);
|
|
1277
|
-
else merge(resolveTx(t), passTx);
|
|
1278
|
-
schedule();
|
|
1304
|
+
function runDisposal(node, zombie) {
|
|
1305
|
+
// Detach the list BEFORE running it (#3601), as `_cleanup` is (#2813). A
|
|
1306
|
+
// cleanup that disposes an ancestor re-enters this node through the death
|
|
1307
|
+
// walk while the loop is still running; with the list still attached, that
|
|
1308
|
+
// walk ran every entry a second time. Detached, the re-entrant drain finds
|
|
1309
|
+
// nothing. The same shape latched a throwing cleanup: the list survived the
|
|
1310
|
+
// throw and every later drain re-ran and re-threw it.
|
|
1311
|
+
const disposal = zombie ? node._x._pendingDisposal : node._disposal;
|
|
1312
|
+
if (!disposal) return;
|
|
1313
|
+
if (zombie) node._x._pendingDisposal = null;
|
|
1314
|
+
else node._disposal = null;
|
|
1315
|
+
// Each cleanup is guarded (`runCleanup`), so the depth is always lowered.
|
|
1316
|
+
enterDisposal();
|
|
1317
|
+
if (Array.isArray(disposal)) {
|
|
1318
|
+
// Unwind order (#3572, restores 1.x #1562): later registrations run
|
|
1319
|
+
// before earlier ones. Children have already been disposed by the caller,
|
|
1320
|
+
// so with LIFO a body that registers cleanup before creating its children
|
|
1321
|
+
// tears down after them — the same order a per-component owner gives.
|
|
1322
|
+
for (let i = disposal.length - 1; i >= 0; i--) {
|
|
1323
|
+
runCleanup(disposal[i]);
|
|
1324
|
+
}
|
|
1325
|
+
} else {
|
|
1326
|
+
runCleanup(disposal);
|
|
1327
|
+
}
|
|
1328
|
+
exitDisposal();
|
|
1279
1329
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1330
|
+
function childId(owner, consume) {
|
|
1331
|
+
let counter = owner;
|
|
1332
|
+
while (counter._config & CONFIG_TRANSPARENT && counter._parent) counter = counter._parent;
|
|
1333
|
+
if (counter.id != null)
|
|
1334
|
+
return formatId(counter.id, consume ? counter._childCount++ : counter._childCount);
|
|
1335
|
+
throw new Error("Cannot get child id from owner without an id");
|
|
1284
1336
|
}
|
|
1285
|
-
/**
|
|
1286
|
-
*
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1337
|
+
/**
|
|
1338
|
+
* Allocates and returns the next stable child id for `owner`. Used by
|
|
1339
|
+
* hydration plumbing and `createUniqueId`. Not part of the user-facing API.
|
|
1340
|
+
*
|
|
1341
|
+
* @internal
|
|
1342
|
+
*/
|
|
1343
|
+
function getNextChildId(owner) {
|
|
1344
|
+
return childId(owner, true);
|
|
1290
1345
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1346
|
+
/**
|
|
1347
|
+
* The id a freshly-created node inherits: an explicit `options.id` wins;
|
|
1348
|
+
* transparent nodes share their parent's id; otherwise the parent's next
|
|
1349
|
+
* child id is consumed (or `undefined` outside an id-carrying tree).
|
|
1350
|
+
*/
|
|
1351
|
+
function inheritId(options, transparent, parent) {
|
|
1352
|
+
return (
|
|
1353
|
+
options?.id ??
|
|
1354
|
+
(transparent ? parent?.id : parent?.id != null ? getNextChildId(parent) : undefined)
|
|
1355
|
+
);
|
|
1294
1356
|
}
|
|
1295
|
-
/**
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
if (!globalQueue._running) schedule();
|
|
1357
|
+
/**
|
|
1358
|
+
* Returns the *next* child id for `owner` without consuming it. Used by
|
|
1359
|
+
* hydration plumbing to peek at the id a future child will receive.
|
|
1360
|
+
*
|
|
1361
|
+
* @internal
|
|
1362
|
+
*/
|
|
1363
|
+
function peekNextChildId(owner) {
|
|
1364
|
+
return childId(owner, false);
|
|
1304
1365
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
// Both ends resolved: a stale pointer to a transaction already merged
|
|
1310
|
-
// into the other would close a cycle `resolveTx` never leaves.
|
|
1311
|
-
t = resolveTx(t);
|
|
1312
|
-
f = resolveTx(f);
|
|
1313
|
-
if (t === f || t._lane || f._lane) return;
|
|
1314
|
-
if (attrHooks !== null) attrHooks.transitionMerged(f, t);
|
|
1315
|
-
t._into = f;
|
|
1316
|
-
f._open += t._open;
|
|
1317
|
-
f._acted ||= t._acted;
|
|
1318
|
-
append(f._nodes, t._nodes);
|
|
1319
|
-
append(f._queues[0], t._queues[0]);
|
|
1320
|
-
append(f._queues[1], t._queues[1]);
|
|
1321
|
-
append(f._reruns, t._reruns);
|
|
1322
|
-
if (t._marks !== null) append((f._marks ??= []), t._marks);
|
|
1323
|
-
transactions.splice(transactions.indexOf(t), 1);
|
|
1366
|
+
function formatId(prefix, id) {
|
|
1367
|
+
const num = id.toString(36),
|
|
1368
|
+
len = num.length - 1;
|
|
1369
|
+
return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
|
|
1324
1370
|
}
|
|
1325
|
-
/**
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
|
|
1341
|
-
function
|
|
1342
|
-
|
|
1343
|
-
// it): a zombie whose removal it stages is moot for it alone (`onScreen`).
|
|
1344
|
-
const prev = judge;
|
|
1345
|
-
judge ??= t;
|
|
1346
|
-
// An action still running in it holds it open (action.ts). Its lanes
|
|
1347
|
-
// (lanes.ts): a lane under `t` blocks it while blocked itself.
|
|
1348
|
-
const r = t._open !== 0 || blockedBy(t._nodes, t) || !!lanesBlockedHook?.(t);
|
|
1349
|
-
judge = prev;
|
|
1350
|
-
return r;
|
|
1371
|
+
/**
|
|
1372
|
+
* Returns the currently-tracking observer (the computation that subscribes to
|
|
1373
|
+
* reactive reads at this point), or `null` if reads here would be untracked.
|
|
1374
|
+
* Used by reactive primitives that need to know whether they're inside a
|
|
1375
|
+
* tracking scope. App code rarely needs this — see `getOwner()` for the
|
|
1376
|
+
* lifecycle owner instead.
|
|
1377
|
+
*
|
|
1378
|
+
* @example
|
|
1379
|
+
* ```ts
|
|
1380
|
+
* // Library predicate: only register a hot-path subscription when the
|
|
1381
|
+
* // caller is inside a tracking scope (memo / effect compute / JSX).
|
|
1382
|
+
* function trackIfTracked(source: () => unknown) {
|
|
1383
|
+
* if (getObserver()) source();
|
|
1384
|
+
* }
|
|
1385
|
+
* ```
|
|
1386
|
+
*/
|
|
1387
|
+
function getObserver() {
|
|
1388
|
+
return tracking ? context : null;
|
|
1351
1389
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1390
|
+
/**
|
|
1391
|
+
* Returns the current reactive **owner** — the lifecycle node that the next
|
|
1392
|
+
* `cleanup()` / `onCleanup()` / `createSignal()` etc. will be attached to.
|
|
1393
|
+
*
|
|
1394
|
+
* Returns `null` if called outside any owner. Capture the owner with
|
|
1395
|
+
* `getOwner()` and re-enter it later with `runWithOwner(owner, fn)` to attach
|
|
1396
|
+
* disposables created from a callback (event handler, async resolution, etc.)
|
|
1397
|
+
* back to a component's lifecycle.
|
|
1398
|
+
*
|
|
1399
|
+
* @example
|
|
1400
|
+
* ```ts
|
|
1401
|
+
* function defer<T>(fn: () => T) {
|
|
1402
|
+
* const owner = getOwner();
|
|
1403
|
+
* queueMicrotask(() => runWithOwner(owner, fn));
|
|
1404
|
+
* }
|
|
1405
|
+
* ```
|
|
1406
|
+
*/
|
|
1407
|
+
function getOwner() {
|
|
1408
|
+
return context;
|
|
1370
1409
|
}
|
|
1371
|
-
/**
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
|
|
1377
|
-
|
|
1410
|
+
/**
|
|
1411
|
+
* Low-level: registers `fn` as a disposal callback on the current owner.
|
|
1412
|
+
* Most code should use `onCleanup()` from `solid-js`, which adds dev-mode
|
|
1413
|
+
* checks. `cleanup()` is the unchecked primitive used by internals; `owner`
|
|
1414
|
+
* (internal) targets a specific owner's disposal list instead of the ambient
|
|
1415
|
+
* one.
|
|
1416
|
+
*/
|
|
1417
|
+
function cleanup(fn, owner = context) {
|
|
1418
|
+
if (!owner) return fn;
|
|
1419
|
+
if (!owner._disposal) owner._disposal = fn;
|
|
1420
|
+
else if (Array.isArray(owner._disposal)) owner._disposal.push(fn);
|
|
1421
|
+
else owner._disposal = [owner._disposal, fn];
|
|
1422
|
+
return fn;
|
|
1378
1423
|
}
|
|
1379
|
-
/** A source the pending node waits on that is `t`'s (held by it, or by no
|
|
1380
|
-
* one) — not another transaction's or lane's work. Itself, when it is the
|
|
1381
|
-
* source. */
|
|
1382
|
-
function ownSource(n, t) {
|
|
1383
|
-
const sources = n._x?._pendingSources;
|
|
1384
|
-
if (sources === undefined) return true;
|
|
1385
|
-
for (const s of sources) {
|
|
1386
|
-
const u = s._x?._transaction;
|
|
1387
|
-
if (u == null || resolveTx(u) === t) return true;
|
|
1388
|
-
}
|
|
1389
|
-
return false;
|
|
1390
|
-
}
|
|
1391
|
-
function blockedBy(nodes, owner, own = false) {
|
|
1392
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1393
|
-
const n = nodes[i];
|
|
1394
|
-
if (
|
|
1395
|
-
!(n._statusFlags & STATUS_PENDING) ||
|
|
1396
|
-
n._flags & REACTIVE_DISPOSED ||
|
|
1397
|
-
n._config & CONFIG_GUESS ||
|
|
1398
|
-
(n._x._transaction !== null && txOf(n) !== owner) ||
|
|
1399
|
-
(own && !ownSource(n, owner))
|
|
1400
|
-
)
|
|
1401
|
-
continue;
|
|
1402
|
-
// The frame's own pending observer (off screen: this one holds nothing;
|
|
1403
|
-
// the next listed node may).
|
|
1404
|
-
if (n._type === EFFECT_RENDER) {
|
|
1405
|
-
if (onScreen(n, judge ?? owner)) return true;
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
for (let s = n._subs; s !== null; s = s._nextSub) {
|
|
1409
|
-
const r = s._sub;
|
|
1410
|
-
// A frame reader: a render effect, or a stale reader of the flight
|
|
1411
|
-
// (REACTIVE_FRAME_READ — a verdict reader, `observeFlight`; lane work,
|
|
1412
|
-
// `frameRead`): served committed instead of going pending, it derives
|
|
1413
|
-
// from the flight all the same and is re-derived at the landing — the
|
|
1414
|
-
// frame's observation survives its reader becoming a lane's (a guess
|
|
1415
|
-
// over a held window, V5/A17). A probe alone does not hold. A
|
|
1416
|
-
// boundary showing content forwards the pending (its output pending,
|
|
1417
|
-
// CONFIG_REDERIVE): transparent, its readers hold through it (A33; the
|
|
1418
|
-
// lane-membership ruling, 2026-10-05). An output that never committed
|
|
1419
|
-
// shows nothing — a mount the frame has not revealed — and holds
|
|
1420
|
-
// nothing.
|
|
1421
|
-
if (
|
|
1422
|
-
((r._type === EFFECT_RENDER && r._statusFlags & STATUS_PENDING) ||
|
|
1423
|
-
r._flags & REACTIVE_FRAME_READ ||
|
|
1424
|
-
(r._config & CONFIG_REDERIVE && r._statusFlags === STATUS_PENDING)) &&
|
|
1425
|
-
(s._gen === r._depGen || r._x?._error != null) &&
|
|
1426
|
-
!(r._flags & REACTIVE_DISPOSED) &&
|
|
1427
|
-
onScreen(r, judge ?? owner)
|
|
1428
|
-
)
|
|
1429
|
-
return true;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
return false;
|
|
1433
|
-
}
|
|
1434
|
-
/** A frame reader whose say counts for `t`'s hold — `t` the transaction
|
|
1435
|
-
* being judged: not a zombie whose removal `t` stages (A15 #3463: a re-ask
|
|
1436
|
-
* in the unmount frame lands its pending on the zombie it unmounts — the
|
|
1437
|
-
* compiled <Show> of #3372; a zombie blocks every other judgment while it
|
|
1438
|
-
* is visible, a lane's reveal included: only the commit that disposes it
|
|
1439
|
-
* makes its say moot), and not behind a fallback (A33: a boundary showing its fallback is the display of
|
|
1440
|
-
* everything under it, so a reader there is not on screen and holds
|
|
1441
|
-
* nothing — boundaries.ts). */
|
|
1442
|
-
function onScreen(r, t) {
|
|
1443
|
-
return !(r._flags & REACTIVE_ZOMBIE && removalStagedBy(r, t)) && !hiddenHook?.(r);
|
|
1444
|
-
}
|
|
1445
|
-
/** A15 (#3463): a reader whose removal is staged — a zombie, parked by a
|
|
1446
|
-
* pass awaiting its commit — is still on screen and live for every hold,
|
|
1447
|
-
* except the transaction staging its removal: for that one its say is moot
|
|
1448
|
-
* (done, and the commit disposes it; not done, and it stays parked). The
|
|
1449
|
-
* staging transaction is the one holding the pass that parked the frame:
|
|
1450
|
-
* the nearest non-zombie ancestor. */
|
|
1451
|
-
function removalStagedBy(r, t) {
|
|
1452
|
-
let o = r;
|
|
1453
|
-
while (o?._flags & REACTIVE_ZOMBIE) o = o._parent;
|
|
1454
|
-
return o?._config & CONFIG_HELD && o._x?._transaction != null && txOf(o) === resolveTx(t);
|
|
1455
|
-
}
|
|
1456
|
-
/** Unchanged passes this flush whose stale dependency tail awaits the
|
|
1457
|
-
* flush's verdict (A30, #3469): trimmed when the flush commits, kept when
|
|
1458
|
-
* it parks — the committed frame still derives from the previous pass's
|
|
1459
|
-
* dependencies, and a write to one of them must reach the node until a
|
|
1460
|
-
* committing pass replaces it. */
|
|
1461
|
-
const heldTrims = [];
|
|
1462
|
-
function heldTrim(el) {
|
|
1463
|
-
heldTrims.push(el);
|
|
1464
|
-
}
|
|
1465
|
-
/** Zombies popped from the dirty heap this flush (`deferZombie`). The seam
|
|
1466
|
-
* decides their fate: a park cancels them (the writes that dirtied them are
|
|
1467
|
-
* held with it); a commit runs the survivors — the ones whose owner is still
|
|
1468
|
-
* awaiting its commit — in height order, after the commits that would have
|
|
1469
|
-
* disposed them (#3546). No second heap: the list is the deferral. */
|
|
1470
|
-
const deferredZombies = [];
|
|
1471
|
-
function deferZombie(el) {
|
|
1472
|
-
deleteFromHeap(el, dirtyQueue);
|
|
1473
|
-
el._flags &= -4;
|
|
1474
|
-
deferredZombies.push(el);
|
|
1475
|
-
}
|
|
1476
|
-
/** Put a staged node in a transaction: flagged, stamped, on its landing
|
|
1477
|
-
* list. */
|
|
1478
|
-
function holdNode(n, t) {
|
|
1479
|
-
n._config |= CONFIG_HELD;
|
|
1480
|
-
list(n, t);
|
|
1481
|
-
}
|
|
1482
|
-
/** Put a node on a transaction's list, once: a node's `_transaction` is the
|
|
1483
|
-
* transaction whose `_nodes` it is on (every site that sets one does the
|
|
1484
|
-
* other), so a re-pass, a re-landing or a re-staging adds nothing. */
|
|
1485
|
-
function list(n, t) {
|
|
1486
|
-
const x = ext(n);
|
|
1487
|
-
if (x._transaction === t) return;
|
|
1488
|
-
x._transaction = t;
|
|
1489
|
-
t._nodes.push(n);
|
|
1490
|
-
}
|
|
1491
|
-
/** Hold the (uncommitted) frame under a held pass: every computed in the
|
|
1492
|
-
* live child chain, recursively. Plain owners are walked through, not held
|
|
1493
|
-
* — nothing reads or re-passes them. Once per park, over the parked frames
|
|
1494
|
-
* only; recomputes pay nothing for it. A lane's node in the frame (listed on
|
|
1495
|
-
* a lane: a verdict reader broken out of this very hold — display-ahead — or
|
|
1496
|
-
* a derivation of a guess, CONFIG_OVERRIDE; a lane pass's effect) is the
|
|
1497
|
-
* lane's, as the seam's own loop has it: "lane work never makes its node
|
|
1498
|
-
* transaction work" (A15, #3698). Re-listed here it would carry the lane's
|
|
1499
|
-
* value on the transaction's list, where the landing nulls its transaction
|
|
1500
|
-
* and `dissolveLane` no longer finds it — a lane node with no lane, which the
|
|
1501
|
-
* next read of it trips over (`laneRead` → `txOf`). Its frame is the lane's
|
|
1502
|
-
* too (ruling A: a lane pass's children are the lane's), revealed and
|
|
1503
|
-
* retired at the lane's seam. */
|
|
1504
|
-
function holdFrame(owner, t) {
|
|
1505
|
-
for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
|
|
1506
|
-
if (c._x?._transaction?._lane) continue;
|
|
1507
|
-
if (c._fn !== undefined) {
|
|
1508
|
-
if (!(c._config & CONFIG_HELD)) holdNode(c, t);
|
|
1509
|
-
c._config |= CONFIG_HELD_FRAME;
|
|
1510
|
-
}
|
|
1511
|
-
holdFrame(c, t);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
/** The nodes under `owner` a transaction other than `t` holds (boundaries.ts:
|
|
1515
|
-
* what an earlier hold staged under a re-armed boundary). */
|
|
1516
|
-
function heldUnder(owner, t, fn) {
|
|
1517
|
-
for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
|
|
1518
|
-
if ((c._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && txOf(c) !== t) fn(c);
|
|
1519
|
-
heldUnder(c, t, fn);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
/** `n` leaves the hold it was staged in: its next pass publishes mainline. */
|
|
1523
|
-
function unhold(n) {
|
|
1524
|
-
n._x._transaction = null;
|
|
1525
|
-
n._config &= ~CONFIG_HELD;
|
|
1526
|
-
}
|
|
1527
|
-
const dirtyQueue = {
|
|
1528
|
-
_heap: new Array(2000).fill(undefined),
|
|
1529
|
-
_marked: false,
|
|
1530
|
-
_min: 0,
|
|
1531
|
-
_max: 0
|
|
1532
|
-
};
|
|
1533
|
-
let clock = 0;
|
|
1534
|
-
let scheduled = false;
|
|
1535
|
-
let halted = false;
|
|
1536
|
-
let haltNotified = false;
|
|
1537
|
-
let syncDepth = 0;
|
|
1538
|
-
let inTrackedQueueCallback = false;
|
|
1539
|
-
let _enforceLoadingBoundary = false;
|
|
1540
|
-
let _hitUnhandledAsync = false;
|
|
1541
|
-
// Once per enforcement window: the ASYNC_OUTSIDE_LOADING_BOUNDARY finding is a
|
|
1542
|
-
// fact about the MOUNT ("the root mount will be deferred"), not about each
|
|
1543
|
-
// pending render effect — N async siblings at mount used to produce N copies.
|
|
1544
|
-
let _reportedUnhandledAsync = false;
|
|
1545
1424
|
/**
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1425
|
+
* Returns `true` if the owner has been disposed. Pair with a captured owner
|
|
1426
|
+
* to bail out of late callbacks whose surrounding component already
|
|
1427
|
+
* unmounted.
|
|
1428
|
+
*
|
|
1429
|
+
* @example
|
|
1430
|
+
* ```ts
|
|
1431
|
+
* function onSettleSafe(fn: () => void) {
|
|
1432
|
+
* const owner = getOwner();
|
|
1433
|
+
* queueMicrotask(() => {
|
|
1434
|
+
* if (owner && isDisposed(owner)) return; // component unmounted; skip
|
|
1435
|
+
* runWithOwner(owner, fn);
|
|
1436
|
+
* });
|
|
1437
|
+
* }
|
|
1438
|
+
* ```
|
|
1548
1439
|
*/
|
|
1549
|
-
function
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1440
|
+
function isDisposed(node) {
|
|
1441
|
+
return !!(node._flags & REACTIVE_DISPOSED);
|
|
1442
|
+
}
|
|
1443
|
+
function disposeRootSelf(self = true) {
|
|
1444
|
+
disposeChildren(this, self);
|
|
1445
|
+
// Outside a flush the dispose call rethrows; inside one, the flush does.
|
|
1446
|
+
// Kept out of `disposeChildren`, whose hot path it slows.
|
|
1447
|
+
if (cleanupError && !globalQueue._running) rethrowCleanupError();
|
|
1554
1448
|
}
|
|
1555
1449
|
/**
|
|
1556
|
-
*
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
*
|
|
1450
|
+
* Creates a fresh owner attached as a child of the current owner (or as a
|
|
1451
|
+
* detached root if there is none). Used by framework internals to group
|
|
1452
|
+
* cleanups; app code should use `createRoot()` (host a reactive scope outside
|
|
1453
|
+
* a component) or `runWithOwner()` (re-enter a captured owner).
|
|
1560
1454
|
*
|
|
1561
1455
|
* @internal
|
|
1562
1456
|
*/
|
|
1563
|
-
function
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
//
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1457
|
+
function createOwner(options) {
|
|
1458
|
+
const parent = context;
|
|
1459
|
+
const transparent = options?.transparent ?? false;
|
|
1460
|
+
// Prod and observe boilerplates (see core.ts computed()). The observe
|
|
1461
|
+
// literal carries the `_name` and `_loc` slots the rendering layer fills
|
|
1462
|
+
// with the component label and JSX site (`owner._name = "<App>"`,
|
|
1463
|
+
// `owner._loc = "src/App.tsx:3:5"`) — slots, so labelling a root is a
|
|
1464
|
+
// plain store rather than a shape fork between labelled and plain roots.
|
|
1465
|
+
const owner = {
|
|
1466
|
+
id: inheritId(options, transparent, parent),
|
|
1467
|
+
_config: transparent ? CONFIG_TRANSPARENT : 0,
|
|
1468
|
+
// Present from birth: disposeChildren marks death here on roots too, and
|
|
1469
|
+
// a first store of a missing property is a shape transition plus a
|
|
1470
|
+
// properties-store allocation per root — on a 100×4 table teardown that
|
|
1471
|
+
// was 12 KB and most of disposal's time (2026-10-17).
|
|
1472
|
+
_flags: REACTIVE_NONE,
|
|
1473
|
+
_root: true,
|
|
1474
|
+
_parentComputed: parent?._root ? parent._parentComputed : parent,
|
|
1475
|
+
_firstChild: null,
|
|
1476
|
+
_nextSibling: null,
|
|
1477
|
+
_prevSibling: null,
|
|
1478
|
+
_disposal: null,
|
|
1479
|
+
_context: parent?._context || defaultContext,
|
|
1480
|
+
_childCount: 0,
|
|
1481
|
+
_x: null,
|
|
1482
|
+
_parent: parent,
|
|
1483
|
+
dispose: disposeRootSelf,
|
|
1484
|
+
_name: undefined,
|
|
1485
|
+
_loc: undefined
|
|
1486
|
+
};
|
|
1487
|
+
if (parent && parent._config & CONFIG_CHILDREN_FORBIDDEN) {
|
|
1488
|
+
emitDiagnostic({
|
|
1489
|
+
code: "PRIMITIVE_IN_FORBIDDEN_SCOPE",
|
|
1490
|
+
kind: "lifecycle",
|
|
1491
|
+
severity: "error",
|
|
1492
|
+
message: PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE,
|
|
1493
|
+
ownerId: parent.id,
|
|
1494
|
+
ownerName: parent._name
|
|
1495
|
+
});
|
|
1496
|
+
throw new Error(PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE);
|
|
1497
|
+
}
|
|
1498
|
+
assertNotInEffectCallback();
|
|
1499
|
+
if (parent) linkChild(parent, owner);
|
|
1500
|
+
else registerRoot(owner);
|
|
1501
|
+
DEV.hooks.onOwner?.(owner);
|
|
1502
|
+
return owner;
|
|
1578
1503
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1504
|
+
/**
|
|
1505
|
+
* Creates a reactive root — an owner scope with its own `dispose()`. A root
|
|
1506
|
+
* created inside an existing owner is owned by it and is disposed when the
|
|
1507
|
+
* parent is disposed; call `dispose()` to tear it down earlier. To create a
|
|
1508
|
+
* root that outlives its creator, detach explicitly:
|
|
1509
|
+
* `runWithOwner(null, () => createRoot(...))`. Pass `id` to seed hydration
|
|
1510
|
+
* ids for the tree it owns.
|
|
1511
|
+
*
|
|
1512
|
+
* `dispose()` tears down every signal, memo, effect, and `onCleanup`
|
|
1513
|
+
* registered inside the root.
|
|
1514
|
+
*
|
|
1515
|
+
* Use this to host long-lived reactive scopes outside of a component (custom
|
|
1516
|
+
* controllers, app bootstrapping, tests). Inside a component, prefer
|
|
1517
|
+
* letting Solid's component lifecycle own things.
|
|
1518
|
+
*
|
|
1519
|
+
* @example
|
|
1520
|
+
* ```ts
|
|
1521
|
+
* // At module level there is no owner, so this root lives until disposed.
|
|
1522
|
+
* const dispose = createRoot(dispose => {
|
|
1523
|
+
* const [n, setN] = createSignal(0);
|
|
1524
|
+
* createEffect(() => n(), value => console.log(value));
|
|
1525
|
+
* setInterval(() => setN(x => x + 1), 1000);
|
|
1526
|
+
* return dispose;
|
|
1527
|
+
* });
|
|
1528
|
+
*
|
|
1529
|
+
* // Later, to tear everything down:
|
|
1530
|
+
* dispose();
|
|
1531
|
+
*
|
|
1532
|
+
* // Inside an owner (component, effect, another root), detach explicitly
|
|
1533
|
+
* // if the root must outlive its creator:
|
|
1534
|
+
* const detached = runWithOwner(null, () => createRoot(d => d));
|
|
1535
|
+
* ```
|
|
1536
|
+
*
|
|
1537
|
+
* @description https://v2.solidjs.com/reference/solid-js/advanced/owner-introspection/create-root
|
|
1538
|
+
*/
|
|
1539
|
+
function createRoot(init, options) {
|
|
1540
|
+
const owner = createOwner(options);
|
|
1541
|
+
// A bound function, not an arrow: an arrow would share this scope, which
|
|
1542
|
+
// holds `init`, so keeping `dispose` would keep `init` and all it closes
|
|
1543
|
+
// over alive (#4095).
|
|
1544
|
+
return runWithOwner(owner, () => init(disposeRootSelf.bind(owner, true)));
|
|
1581
1545
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
let
|
|
1591
|
-
|
|
1592
|
-
|
|
1546
|
+
|
|
1547
|
+
// Optional-module hooks: module bindings, not `GlobalQueue` statics, so a
|
|
1548
|
+
// bundle that never imports the installing module never assigns them — its
|
|
1549
|
+
// minifier folds every guarded call site (and the branch behind it) away.
|
|
1550
|
+
// Each module installs its own set at evaluation (pay-for-use).
|
|
1551
|
+
//
|
|
1552
|
+
// Lanes (lanes.ts; installed when `createOptimistic` — or verdict.ts — is
|
|
1553
|
+
// imported; every call site is behind a bit or a `passLane` nothing else sets).
|
|
1554
|
+
let laneReadHook = null;
|
|
1555
|
+
let laneStageHook = null;
|
|
1556
|
+
let laneOutcomeHook = null;
|
|
1557
|
+
let laneWriteHook = null;
|
|
1558
|
+
let applyGuessesHook = null;
|
|
1559
|
+
let laneSeamsHook = null;
|
|
1560
|
+
let laneCorrectionsHook = null;
|
|
1561
|
+
let endLanesHook = null;
|
|
1562
|
+
let lanesBlockedHook = null;
|
|
1563
|
+
function installLaneHooks(
|
|
1564
|
+
read,
|
|
1565
|
+
stage,
|
|
1566
|
+
outcome,
|
|
1567
|
+
write,
|
|
1568
|
+
applyGuesses,
|
|
1569
|
+
seams,
|
|
1570
|
+
corrections,
|
|
1571
|
+
end,
|
|
1572
|
+
blocked
|
|
1573
|
+
) {
|
|
1574
|
+
laneReadHook = read;
|
|
1575
|
+
laneStageHook = stage;
|
|
1576
|
+
laneOutcomeHook = outcome;
|
|
1577
|
+
laneWriteHook = write;
|
|
1578
|
+
applyGuessesHook = applyGuesses;
|
|
1579
|
+
laneSeamsHook = seams;
|
|
1580
|
+
laneCorrectionsHook = corrections;
|
|
1581
|
+
endLanesHook = end;
|
|
1582
|
+
lanesBlockedHook = blocked;
|
|
1593
1583
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1584
|
+
// Boundaries (boundaries.ts): the display consumers between an observer
|
|
1585
|
+
// and the root. `catch` — status from a frame reader, nearest boundary
|
|
1586
|
+
// first (true: caught, the root never hears of it; a clear — flags 0 —
|
|
1587
|
+
// settles the reader there); `fresh` — a pass that read a hold (true: a
|
|
1588
|
+
// first pass a loading boundary that has not shown content caught,
|
|
1589
|
+
// `joinPass`; `held`: the flush's hold owns it, nothing catches it);
|
|
1590
|
+
// `hidden` — a frame reader behind a fallback (the boundary showing it)
|
|
1591
|
+
// is not on screen and holds nothing; `boundarySeam` — the seam's sweep (readers
|
|
1592
|
+
// gone or settled without a pass reveal; an `on` re-arm resolves);
|
|
1593
|
+
// `heldRun` — a queued run under a fallback-showing boundary waits for
|
|
1594
|
+
// the reveal (true: held; the boundary re-queues it by type). The
|
|
1595
|
+
// synchronous first render on creation builds the subtree, attached or
|
|
1596
|
+
// not; its updates and the user effects wait.
|
|
1597
|
+
let catchHook = null;
|
|
1598
|
+
let freshHook = null;
|
|
1599
|
+
let hiddenHook = null;
|
|
1600
|
+
let boundarySeamHook = null;
|
|
1601
|
+
let heldRunHook = null;
|
|
1602
|
+
function installBoundaryHooks(caught, fresh, hidden, seam, heldRun) {
|
|
1603
|
+
catchHook = caught;
|
|
1604
|
+
freshHook = fresh;
|
|
1605
|
+
hiddenHook = hidden;
|
|
1606
|
+
boundarySeamHook = seam;
|
|
1607
|
+
heldRunHook = heldRun;
|
|
1596
1608
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1609
|
+
/** Store (store/store.ts): `park` — the flush parked into `t`: the store
|
|
1610
|
+
* materializes and holds the container node of every staging that had none
|
|
1611
|
+
* (a staging needs the node as its home only when something can read the
|
|
1612
|
+
* frame through it; a hold can); `commit` — fold the pending backings whose
|
|
1613
|
+
* container nodes this flush committed (the owned-raw model's one mutation
|
|
1614
|
+
* point). */
|
|
1615
|
+
let storeParkHook = null;
|
|
1616
|
+
let storeCommitHook = null;
|
|
1617
|
+
function installStoreHooks(park, commit) {
|
|
1618
|
+
storeParkHook = park;
|
|
1619
|
+
storeCommitHook = commit;
|
|
1601
1620
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (!syncDepth && !globalQueue._running) {
|
|
1610
|
-
queueMicrotask(flush);
|
|
1611
|
-
}
|
|
1621
|
+
/** `affects()` marks (affects.ts): the releases at a landing and at the seam
|
|
1622
|
+
* (ambient marks). */
|
|
1623
|
+
let releaseMarksHook = null;
|
|
1624
|
+
let releaseAmbientMarksHook = null;
|
|
1625
|
+
function installMarkHooks(release, releaseAmbient) {
|
|
1626
|
+
releaseMarksHook = release;
|
|
1627
|
+
releaseAmbientMarksHook = releaseAmbient;
|
|
1612
1628
|
}
|
|
1613
|
-
/**
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
*/
|
|
1618
|
-
/**
|
|
1619
|
-
* The key a root owner carries its client error hook under (`render`'s
|
|
1620
|
-
* `onError`) — registered, so a runtime writes it with no import of the hook
|
|
1621
|
-
* module (core/error-hooks.ts) and no property mangling in the way. Defined
|
|
1622
|
-
* HERE, not there: a runtime that only writes the key must not retain the
|
|
1623
|
-
* hook machinery (pay-for-use).
|
|
1624
|
-
*/
|
|
1625
|
-
const ROOT_ERROR_HOOK = Symbol.for("solid-js/root-error-hook");
|
|
1626
|
-
function haltReactivity(cause) {
|
|
1627
|
-
if (halted) return;
|
|
1628
|
-
halted = true;
|
|
1629
|
-
let message = "[REACTIVITY_HALTED]";
|
|
1630
|
-
{
|
|
1631
|
-
message +=
|
|
1632
|
-
" An uncaught error halted the reactive system. No further updates will be processed. Handle errors with <Errored> or treat this as a crash.";
|
|
1633
|
-
emitDiagnostic({
|
|
1634
|
-
code: "REACTIVITY_HALTED",
|
|
1635
|
-
kind: "error",
|
|
1636
|
-
severity: "error",
|
|
1637
|
-
message
|
|
1638
|
-
});
|
|
1639
|
-
}
|
|
1640
|
-
// Surface the cause here too: callers rethrow it, but a creation-time throw
|
|
1641
|
-
// unwinds through ancestor recomputes that convert it to status instead of
|
|
1642
|
-
// surfacing it (#2884), so the rethrow alone cannot guarantee visibility.
|
|
1643
|
-
// Where the platform has one, hand the cause to its uncaught-error channel
|
|
1644
|
-
// (`reportError` → `error` event → window.onerror / error monitoring): a
|
|
1645
|
-
// halt that only reaches console.error leaves a page that LOOKS alive with
|
|
1646
|
-
// nothing an app or its telemetry can act on (#3338 — an uncaught throw
|
|
1647
|
-
// during the hydration render). The rethrow may reach the top as well in
|
|
1648
|
-
// the non-swallowed cases; a duplicate report beats a silent one.
|
|
1649
|
-
const report = cause !== undefined && globalThis.reportError;
|
|
1650
|
-
report || cause === undefined ? console.error(message) : console.error(message, cause);
|
|
1651
|
-
report && report(cause);
|
|
1629
|
+
/** Verdicts (verdict.ts). */
|
|
1630
|
+
let observeFlightHook = null;
|
|
1631
|
+
function installVerdictHooks(observeFlight) {
|
|
1632
|
+
observeFlightHook = observeFlight;
|
|
1652
1633
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1634
|
+
/** External-source bridge (wired by enableExternalSource(); undefined while
|
|
1635
|
+
* no config is active — including after _resetExternalSourceConfig()). */
|
|
1636
|
+
let wireExternalSourceHook = undefined;
|
|
1637
|
+
let externalUntrackHook = undefined;
|
|
1638
|
+
function setExternalHooks(wire, untrack) {
|
|
1639
|
+
wireExternalSourceHook = wire;
|
|
1640
|
+
externalUntrackHook = untrack;
|
|
1660
1641
|
}
|
|
1661
|
-
/**
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1642
|
+
/** Live transactions — opened, unmerged, not landed. Scanned at every seam
|
|
1643
|
+
* while non-empty; nothing on the plain path. */
|
|
1644
|
+
const transactions = [];
|
|
1645
|
+
/** Observe tier: this flush parked its transaction (`holdStart` fired). */
|
|
1646
|
+
let holding = false;
|
|
1647
|
+
/** A transaction (listed here) or a lane (lanes.ts lists its own). */
|
|
1648
|
+
function newTransaction(lane, parent = null) {
|
|
1649
|
+
const t = {
|
|
1650
|
+
_nodes: [],
|
|
1651
|
+
_queues: [[], []],
|
|
1652
|
+
_reruns: [],
|
|
1653
|
+
_into: null,
|
|
1654
|
+
_parent: parent,
|
|
1655
|
+
_lane: lane,
|
|
1656
|
+
_verdict: null,
|
|
1657
|
+
_open: 0,
|
|
1658
|
+
_acted: false,
|
|
1659
|
+
_links: null,
|
|
1660
|
+
_shown: false,
|
|
1661
|
+
_held: false,
|
|
1662
|
+
_marks: null
|
|
1663
|
+
};
|
|
1664
|
+
if (!lane) transactions.push(t);
|
|
1665
|
+
return t;
|
|
1665
1666
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
zombies.sort((a, b) => a._height - b._height);
|
|
1800
|
-
for (let i = 0; i < zombies.length; i++)
|
|
1801
|
-
if (
|
|
1802
|
-
(zombies[i]._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE)) === REACTIVE_ZOMBIE &&
|
|
1803
|
-
zombies[i]._config & CONFIG_VERDICT
|
|
1804
|
-
)
|
|
1805
|
-
recompute(zombies[i]);
|
|
1806
|
-
}
|
|
1807
|
-
// The unchanged passes' tails stay linked (A30, #3469): their inputs
|
|
1808
|
-
// are held, and the committed frame still derives from them — no trim.
|
|
1809
|
-
} else {
|
|
1810
|
-
commitPendingNodes();
|
|
1811
|
-
// The flush committed: an unchanged pass's stale tail goes now. Not
|
|
1812
|
-
// after a later pass this flush that threw (NotReady included — it
|
|
1813
|
-
// keeps its full list, `_depsTail` marking where it stopped) or that
|
|
1814
|
-
// left an effect owing a run (`runEffect` trims when the run applies):
|
|
1815
|
-
// `recompute`'s own gates.
|
|
1816
|
-
for (let i = 0; i < heldTrims.length; i++) {
|
|
1817
|
-
const t = heldTrims[i];
|
|
1818
|
-
if (t._x?._error == null && !t._modified) trimStaleDeps(t);
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
// The list is the flush's either way. Reset only when there is something
|
|
1822
|
-
// to reset (`stagedReaders` below likewise): `length = 0` is a runtime
|
|
1823
|
-
// call even on an empty array, and the plain flush — no unchanged pass,
|
|
1824
|
-
// no lane work — has nothing in either.
|
|
1825
|
-
if (heldTrims.length) heldTrims.length = 0;
|
|
1826
|
-
// Lanes: a blocked one parks its frame (its own flight is up); an
|
|
1827
|
-
// unblocked one reveals this round's work. Then land every transaction
|
|
1828
|
-
// no frame is waiting on — its lanes end with it. Backwards: a landing
|
|
1829
|
-
// removes its entry.
|
|
1830
|
-
// This flush's own runs, the lanes' reveals, and the landings' held runs
|
|
1831
|
-
// are collected apart and ordered below.
|
|
1832
|
-
const own = this._queues;
|
|
1833
|
-
const lanes = (this._queues = [[], []]);
|
|
1834
|
-
// The passes that read this frame's stagings as the screen read held
|
|
1835
|
-
// writes if it parked: the lane seam re-derives them on the committed
|
|
1836
|
-
// world (a lane's runs wait this round).
|
|
1837
|
-
if (laneSeamsHook) {
|
|
1838
|
-
laneSeamsHook(t !== null ? stagedReaders : null);
|
|
1839
|
-
if (stagedReaders.length) stagedReaders.length = 0;
|
|
1840
|
-
}
|
|
1841
|
-
this._queues = [[], []];
|
|
1842
|
-
for (let k = transactions.length - 1; k >= 0; k--) {
|
|
1843
|
-
const u = transactions[k];
|
|
1844
|
-
if (blocked(u)) continue;
|
|
1845
|
-
transactions.splice(k, 1);
|
|
1846
|
-
endLanesHook?.(u);
|
|
1847
|
-
land(u);
|
|
1848
|
-
// A landing's commits can dispose a zombie a transaction judged above
|
|
1849
|
-
// was blocked on (#3463: live "until the commit that disposes it") —
|
|
1850
|
-
// judge them again.
|
|
1851
|
-
k = transactions.length;
|
|
1852
|
-
}
|
|
1853
|
-
// The store folds the pending backings whose container nodes committed
|
|
1854
|
-
// — this flush's, or a landing's (store/store.ts installs it).
|
|
1855
|
-
storeCommitHook?.();
|
|
1856
|
-
// Marks declared outside a transaction: the flush that carried them
|
|
1857
|
-
// parked (something async below) — the window is the frame's, the marks
|
|
1858
|
-
// are its transaction's; else they release at the seam, verdict-only,
|
|
1859
|
-
// nothing to show (affects.ts).
|
|
1860
|
-
const live = t && liveTx(t);
|
|
1861
|
-
releaseAmbientMarksHook?.(live);
|
|
1862
|
-
// The effect phase: lane work first (displayed ahead of the frame), then
|
|
1863
|
-
// this flush's runs, then the runs the landings held from earlier
|
|
1864
|
-
// flushes — as one pass would have queued them (#3540: a shell
|
|
1865
|
-
// re-derived at the landing before a boundary swap held since the
|
|
1866
|
-
// write; #3528: a display-ahead swap before the count it is shown
|
|
1867
|
-
// beside). A parked frame's runs wait with its transaction instead: the
|
|
1868
|
-
// effect phase applies a committed frame.
|
|
1869
|
-
const parked = live !== null;
|
|
1870
|
-
// Attribution hook: this flush found its transaction incomplete — its
|
|
1871
|
-
// writes stay staged, its runs are stashed below. Before the effect
|
|
1872
|
-
// phase (the lanes' display-ahead runs are the visible acknowledgers);
|
|
1873
|
-
// `holdEnd` fires after it (`flush`).
|
|
1874
|
-
if (parked && attrHooks !== null) {
|
|
1875
|
-
attrHooks.holdStart(live);
|
|
1876
|
-
holding = true;
|
|
1877
|
-
}
|
|
1878
|
-
for (let i = 0; i < 2; i++) {
|
|
1879
|
-
if (parked) append(live._queues[i], own[i]);
|
|
1880
|
-
// The plain flush — no reveal, no landing, not parked — has nothing to
|
|
1881
|
-
// order around its own runs: they are the queue as they stand (the
|
|
1882
|
-
// same array, not a copy; `run` takes it whole). Every other seam
|
|
1883
|
-
// builds the ordered queue above.
|
|
1884
|
-
this._queues[i] =
|
|
1885
|
-
parked || lanes[i].length || this._queues[i].length
|
|
1886
|
-
? lanes[i].concat(parked ? [] : own[i], this._queues[i])
|
|
1887
|
-
: own[i];
|
|
1888
|
-
}
|
|
1889
|
-
if (deferredZombies.length !== 0) {
|
|
1890
|
-
// Survivors are the displayed frame of a held pass: they rerun for the
|
|
1891
|
-
// live write, parents before children.
|
|
1892
|
-
const zombies = deferredZombies.splice(0).sort((a, b) => a._height - b._height);
|
|
1893
|
-
for (let i = 0; i < zombies.length; i++) {
|
|
1894
|
-
if (!(zombies[i]._flags & REACTIVE_DISPOSED)) recompute(zombies[i]);
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
// Boundaries: readers that settled, landed or died this flush are
|
|
1898
|
-
// dropped; a fallback with none left reveals next round.
|
|
1899
|
-
boundarySeamHook?.();
|
|
1900
|
-
}
|
|
1901
|
-
run(type) {
|
|
1902
|
-
const effects = this._queues[type - 1];
|
|
1903
|
-
if (effects.length) {
|
|
1904
|
-
this._queues[type - 1] = [];
|
|
1905
|
-
runQueue(effects, type);
|
|
1906
|
-
}
|
|
1667
|
+
// Lanes (plan sec. 28, 2026-10-02; lanes.ts). A lane is "a new base of a transition":
|
|
1668
|
+
// the sub-frame an optimistic write opens in the transition of the frame it
|
|
1669
|
+
// is made in. It sees the screen plus its own guesses (plan sec. 19), breaks out of
|
|
1670
|
+
// the parent's hold — its effects run now — and holds itself if its own
|
|
1671
|
+
// derivations hit async. It ends when the parent lands: a guess reverts to
|
|
1672
|
+
// the truth it covered, or lands as the truth that superseded it. A frame
|
|
1673
|
+
// that does not park has nothing to be optimistic over: the write is void.
|
|
1674
|
+
//
|
|
1675
|
+
// The seat of a pass is its node's: a node carrying a lane's value runs as
|
|
1676
|
+
// the lane's whoever dirtied it; a derivation's tracked read of a lane's
|
|
1677
|
+
// value moves its pass into the lane; a leaf's never does (it reads the
|
|
1678
|
+
// screen). Lane work's values go to the lane slot, its runs to the lane's
|
|
1679
|
+
// queues; the rest of the frame is the flush's.
|
|
1680
|
+
/** The lane the running pass is work of; set at `recompute`'s head from the
|
|
1681
|
+
* node, moved by a derivation's lane read, saved and restored around the
|
|
1682
|
+
* pass. */
|
|
1683
|
+
let passLane = null;
|
|
1684
|
+
function setPassLane(l) {
|
|
1685
|
+
passLane = l;
|
|
1686
|
+
}
|
|
1687
|
+
/** The transaction this flush parks into: set by the first join, merged
|
|
1688
|
+
* with every further one. Consumed at the seam. A join outside a flush (a
|
|
1689
|
+
* mount's pending observer, a write to a held node, an action opening)
|
|
1690
|
+
* schedules the seam that consumes it: membership is the tick's — the same
|
|
1691
|
+
* synchronous frame — and the next tick's writes are not this one's. */
|
|
1692
|
+
let flushTransaction = null;
|
|
1693
|
+
/** The transaction the passes running OUTSIDE a flush this tick derive from
|
|
1694
|
+
* (a mainline mount, a mainline re-pass): the passes' own (A29, creation-
|
|
1695
|
+
* time form — "the entry is the pass's alone"): their results are staged
|
|
1696
|
+
* into it, and nothing else in the tick is — a write made after the mount
|
|
1697
|
+
* is a mainline write, a render effect mounted beside it a stale reader.
|
|
1698
|
+
* Inside a flush, or in a tick that already has its transaction (an
|
|
1699
|
+
* action's body), the frame joins instead (`flushTransaction`) — except a
|
|
1700
|
+
* first pass a loading boundary that has not shown content catches (`own`,
|
|
1701
|
+
* A29's boundary exemption, #3540): it is the boundary's, not the tick's,
|
|
1702
|
+
* and a flush that has joined nothing keeps it pass-scoped as outside one.
|
|
1703
|
+
* Cleared by the flush the join schedules, and at the end of a flush that
|
|
1704
|
+
* set it. */
|
|
1705
|
+
let passTx = null;
|
|
1706
|
+
function joinPassTx(t, own) {
|
|
1707
|
+
if ((globalQueue._running && !own) || flushTransaction !== null) return joinFuture(t);
|
|
1708
|
+
if (passTx === null) passTx = resolveTx(t);
|
|
1709
|
+
else merge(resolveTx(t), passTx);
|
|
1710
|
+
schedule();
|
|
1711
|
+
}
|
|
1712
|
+
/** The transaction holding a node (CONFIG_HELD set), resolved through
|
|
1713
|
+
* merges; the path is compressed. */
|
|
1714
|
+
function txOf(n) {
|
|
1715
|
+
return (n._x._transaction = resolveTx(n._x._transaction));
|
|
1716
|
+
}
|
|
1717
|
+
/** `t` resolved through merges while it is still parked or open; null once
|
|
1718
|
+
* it has landed. */
|
|
1719
|
+
function liveTx(t) {
|
|
1720
|
+
t = resolveTx(t);
|
|
1721
|
+
return transactions.indexOf(t) !== -1 ? t : null;
|
|
1722
|
+
}
|
|
1723
|
+
function resolveTx(t) {
|
|
1724
|
+
while (t._into !== null) t = t._into;
|
|
1725
|
+
return t;
|
|
1726
|
+
}
|
|
1727
|
+
/** The flush joins transaction `t` — or needs one of its own (`null`: a pass
|
|
1728
|
+
* went pending with nothing held). Joining a second one merges it into the
|
|
1729
|
+
* flush's: the work that touched both entangles them (A15). */
|
|
1730
|
+
function joinFuture(t) {
|
|
1731
|
+
const f = flushTransaction;
|
|
1732
|
+
if (f === null) {
|
|
1733
|
+
flushTransaction = t === null ? newTransaction(false) : resolveTx(t);
|
|
1734
|
+
} else if (t !== null) merge(resolveTx(t), f);
|
|
1735
|
+
if (!globalQueue._running) schedule();
|
|
1736
|
+
}
|
|
1737
|
+
/** `t` merges into `f`: the work that touched both entangles them (A15).
|
|
1738
|
+
* A lane never merges: it is a sub-frame, not a peer — its work is routed
|
|
1739
|
+
* per pass (`passLane`). */
|
|
1740
|
+
function merge(t, f) {
|
|
1741
|
+
// Both ends resolved: a stale pointer to a transaction already merged
|
|
1742
|
+
// into the other would close a cycle `resolveTx` never leaves.
|
|
1743
|
+
t = resolveTx(t);
|
|
1744
|
+
f = resolveTx(f);
|
|
1745
|
+
if (t === f || t._lane || f._lane) return;
|
|
1746
|
+
if (attrHooks !== null) attrHooks.transitionMerged(f, t);
|
|
1747
|
+
t._into = f;
|
|
1748
|
+
f._open += t._open;
|
|
1749
|
+
f._acted ||= t._acted;
|
|
1750
|
+
append(f._nodes, t._nodes);
|
|
1751
|
+
append(f._queues[0], t._queues[0]);
|
|
1752
|
+
append(f._queues[1], t._queues[1]);
|
|
1753
|
+
append(f._reruns, t._reruns);
|
|
1754
|
+
if (t._marks !== null) append((f._marks ??= []), t._marks);
|
|
1755
|
+
transactions.splice(transactions.indexOf(t), 1);
|
|
1756
|
+
}
|
|
1757
|
+
/** A15: a hold is a property of the async node — observed pending by a
|
|
1758
|
+
* render reader. A transaction is blocked while a node it holds, in flight,
|
|
1759
|
+
* has one: a pending render effect (the frame itself), or a pending held
|
|
1760
|
+
* node a render effect reads. A stale reader served the committed value
|
|
1761
|
+
* never went pending, but derives from the flight all the same (#3494) and
|
|
1762
|
+
* is still linked to it; a memo between them is pending, and held, too — so
|
|
1763
|
+
* one hop of `_subs` is the chain. Reads of the reader's LAST pass only (the
|
|
1764
|
+
* link's generation is the pass's): a tail kept for A30 — the committed
|
|
1765
|
+
* frame's dependency, awaiting the run that retires it — is not a read of
|
|
1766
|
+
* the flight, and a reader that stopped reading releases it (O3, #3494).
|
|
1767
|
+
* An errored pass (NotReady included) did not stop: it never got there, and
|
|
1768
|
+
* its full list stands (A30) — the reads it did not reach still observe. A
|
|
1769
|
+
* flight nobody renders holds nothing (A29). Only the nodes `t` still owns
|
|
1770
|
+
* count: one a lane took over since is the lane's to wait on. A guess whose
|
|
1771
|
+
* own truth is in flight blocks the lane's parent, not the lane — it stands
|
|
1772
|
+
* in for the flight (A17: "visible until its own fetch settles"). */
|
|
1773
|
+
function blocked(t) {
|
|
1774
|
+
// The transaction being judged (a lane consulted on its behalf judges for
|
|
1775
|
+
// it): a zombie whose removal it stages is moot for it alone (`onScreen`).
|
|
1776
|
+
const prev = judge;
|
|
1777
|
+
judge ??= t;
|
|
1778
|
+
// An action still running in it holds it open (action.ts). Its lanes
|
|
1779
|
+
// (lanes.ts): a lane under `t` blocks it while blocked itself.
|
|
1780
|
+
const r = t._open !== 0 || blockedBy(t._nodes, t) || !!lanesBlockedHook?.(t);
|
|
1781
|
+
judge = prev;
|
|
1782
|
+
return r;
|
|
1783
|
+
}
|
|
1784
|
+
let judge = null;
|
|
1785
|
+
/** Observe tier: the flights `t` waits on — its pending held sources a
|
|
1786
|
+
* frame reader observes (`blockedBy`'s predicate, collected; the readers
|
|
1787
|
+
* themselves are not sources). */
|
|
1788
|
+
function blockersOf(t) {
|
|
1789
|
+
const out = [];
|
|
1790
|
+
for (let i = 0; i < t._nodes.length; i++) {
|
|
1791
|
+
const n = t._nodes[i];
|
|
1792
|
+
if (
|
|
1793
|
+
!n._type &&
|
|
1794
|
+
n._statusFlags & STATUS_PENDING &&
|
|
1795
|
+
!(n._flags & REACTIVE_DISPOSED) &&
|
|
1796
|
+
(n._x._transaction === null || txOf(n) === t) &&
|
|
1797
|
+
blockedBy([n], t)
|
|
1798
|
+
)
|
|
1799
|
+
out.push(n);
|
|
1907
1800
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1801
|
+
return out;
|
|
1802
|
+
}
|
|
1803
|
+
/** `t`'s own flights only — not its lanes': a flight the frame asked for
|
|
1804
|
+
* (a refetch, a plain load) is authoritative; a lane's derivation flight is
|
|
1805
|
+
* not (lanes.ts, the body-end corollary). A pending reader counts only if
|
|
1806
|
+
* one of the sources it waits on is `t`'s (a render effect marked pending
|
|
1807
|
+
* by a lane flight, never re-run, is the lane's wait, not the frame's). */
|
|
1808
|
+
function ownFlights(t) {
|
|
1809
|
+
return blockedBy(t._nodes, t, true);
|
|
1810
|
+
}
|
|
1811
|
+
/** A source the pending node waits on that is `t`'s (held by it, or by no
|
|
1812
|
+
* one) — not another transaction's or lane's work. Itself, when it is the
|
|
1813
|
+
* source. */
|
|
1814
|
+
function ownSource(n, t) {
|
|
1815
|
+
const sources = n._x?._pendingSources;
|
|
1816
|
+
if (sources === undefined) return true;
|
|
1817
|
+
for (const s of sources) {
|
|
1818
|
+
const u = s._x?._transaction;
|
|
1819
|
+
if (u == null || resolveTx(u) === t) return true;
|
|
1915
1820
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
if (
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1821
|
+
return false;
|
|
1822
|
+
}
|
|
1823
|
+
function blockedBy(nodes, owner, own = false) {
|
|
1824
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1825
|
+
const n = nodes[i];
|
|
1826
|
+
if (
|
|
1827
|
+
!(n._statusFlags & STATUS_PENDING) ||
|
|
1828
|
+
n._flags & REACTIVE_DISPOSED ||
|
|
1829
|
+
n._config & CONFIG_GUESS ||
|
|
1830
|
+
(n._x._transaction !== null && txOf(n) !== owner) ||
|
|
1831
|
+
(own && !ownSource(n, owner))
|
|
1832
|
+
)
|
|
1833
|
+
continue;
|
|
1834
|
+
// The frame's own pending observer (off screen: this one holds nothing;
|
|
1835
|
+
// the next listed node may).
|
|
1836
|
+
if (n._type === EFFECT_RENDER) {
|
|
1837
|
+
if (onScreen(n, judge ?? owner)) return true;
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
for (let s = n._subs; s !== null; s = s._nextSub) {
|
|
1841
|
+
const r = s._sub;
|
|
1842
|
+
// A frame reader: a render effect, or a stale reader of the flight
|
|
1843
|
+
// (REACTIVE_FRAME_READ — a verdict reader, `observeFlight`; lane work,
|
|
1844
|
+
// `frameRead`): served committed instead of going pending, it derives
|
|
1845
|
+
// from the flight all the same and is re-derived at the landing — the
|
|
1846
|
+
// frame's observation survives its reader becoming a lane's (a guess
|
|
1847
|
+
// over a held window, V5/A17). A probe alone does not hold. A
|
|
1848
|
+
// boundary showing content forwards the pending (its output pending,
|
|
1849
|
+
// CONFIG_REDERIVE): transparent, its readers hold through it (A33; the
|
|
1850
|
+
// lane-membership ruling, 2026-10-05). An output that never committed
|
|
1851
|
+
// shows nothing — a mount the frame has not revealed — and holds
|
|
1852
|
+
// nothing.
|
|
1853
|
+
if (
|
|
1854
|
+
((r._type === EFFECT_RENDER && r._statusFlags & STATUS_PENDING) ||
|
|
1855
|
+
r._flags & REACTIVE_FRAME_READ ||
|
|
1856
|
+
(r._config & CONFIG_REDERIVE && r._statusFlags === STATUS_PENDING)) &&
|
|
1857
|
+
(s._gen === r._depGen || r._x?._error != null) &&
|
|
1858
|
+
!(r._flags & REACTIVE_DISPOSED) &&
|
|
1859
|
+
onScreen(r, judge ?? owner)
|
|
1860
|
+
)
|
|
1861
|
+
return true;
|
|
1954
1862
|
}
|
|
1955
|
-
return false;
|
|
1956
1863
|
}
|
|
1864
|
+
return false;
|
|
1957
1865
|
}
|
|
1958
|
-
/**
|
|
1959
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
1963
|
-
*
|
|
1964
|
-
* it
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1866
|
+
/** A frame reader whose say counts for `t`'s hold — `t` the transaction
|
|
1867
|
+
* being judged: not a zombie whose removal `t` stages (A15 #3463: a re-ask
|
|
1868
|
+
* in the unmount frame lands its pending on the zombie it unmounts — the
|
|
1869
|
+
* compiled <Show> of #3372; a zombie blocks every other judgment while it
|
|
1870
|
+
* is visible, a lane's reveal included: only the commit that disposes it
|
|
1871
|
+
* makes its say moot), and not behind a fallback (A33: a boundary showing its fallback is the display of
|
|
1872
|
+
* everything under it, so a reader there is not on screen and holds
|
|
1873
|
+
* nothing — boundaries.ts). */
|
|
1874
|
+
function onScreen(r, t) {
|
|
1875
|
+
return !(r._flags & REACTIVE_ZOMBIE && removalStagedBy(r, t)) && !hiddenHook?.(r);
|
|
1876
|
+
}
|
|
1877
|
+
/** A15 (#3463): a reader whose removal is staged — a zombie, parked by a
|
|
1878
|
+
* pass awaiting its commit — is still on screen and live for every hold,
|
|
1879
|
+
* except the transaction staging its removal: for that one its say is moot
|
|
1880
|
+
* (done, and the commit disposes it; not done, and it stays parked). The
|
|
1881
|
+
* staging transaction is the one holding the pass that parked the frame:
|
|
1882
|
+
* the nearest non-zombie ancestor. */
|
|
1883
|
+
function removalStagedBy(r, t) {
|
|
1884
|
+
let o = r;
|
|
1885
|
+
while (o?._flags & REACTIVE_ZOMBIE) o = o._parent;
|
|
1886
|
+
return o?._config & CONFIG_HELD && o._x?._transaction != null && txOf(o) === resolveTx(t);
|
|
1887
|
+
}
|
|
1888
|
+
/** Unchanged passes this flush whose stale dependency tail awaits the
|
|
1889
|
+
* flush's verdict (A30, #3469): trimmed when the flush commits, kept when
|
|
1890
|
+
* it parks — the committed frame still derives from the previous pass's
|
|
1891
|
+
* dependencies, and a write to one of them must reach the node until a
|
|
1892
|
+
* committing pass replaces it. */
|
|
1893
|
+
const heldTrims = [];
|
|
1894
|
+
function heldTrim(el) {
|
|
1895
|
+
heldTrims.push(el);
|
|
1896
|
+
}
|
|
1897
|
+
/** Zombies popped from the dirty heap this flush (`deferZombie`). The seam
|
|
1898
|
+
* decides their fate: a park cancels them (the writes that dirtied them are
|
|
1899
|
+
* held with it); a commit runs the survivors — the ones whose owner is still
|
|
1900
|
+
* awaiting its commit — in height order, after the commits that would have
|
|
1901
|
+
* disposed them (#3546). No second heap: the list is the deferral. */
|
|
1902
|
+
const deferredZombies = [];
|
|
1903
|
+
function deferZombie(el) {
|
|
1904
|
+
deleteFromHeap(el, dirtyQueue);
|
|
1905
|
+
el._flags &= -4;
|
|
1906
|
+
deferredZombies.push(el);
|
|
1907
|
+
}
|
|
1908
|
+
/** Put a staged node in a transaction: flagged, stamped, on its landing
|
|
1909
|
+
* list. */
|
|
1910
|
+
function holdNode(n, t) {
|
|
1911
|
+
n._config |= CONFIG_HELD;
|
|
1912
|
+
list(n, t);
|
|
1913
|
+
}
|
|
1914
|
+
/** Put a node on a transaction's list, once: a node's `_transaction` is the
|
|
1915
|
+
* transaction whose `_nodes` it is on (every site that sets one does the
|
|
1916
|
+
* other), so a re-pass, a re-landing or a re-staging adds nothing. */
|
|
1917
|
+
function list(n, t) {
|
|
1918
|
+
const x = ext(n);
|
|
1919
|
+
if (x._transaction === t) return;
|
|
1920
|
+
x._transaction = t;
|
|
1921
|
+
t._nodes.push(n);
|
|
1922
|
+
}
|
|
1923
|
+
/** Hold the (uncommitted) frame under a held pass: every computed in the
|
|
1924
|
+
* live child chain, recursively. Plain owners are walked through, not held
|
|
1925
|
+
* — nothing reads or re-passes them. Once per park, over the parked frames
|
|
1926
|
+
* only; recomputes pay nothing for it. A lane's node in the frame (listed on
|
|
1927
|
+
* a lane: a verdict reader broken out of this very hold — display-ahead — or
|
|
1928
|
+
* a derivation of a guess, CONFIG_OVERRIDE; a lane pass's effect) is the
|
|
1929
|
+
* lane's, as the seam's own loop has it: "lane work never makes its node
|
|
1930
|
+
* transaction work" (A15, #3698). Re-listed here it would carry the lane's
|
|
1931
|
+
* value on the transaction's list, where the landing nulls its transaction
|
|
1932
|
+
* and `dissolveLane` no longer finds it — a lane node with no lane, which the
|
|
1933
|
+
* next read of it trips over (`laneRead` → `txOf`). Its frame is the lane's
|
|
1934
|
+
* too (ruling A: a lane pass's children are the lane's), revealed and
|
|
1935
|
+
* retired at the lane's seam. */
|
|
1936
|
+
function holdFrame(owner, t) {
|
|
1937
|
+
for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
|
|
1938
|
+
if (c._x?._transaction?._lane) continue;
|
|
1939
|
+
if (c._fn !== undefined) {
|
|
1940
|
+
if (!(c._config & CONFIG_HELD)) holdNode(c, t);
|
|
1941
|
+
c._config |= CONFIG_HELD_FRAME;
|
|
1980
1942
|
}
|
|
1943
|
+
holdFrame(c, t);
|
|
1981
1944
|
}
|
|
1982
|
-
u._reruns.length = 0;
|
|
1983
1945
|
}
|
|
1984
|
-
/** The
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
if (u._marks !== null) releaseMarksHook(u._marks);
|
|
1991
|
-
// Old children die in the commits (cleanups first), then the stashed
|
|
1992
|
-
// effects run ahead of this flush's own. A node a lane took over since
|
|
1993
|
-
// (a guess written over the staged truth) is the lane's to land.
|
|
1994
|
-
for (let i = 0; i < u._nodes.length; i++) {
|
|
1995
|
-
const n = u._nodes[i];
|
|
1996
|
-
if (n._x._transaction === null || txOf(n) !== u) continue;
|
|
1997
|
-
n._x._transaction = null;
|
|
1998
|
-
commitPendingNode(n);
|
|
1946
|
+
/** The nodes under `owner` a transaction other than `t` holds (boundaries.ts:
|
|
1947
|
+
* what an earlier hold staged under a re-armed boundary). */
|
|
1948
|
+
function heldUnder(owner, t, fn) {
|
|
1949
|
+
for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
|
|
1950
|
+
if ((c._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && txOf(c) !== t) fn(c);
|
|
1951
|
+
heldUnder(c, t, fn);
|
|
1999
1952
|
}
|
|
2000
|
-
// A cleanup those commits ran that wrote a node `u` still held joined
|
|
2001
|
-
// `u`, which has left `transactions`: the write is this landing's, and
|
|
2002
|
-
// the next flush must not park in it (#4089).
|
|
2003
|
-
if (flushTransaction === u) flushTransaction = null;
|
|
2004
|
-
releaseQueues(u);
|
|
2005
1953
|
}
|
|
2006
|
-
/**
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
function releaseQueues(u) {
|
|
2011
|
-
for (let i = 0; i < 2; i++) {
|
|
2012
|
-
append(globalQueue._queues[i], u._queues[i]);
|
|
2013
|
-
u._queues[i] = [];
|
|
2014
|
-
}
|
|
1954
|
+
/** `n` leaves the hold it was staged in: its next pass publishes mainline. */
|
|
1955
|
+
function unhold(n) {
|
|
1956
|
+
n._x._transaction = null;
|
|
1957
|
+
n._config &= ~CONFIG_HELD;
|
|
2015
1958
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1959
|
+
const dirtyQueue = {
|
|
1960
|
+
_heap: new Array(2000).fill(undefined),
|
|
1961
|
+
_marked: false,
|
|
1962
|
+
_min: 0,
|
|
1963
|
+
_max: 0
|
|
1964
|
+
};
|
|
1965
|
+
let clock = 0;
|
|
1966
|
+
let scheduled = false;
|
|
1967
|
+
let halted = false;
|
|
1968
|
+
let haltNotified = false;
|
|
1969
|
+
// Dev: where the halt happened (the owner path and source lines of its
|
|
1970
|
+
// diagnostic), repeated on the "Update ignored" notice so a frozen interaction
|
|
1971
|
+
// points back at the computation that threw (#4178).
|
|
1972
|
+
let haltedAt = "";
|
|
1973
|
+
// Dev: the computation the halt in progress is located at (haltReactivityAt).
|
|
1974
|
+
let haltSubject;
|
|
1975
|
+
let syncDepth = 0;
|
|
1976
|
+
let inTrackedQueueCallback = false;
|
|
1977
|
+
let _enforceLoadingBoundary = false;
|
|
1978
|
+
let _hitUnhandledAsync = false;
|
|
1979
|
+
// Once per enforcement window: the ASYNC_OUTSIDE_LOADING_BOUNDARY finding is a
|
|
1980
|
+
// fact about the MOUNT ("the root mount will be deferred"), not about each
|
|
1981
|
+
// pending render effect — N async siblings at mount used to produce N copies.
|
|
1982
|
+
let _reportedUnhandledAsync = false;
|
|
1983
|
+
/**
|
|
1984
|
+
* Consume the unhandled-async hit. Returns whether this is the first report
|
|
1985
|
+
* of the current enforcement window — the caller warns only then.
|
|
1986
|
+
*/
|
|
1987
|
+
function resetUnhandledAsync() {
|
|
1988
|
+
_hitUnhandledAsync = false;
|
|
1989
|
+
if (_reportedUnhandledAsync) return false;
|
|
1990
|
+
_reportedUnhandledAsync = true;
|
|
1991
|
+
return true;
|
|
2019
1992
|
}
|
|
2020
|
-
/**
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2025
|
-
*
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
if (!(c._flags & REACTIVE_FRAME_READ)) {
|
|
2032
|
-
c._flags |= REACTIVE_FRAME_READ;
|
|
2033
|
-
t._reruns.push(c);
|
|
2034
|
-
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Toggles the dev-mode "must be inside a `<Loading>` boundary" enforcement
|
|
1995
|
+
* window. Only `render()` calls this — wrapping the initial mount so that a
|
|
1996
|
+
* top-level uncaught async read surfaces the diagnostic. Not part of the
|
|
1997
|
+
* user-facing API.
|
|
1998
|
+
*
|
|
1999
|
+
* @internal
|
|
2000
|
+
*/
|
|
2001
|
+
function enforceLoadingBoundary(enabled) {
|
|
2002
|
+
_enforceLoadingBoundary = enabled;
|
|
2003
|
+
if (enabled) _reportedUnhandledAsync = false;
|
|
2035
2004
|
}
|
|
2036
|
-
function
|
|
2037
|
-
|
|
2038
|
-
pendingNodes.push(node);
|
|
2039
|
-
if (!globalQueue._running) markUnflushedStaged(); // A28
|
|
2005
|
+
function setTrackedQueueCallback(value) {
|
|
2006
|
+
inTrackedQueueCallback = value;
|
|
2040
2007
|
}
|
|
2041
|
-
// Dev-only
|
|
2042
|
-
//
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
let notifyEpoch = 0;
|
|
2050
|
-
function bumpNotifyEpoch() {
|
|
2051
|
-
notifyEpoch++;
|
|
2008
|
+
// Dev-only marker for the effect half of createEffect/createRenderEffect, so
|
|
2009
|
+
// flush() can report the no-op instead of failing silently (React parity).
|
|
2010
|
+
/** Action bodies on the stack (action.ts): `flush()` is refused inside one —
|
|
2011
|
+
* the body's writes are its transaction's and commit when it settles; a
|
|
2012
|
+
* drain mid-step cannot reveal them, and dev says so. */
|
|
2013
|
+
let actionDepth = 0;
|
|
2014
|
+
function enterAction() {
|
|
2015
|
+
actionDepth++;
|
|
2052
2016
|
}
|
|
2053
|
-
function
|
|
2054
|
-
|
|
2055
|
-
// the next walk for this node while the epoch holds (marking is idempotent).
|
|
2056
|
-
node._notifiedAt = notifyEpoch;
|
|
2057
|
-
// Presence bits gate the optional-slot probes (see constants.ts): one
|
|
2058
|
-
// masked read of the always-present _config instead of missing-property
|
|
2059
|
-
// lookups in the hottest notify loop.
|
|
2060
|
-
const cfg = node._config;
|
|
2061
|
-
const hasSnapshot = (cfg & CONFIG_HAS_SNAPSHOT) !== 0 && node._x?._snapshotValue !== undefined;
|
|
2062
|
-
// Observe-tier fan-out: this walk visits every subscriber edge anyway, so
|
|
2063
|
-
// the graph-size count is one local increment here and no field anywhere.
|
|
2064
|
-
let fanOut = 0;
|
|
2065
|
-
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
2066
|
-
const sub = s._sub;
|
|
2067
|
-
fanOut++;
|
|
2068
|
-
// Missed-wake latch (#3037): this write is landing while the subscriber
|
|
2069
|
-
// is mid-recompute (a nested pull committing beneath its reads), and the
|
|
2070
|
-
// heap refuses RECOMPUTING nodes. A gen-current link means the pass
|
|
2071
|
-
// already validated this dep — the value it read is now stale — so latch
|
|
2072
|
-
// for recompute's tail to reschedule. Untouched links need no latch (the
|
|
2073
|
-
// pass either re-reads them fresh or trims them), and neither does the
|
|
2074
|
-
// tail link: it is the read IN FLIGHT — read() links before it pulls, so
|
|
2075
|
-
// this very commit is what that read returns.
|
|
2076
|
-
if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
|
|
2077
|
-
sub._flags |= REACTIVE_MISSED_WAKE;
|
|
2078
|
-
if (hasSnapshot && sub._config & CONFIG_IN_SNAPSHOT_SCOPE) {
|
|
2079
|
-
sub._flags |= REACTIVE_SNAPSHOT_STALE;
|
|
2080
|
-
continue;
|
|
2081
|
-
}
|
|
2082
|
-
enqueueSub(sub);
|
|
2083
|
-
}
|
|
2084
|
-
if (fanOut >= GRAPH_SIZE_WARN_AT) noteFanOut(node, fanOut);
|
|
2017
|
+
function exitAction() {
|
|
2018
|
+
actionDepth--;
|
|
2085
2019
|
}
|
|
2086
|
-
/**
|
|
2087
|
-
* its
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
if (t?._lane ? sameLane(t, l) : s._sub._type) s._sub._flags |= REACTIVE_LANE_DIRTY;
|
|
2098
|
-
}
|
|
2020
|
+
/** Provenance (A18, #3331): the question being asked — an action's
|
|
2021
|
+
* sequence while a slice of its body and the flush that carries it run, 0
|
|
2022
|
+
* otherwise. A guess is stamped with it (lanes.ts; a mainline guess takes a
|
|
2023
|
+
* fresh sequence: the newest intent), a flight with the question that
|
|
2024
|
+
* started it (async.ts; mainline is always the current question). An
|
|
2025
|
+
* answer from an older question than the guess it lands on is stale: held
|
|
2026
|
+
* silently, it does not move the graph. */
|
|
2027
|
+
let question = 0;
|
|
2028
|
+
let questions = 0;
|
|
2029
|
+
function setQuestion(q) {
|
|
2030
|
+
question = q;
|
|
2099
2031
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
return a === b || (a._links !== null && a._links === b._links);
|
|
2032
|
+
function nextQuestion() {
|
|
2033
|
+
return ++questions;
|
|
2103
2034
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2035
|
+
const MAINLINE_QUESTION = 0x7fffffff;
|
|
2036
|
+
let inEffectCallback = false;
|
|
2037
|
+
/** Dev-only: matches no event, so a window or phase holding it applies
|
|
2038
|
+
* whatever event is current (see `strictReadEvent`, core.ts). */
|
|
2039
|
+
const ANY_EVENT = {};
|
|
2040
|
+
/** Dev-only: the DOM event current when the effect phase was entered. A
|
|
2041
|
+
* handler dispatched synchronously from that phase (`el.focus()`) runs under
|
|
2042
|
+
* a different event and is not in it (#4235). */
|
|
2043
|
+
let effectCallbackEvent = ANY_EVENT;
|
|
2044
|
+
function setEffectCallback(value, event = ANY_EVENT) {
|
|
2045
|
+
{
|
|
2046
|
+
inEffectCallback = value;
|
|
2047
|
+
effectCallbackEvent = event;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
function schedule() {
|
|
2051
|
+
if (halted) {
|
|
2052
|
+
notifyHalted();
|
|
2117
2053
|
return;
|
|
2118
2054
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
const err = n._config & CONFIG_STAGED_ERROR;
|
|
2124
|
-
// An answer commits: the error it replaces leaves the display (D-41).
|
|
2125
|
-
if (!inFlight && c._x) c._x._shownError = undefined;
|
|
2126
|
-
n._config = inFlight ? (n._config & -1048833) | CONFIG_INPUTS_PUBLISHED : n._config & -3147137;
|
|
2127
|
-
// D-41: the staged answer is an error — published now, when a value would
|
|
2128
|
-
// show, through the ordinary error path (`stageError`). Its readers first
|
|
2129
|
-
// see it in the cycle after this seam: a tracked re-read there is not a
|
|
2130
|
-
// retry (`read`), or a boundary re-derived by it would retry it forever.
|
|
2131
|
-
if (err) {
|
|
2132
|
-
const e = n._pendingValue;
|
|
2133
|
-
n._pendingValue = NOT_PENDING;
|
|
2134
|
-
n._time = clock + 1;
|
|
2135
|
-
notifyStatus(c, STATUS_ERROR, e);
|
|
2136
|
-
} else if (n._pendingValue !== NOT_PENDING) {
|
|
2137
|
-
n._value = n._pendingValue;
|
|
2138
|
-
n._pendingValue = NOT_PENDING;
|
|
2139
|
-
// A node born staged (recompute) initializes at this commit.
|
|
2140
|
-
c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
2141
|
-
// A quiet re-ask's classification survives its landing and dies with
|
|
2142
|
-
// the landed value's commit (A19 exc. 2, #3178): verdict-quiet through
|
|
2143
|
-
// the reveal. (The sweep of a node still in flight is not a landing.)
|
|
2144
|
-
// The A28 stash goes with it.
|
|
2145
|
-
if (c._x != null) ((c._x._reask = false), (c._x._flushed = NOT_PENDING));
|
|
2146
|
-
// An effect with a staged value was born held (L2): its first run is
|
|
2147
|
-
// this commit's, not its creation's (A29) — queue it now.
|
|
2148
|
-
if (n._type && n._type !== EFFECT_TRACKED) {
|
|
2149
|
-
n._modified = true;
|
|
2150
|
-
globalQueue.enqueue(n._type, (n._boundRunEffect ??= GlobalQueue._runEffect.bind(null, c)));
|
|
2151
|
-
}
|
|
2055
|
+
if (scheduled) return;
|
|
2056
|
+
scheduled = true;
|
|
2057
|
+
if (!syncDepth && !globalQueue._running) {
|
|
2058
|
+
queueMicrotask(flush);
|
|
2152
2059
|
}
|
|
2153
|
-
// The committed value is the first observable answer for a loading-window
|
|
2154
|
-
// node — the window closes here, not at compute time (#2990). Unconditional
|
|
2155
|
-
// store to an always-present computed slot.
|
|
2156
|
-
c._loading = false;
|
|
2157
|
-
c._flags &= ~REACTIVE_MANUAL_WRITE;
|
|
2158
|
-
// The dependencies of the pass that produced the value are the frame's now:
|
|
2159
|
-
// the previous frame's tail goes (A30, #3410; `recompute` left it for a
|
|
2160
|
-
// staged pass). Only after a clean pass: `_error` is cleared by a clean
|
|
2161
|
-
// pass or by the node's own landing (whose pass was clean), so a set
|
|
2162
|
-
// `_error` means the last pass threw, kept its full list, and `_depsTail`
|
|
2163
|
-
// marks where it stopped.
|
|
2164
|
-
if (c._x?._error == null) trimStaleDeps(c);
|
|
2165
|
-
if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
2166
|
-
// L2: the children this commit publishes are the frame's now — the frame
|
|
2167
|
-
// they replace, parked by the pass (`recompute`), goes.
|
|
2168
|
-
if (
|
|
2169
|
-
!inFlight &&
|
|
2170
|
-
c._x != null &&
|
|
2171
|
-
(c._x._pendingFirstChild !== null || c._x._pendingDisposal !== null)
|
|
2172
|
-
)
|
|
2173
|
-
GlobalQueue._dispose(c, false, true);
|
|
2174
2060
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2061
|
+
/**
|
|
2062
|
+
* Permanently halts the reactive system. Called when a user error escapes
|
|
2063
|
+
* every boundary — app state is undefined at that point, so scheduling stops
|
|
2064
|
+
* entirely rather than limping along with a half-applied update.
|
|
2065
|
+
*/
|
|
2066
|
+
/**
|
|
2067
|
+
* The key a root owner carries its client error hook under (`render`'s
|
|
2068
|
+
* `onError`) — registered, so a runtime writes it with no import of the hook
|
|
2069
|
+
* module (core/error-hooks.ts) and no property mangling in the way. Defined
|
|
2070
|
+
* HERE, not there: a runtime that only writes the key must not retain the
|
|
2071
|
+
* hook machinery (pay-for-use).
|
|
2072
|
+
*/
|
|
2073
|
+
const ROOT_ERROR_HOOK = Symbol.for("solid-js/root-error-hook");
|
|
2074
|
+
function haltReactivity(cause) {
|
|
2075
|
+
if (halted) return;
|
|
2076
|
+
halted = true;
|
|
2077
|
+
// A cleanup error collected before the halt is dropped (owner.ts).
|
|
2078
|
+
rethrowCleanupError(true);
|
|
2079
|
+
let message = "[REACTIVITY_HALTED]";
|
|
2080
|
+
{
|
|
2081
|
+
message +=
|
|
2082
|
+
" An uncaught error halted the reactive system. No further updates will be processed. Handle errors with <Errored> or treat this as a crash.";
|
|
2083
|
+
// Located at the computation the error escaped from when the call site
|
|
2084
|
+
// named it (haltReactivityAt), so the console says where it happened
|
|
2085
|
+
// (`in <App> › <Composer>`), not only that it did (#4178). Otherwise the
|
|
2086
|
+
// ambient owner, as every other diagnostic.
|
|
2087
|
+
const entry = emitDiagnostic(
|
|
2088
|
+
{
|
|
2089
|
+
code: "REACTIVITY_HALTED",
|
|
2090
|
+
kind: "error",
|
|
2091
|
+
severity: "error",
|
|
2092
|
+
message
|
|
2093
|
+
},
|
|
2094
|
+
haltSubject
|
|
2095
|
+
);
|
|
2096
|
+
haltedAt = locationLines(entry);
|
|
2097
|
+
message += haltedAt;
|
|
2182
2098
|
}
|
|
2183
|
-
|
|
2099
|
+
// Surface the cause here too: callers rethrow it, but a creation-time throw
|
|
2100
|
+
// unwinds through ancestor recomputes that convert it to status instead of
|
|
2101
|
+
// surfacing it (#2884), so the rethrow alone cannot guarantee visibility.
|
|
2102
|
+
// Where the platform has one, hand the cause to its uncaught-error channel
|
|
2103
|
+
// (`reportError` → `error` event → window.onerror / error monitoring): a
|
|
2104
|
+
// halt that only reaches console.error leaves a page that LOOKS alive with
|
|
2105
|
+
// nothing an app or its telemetry can act on (#3338 — an uncaught throw
|
|
2106
|
+
// during the hydration render). The rethrow may reach the top as well in
|
|
2107
|
+
// the non-swallowed cases; a duplicate report beats a silent one.
|
|
2108
|
+
const report = cause !== undefined && globalThis.reportError;
|
|
2109
|
+
report || cause === undefined ? console.error(message) : console.error(message, cause);
|
|
2110
|
+
report && report(cause);
|
|
2184
2111
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2112
|
+
/**
|
|
2113
|
+
* Dev: `haltReactivity`, located at `subject` — the computation the uncaught
|
|
2114
|
+
* error escaped from — for the REACTIVITY_HALTED message's owner path. Call
|
|
2115
|
+
* sites pick it in dev builds only (`haltReactivity(e)` in production), so
|
|
2116
|
+
* production keeps the one-argument halt and its bytes unchanged.
|
|
2117
|
+
*/
|
|
2118
|
+
function haltReactivityAt(cause, subject) {
|
|
2119
|
+
haltSubject = subject;
|
|
2120
|
+
haltReactivity(cause);
|
|
2121
|
+
haltSubject = undefined;
|
|
2122
|
+
}
|
|
2123
|
+
// Logs on the first write after a halt so a frozen interaction is traceable.
|
|
2124
|
+
function notifyHalted() {
|
|
2125
|
+
if (haltNotified) return;
|
|
2126
|
+
haltNotified = true;
|
|
2127
|
+
console.error(
|
|
2128
|
+
"[REACTIVITY_HALTED] Update ignored: the reactive system was halted by an earlier uncaught error." +
|
|
2129
|
+
haltedAt
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
/** @internal Test/dev-reload hook. Revives scheduling after a halt. */
|
|
2133
|
+
function resetErrorHalt() {
|
|
2134
|
+
halted = false;
|
|
2135
|
+
haltNotified = false;
|
|
2136
|
+
haltedAt = "";
|
|
2137
|
+
}
|
|
2138
|
+
class GlobalQueue {
|
|
2139
|
+
_queues = [[], []];
|
|
2140
|
+
_running = false;
|
|
2141
|
+
flush() {
|
|
2142
|
+
if (this._running) return;
|
|
2143
|
+
this._running = true;
|
|
2144
|
+
// A28: companions of nodes written since the last flush mirror the
|
|
2145
|
+
// flushed world — re-synced inside the running window (the write is
|
|
2146
|
+
// "flushed" from here on).
|
|
2147
|
+
resyncUnflushedCompanions();
|
|
2148
|
+
passLane = null;
|
|
2149
|
+
passTx = null;
|
|
2199
2150
|
try {
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
//
|
|
2203
|
-
//
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2151
|
+
// Sweep before the heap: unobserved() pulls swept nodes out of it, so
|
|
2152
|
+
// a dormant memo dirtied in the same tick is reclaimed instead of
|
|
2153
|
+
// recomputed; late subscribers (an effect reading a swept memo this
|
|
2154
|
+
// flush) revive it, which is the pay-for-use contract.
|
|
2155
|
+
sweepDormant();
|
|
2156
|
+
runHeap(dirtyQueue, GlobalQueue._update);
|
|
2157
|
+
this.settle();
|
|
2158
|
+
clock++;
|
|
2159
|
+
// A write the commit sweep staged with no subscriber to dirty is work
|
|
2160
|
+
// too — the next round commits it.
|
|
2161
|
+
scheduled = dirtyQueue._max >= dirtyQueue._min || pendingNodes.length !== 0;
|
|
2162
|
+
this.run(EFFECT_RENDER);
|
|
2163
|
+
this.run(EFFECT_USER);
|
|
2164
|
+
if (true && holding) {
|
|
2165
|
+
holding = false;
|
|
2166
|
+
attrHooks.holdEnd();
|
|
2167
|
+
}
|
|
2168
|
+
if (true && !scheduled) {
|
|
2169
|
+
// Fully drained: no staged value may survive this point unqueued or
|
|
2170
|
+
// unheld.
|
|
2171
|
+
devCheckQuiescent(
|
|
2172
|
+
n => pendingNodes.includes(n) || (n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) !== 0
|
|
2173
|
+
);
|
|
2208
2174
|
}
|
|
2175
|
+
if (true) DEV.hooks.onUpdate?.();
|
|
2176
|
+
} finally {
|
|
2177
|
+
this._running = false;
|
|
2178
|
+
passTx = null;
|
|
2209
2179
|
}
|
|
2210
2180
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2181
|
+
/** L2 — the seam: end of the pure phase. Commit this flush's staged nodes,
|
|
2182
|
+
* or park them with the transaction the flush joined; then land every
|
|
2183
|
+
* transaction no frame is waiting on. The landing flush is an ordinary
|
|
2184
|
+
* flush: the settle walk (async.ts) re-ran the parked readers in its pure
|
|
2185
|
+
* phase, and here no flight the transaction holds has a frame deriving
|
|
2186
|
+
* from it (`blocked`). */
|
|
2187
|
+
settle() {
|
|
2188
|
+
// Lanes: an ended action body judges its guesses now that their passes
|
|
2189
|
+
// have run (lanes.ts); a correction's re-derivations run in one more
|
|
2190
|
+
// pure round — the parent's flights before the parent is judged below.
|
|
2191
|
+
if (laneCorrectionsHook?.()) runHeap(dirtyQueue, GlobalQueue._update);
|
|
2192
|
+
const joined = flushTransaction;
|
|
2193
|
+
flushTransaction = null;
|
|
2194
|
+
// The frame's verdict is in: the guesses written since the last seam
|
|
2195
|
+
// open their lane under the frame's transaction (a lane's own flush
|
|
2196
|
+
// parents a nested lane to it), or are dropped — the frame commits, and
|
|
2197
|
+
// there is nothing to be optimistic over. Their passes run next round;
|
|
2198
|
+
// a parent landing at this very seam ends the lane before they do.
|
|
2199
|
+
applyGuessesHook?.(joined);
|
|
2200
|
+
// A lane's own flush (its flight landing, its reveal) parks nothing: the
|
|
2201
|
+
// lane's work reached it per pass; the frame's plain writes commit.
|
|
2202
|
+
// (Resolved: a guess over another transaction's guess merged them.)
|
|
2203
|
+
const t = joined !== null && !joined._lane ? resolveTx(joined) : null;
|
|
2204
|
+
if (t !== null) {
|
|
2205
|
+
// `t._nodes` grows under the walk (holdFrame pushes); the bound is
|
|
2206
|
+
// this flush's list, which does not.
|
|
2207
|
+
const count = pendingNodes.length;
|
|
2208
|
+
for (let i = 0; i < count; i++) {
|
|
2209
|
+
const n = pendingNodes[i];
|
|
2210
|
+
// Already held: the pass that re-staged it joined its transaction,
|
|
2211
|
+
// which is `t` (or merged into it). Lane work is the lane's (a
|
|
2212
|
+
// pending mark propagated onto it queued it here too).
|
|
2213
|
+
if (
|
|
2214
|
+
!(n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) &&
|
|
2215
|
+
(n._x === null || n._x._transaction === null || !n._x._transaction._lane)
|
|
2216
|
+
) {
|
|
2217
|
+
// A loading source — pending with nothing staged and no committed
|
|
2218
|
+
// value (A16, A19 exc. 1: uninitialized is loading, not pending)
|
|
2219
|
+
// that is not an observer — is nobody's frame: not held. Its
|
|
2220
|
+
// first landing is commit #0, not a resumption of this frame, and
|
|
2221
|
+
// a write that reaches it later overlaps nothing (#2937: the
|
|
2222
|
+
// stale stamp was the bridge that entangled unrelated updates
|
|
2223
|
+
// with a never-resolving flight). Its observers are held and
|
|
2224
|
+
// wait on its flight (`blocked`).
|
|
2225
|
+
if (
|
|
2226
|
+
n._pendingValue === NOT_PENDING &&
|
|
2227
|
+
n._statusFlags & STATUS_UNINITIALIZED &&
|
|
2228
|
+
n._type !== EFFECT_RENDER
|
|
2229
|
+
)
|
|
2230
|
+
continue;
|
|
2231
|
+
// A34 (2): a tick whose writes net to the committed value made no
|
|
2232
|
+
// proposal (`setShow(false); setShow(true)` on a committed true) —
|
|
2233
|
+
// not staged, not held, pends nothing; a later write to it is
|
|
2234
|
+
// plain mainline. (A node already held stays held when written
|
|
2235
|
+
// back: A34 (1).)
|
|
2236
|
+
if (
|
|
2237
|
+
n._pendingValue !== NOT_PENDING &&
|
|
2238
|
+
!(n._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) &&
|
|
2239
|
+
n._x?._pendingFirstChild == null &&
|
|
2240
|
+
n._x?._pendingDisposal == null &&
|
|
2241
|
+
// The comparator last: a pass that created children (an object
|
|
2242
|
+
// owning a fresh async memo, #4111) must not have it read them.
|
|
2243
|
+
// A NotReady read there is a change (`isSame`).
|
|
2244
|
+
isSame(n, n._value, n._pendingValue)
|
|
2245
|
+
) {
|
|
2246
|
+
n._pendingValue = NOT_PENDING;
|
|
2247
|
+
n._config &= ~CONFIG_STAGED;
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
holdNode(n, t);
|
|
2251
|
+
}
|
|
2252
|
+
// A held pass's frame is held with it (ruling A, O2): the children it
|
|
2253
|
+
// created are the transaction's — they compute, and a pass over them
|
|
2254
|
+
// or a write that reaches them joins; they effect at the landing.
|
|
2255
|
+
if (n._config & CONFIG_STAGED) holdFrame(n, t);
|
|
2256
|
+
}
|
|
2257
|
+
pendingNodes.length = 0;
|
|
2258
|
+
storeParkHook?.(t);
|
|
2259
|
+
// (This flush's runs are stashed with `t` below — after the landings,
|
|
2260
|
+
// so a `t` that lands at this very seam runs them first, ahead of
|
|
2261
|
+
// the runs it held from earlier flushes.)
|
|
2262
|
+
// The batch joins: the writes that dirtied these are held with it — a
|
|
2263
|
+
// zombie never displays that world. A verdict reader among them does
|
|
2264
|
+
// (#3444: `latest()` inside a branch a held Show is removing peers
|
|
2265
|
+
// through like the same read outside): it re-runs now, as the
|
|
2266
|
+
// holder's verdict lane's work — before the lane seams, which reveal
|
|
2267
|
+
// it (nothing commits this seam that could dispose it).
|
|
2268
|
+
// (Verdict readers exist only with the verdict engine installed.)
|
|
2269
|
+
const zombies = deferredZombies.splice(0);
|
|
2270
|
+
if (observeFlightHook) {
|
|
2271
|
+
zombies.sort((a, b) => a._height - b._height);
|
|
2272
|
+
for (let i = 0; i < zombies.length; i++)
|
|
2273
|
+
if (
|
|
2274
|
+
(zombies[i]._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE)) === REACTIVE_ZOMBIE &&
|
|
2275
|
+
zombies[i]._config & CONFIG_VERDICT
|
|
2276
|
+
)
|
|
2277
|
+
recompute(zombies[i]);
|
|
2278
|
+
}
|
|
2279
|
+
// The unchanged passes' tails stay linked (A30, #3469): their inputs
|
|
2280
|
+
// are held, and the committed frame still derives from them — no trim.
|
|
2281
|
+
} else {
|
|
2282
|
+
commitPendingNodes();
|
|
2283
|
+
// The flush committed: an unchanged pass's stale tail goes now. Not
|
|
2284
|
+
// after a later pass this flush that threw (NotReady included — it
|
|
2285
|
+
// keeps its full list, `_depsTail` marking where it stopped) or that
|
|
2286
|
+
// left an effect owing a run (`runEffect` trims when the run applies):
|
|
2287
|
+
// `recompute`'s own gates.
|
|
2288
|
+
for (let i = 0; i < heldTrims.length; i++) {
|
|
2289
|
+
const t = heldTrims[i];
|
|
2290
|
+
if (t._x?._error == null && !t._modified) trimStaleDeps(t);
|
|
2291
|
+
}
|
|
2217
2292
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2293
|
+
// The list is the flush's either way. Reset only when there is something
|
|
2294
|
+
// to reset (`stagedReaders` below likewise): `length = 0` is a runtime
|
|
2295
|
+
// call even on an empty array, and the plain flush — no unchanged pass,
|
|
2296
|
+
// no lane work — has nothing in either.
|
|
2297
|
+
if (heldTrims.length) heldTrims.length = 0;
|
|
2298
|
+
// Lanes: a blocked one parks its frame (its own flight is up); an
|
|
2299
|
+
// unblocked one reveals this round's work. Then land every transaction
|
|
2300
|
+
// no frame is waiting on — its lanes end with it. Backwards: a landing
|
|
2301
|
+
// removes its entry.
|
|
2302
|
+
// This flush's own runs, the lanes' reveals, and the landings' held runs
|
|
2303
|
+
// are collected apart and ordered below.
|
|
2304
|
+
const own = this._queues;
|
|
2305
|
+
const lanes = (this._queues = [[], []]);
|
|
2306
|
+
// The passes that read this frame's stagings as the screen read held
|
|
2307
|
+
// writes if it parked: the lane seam re-derives them on the committed
|
|
2308
|
+
// world (a lane's runs wait this round).
|
|
2309
|
+
if (laneSeamsHook) {
|
|
2310
|
+
laneSeamsHook(t !== null ? stagedReaders : null);
|
|
2311
|
+
if (stagedReaders.length) stagedReaders.length = 0;
|
|
2312
|
+
}
|
|
2313
|
+
this._queues = [[], []];
|
|
2314
|
+
for (let k = transactions.length - 1; k >= 0; k--) {
|
|
2315
|
+
const u = transactions[k];
|
|
2316
|
+
if (blocked(u)) continue;
|
|
2317
|
+
transactions.splice(k, 1);
|
|
2318
|
+
endLanesHook?.(u);
|
|
2319
|
+
land(u);
|
|
2320
|
+
// A landing's commits can dispose a zombie a transaction judged above
|
|
2321
|
+
// was blocked on (#3463: live "until the commit that disposes it") —
|
|
2322
|
+
// judge them again.
|
|
2323
|
+
k = transactions.length;
|
|
2230
2324
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2325
|
+
// The store folds the pending backings whose container nodes committed
|
|
2326
|
+
// — this flush's, or a landing's (store/store.ts installs it).
|
|
2327
|
+
storeCommitHook?.();
|
|
2328
|
+
// Marks declared outside a transaction: the flush that carried them
|
|
2329
|
+
// parked (something async below) — the window is the frame's, the marks
|
|
2330
|
+
// are its transaction's; else they release at the seam, verdict-only,
|
|
2331
|
+
// nothing to show (affects.ts).
|
|
2332
|
+
const live = t && liveTx(t);
|
|
2333
|
+
releaseAmbientMarksHook?.(live);
|
|
2334
|
+
// The effect phase: lane work first (displayed ahead of the frame), then
|
|
2335
|
+
// this flush's runs, then the runs the landings held from earlier
|
|
2336
|
+
// flushes — as one pass would have queued them (#3540: a shell
|
|
2337
|
+
// re-derived at the landing before a boundary swap held since the
|
|
2338
|
+
// write; #3528: a display-ahead swap before the count it is shown
|
|
2339
|
+
// beside). A parked frame's runs wait with its transaction instead: the
|
|
2340
|
+
// effect phase applies a committed frame.
|
|
2341
|
+
const parked = live !== null;
|
|
2342
|
+
// Attribution hook: this flush found its transaction incomplete — its
|
|
2343
|
+
// writes stay staged, its runs are stashed below. Before the effect
|
|
2344
|
+
// phase (the lanes' display-ahead runs are the visible acknowledgers);
|
|
2345
|
+
// `holdEnd` fires after it (`flush`).
|
|
2346
|
+
if (parked && attrHooks !== null) {
|
|
2347
|
+
attrHooks.holdStart(live);
|
|
2348
|
+
holding = true;
|
|
2249
2349
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
for (let i = 0; i < queue.length; i++) queue[i](type);
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
/**
|
|
2264
|
-
* Schedule one subscriber to re-run on the next flush: inserted into the
|
|
2265
|
-
* dirty heap with the `_min` cursor pulled down. Tracked effects ride the
|
|
2266
|
-
* heap too — the heap visit is their (empty) compute phase, which hands the
|
|
2267
|
-
* callback to the user queue once the pass has committed (see
|
|
2268
|
-
* GlobalQueue._update, #3291).
|
|
2269
|
-
*/
|
|
2270
|
-
function enqueueSub(node) {
|
|
2271
|
-
if (dirtyQueue._min > node._height) dirtyQueue._min = node._height;
|
|
2272
|
-
insertIntoHeap(node, dirtyQueue);
|
|
2273
|
-
}
|
|
2274
|
-
function actualInsertIntoHeap(n, heap) {
|
|
2275
|
-
const parentHeight =
|
|
2276
|
-
(n._parent?._root ? n._parent._parentComputed?._height : n._parent?._height) ?? -1;
|
|
2277
|
-
if (parentHeight >= n._height) n._height = parentHeight + 1;
|
|
2278
|
-
const height = n._height;
|
|
2279
|
-
const heapAtHeight = heap._heap[height];
|
|
2280
|
-
if (heapAtHeight === undefined) heap._heap[height] = n;
|
|
2281
|
-
else {
|
|
2282
|
-
const tail = heapAtHeight._prevHeap;
|
|
2283
|
-
tail._nextHeap = n;
|
|
2284
|
-
n._prevHeap = tail;
|
|
2285
|
-
heapAtHeight._prevHeap = n;
|
|
2286
|
-
}
|
|
2287
|
-
if (height > heap._max) heap._max = height;
|
|
2288
|
-
}
|
|
2289
|
-
function insertIntoHeap(n, heap) {
|
|
2290
|
-
let flags = n._flags;
|
|
2291
|
-
// RECOMPUTING refusals are not always losses: a genuinely missed wake (a
|
|
2292
|
-
// write to a link this pass already validated) is latched link-side in
|
|
2293
|
-
// insertSubs as REACTIVE_MISSED_WAKE for recompute's tail (#3037).
|
|
2294
|
-
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS)) return;
|
|
2295
|
-
if (flags & REACTIVE_CHECK) {
|
|
2296
|
-
n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
2297
|
-
} else {
|
|
2298
|
-
n._flags = flags | REACTIVE_IN_HEAP;
|
|
2299
|
-
// An unmarked node entering an already-marked heap is marked on the
|
|
2300
|
-
// spot, keeping the markHeap memo valid. `_marked` is only reset by
|
|
2301
|
-
// runHeap, so a write between two mid-tick pulls (read-time markHeap +
|
|
2302
|
-
// updateIfNecessary) would otherwise leave this node unmarked and every
|
|
2303
|
-
// downstream pull stale until the next flush (#2922: the second
|
|
2304
|
-
// `latest()` returned the first write's value). Invalidating the memo
|
|
2305
|
-
// instead re-walked the WHOLE heap on the next pull — with N effects
|
|
2306
|
-
// parked in the heap for a synchronous mount (each row writing a ref
|
|
2307
|
-
// signal its effect subscribes to), mounting N rows was O(N²) (#3350).
|
|
2308
|
-
// markNode's own guard skips an already-DIRTY node.
|
|
2309
|
-
if (heap._marked) markNode(n);
|
|
2310
|
-
}
|
|
2311
|
-
if (!(flags & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(n, heap);
|
|
2312
|
-
}
|
|
2313
|
-
function insertIntoHeapHeight(n, heap) {
|
|
2314
|
-
let flags = n._flags;
|
|
2315
|
-
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_IN_HEAP_HEIGHT)) return;
|
|
2316
|
-
n._flags = flags | REACTIVE_IN_HEAP_HEIGHT;
|
|
2317
|
-
actualInsertIntoHeap(n, heap);
|
|
2318
|
-
}
|
|
2319
|
-
function deleteFromHeap(n, heap) {
|
|
2320
|
-
const flags = n._flags;
|
|
2321
|
-
if (!(flags & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT))) return;
|
|
2322
|
-
n._flags = flags & -25;
|
|
2323
|
-
const height = n._height;
|
|
2324
|
-
if (n._prevHeap === n) heap._heap[height] = undefined;
|
|
2325
|
-
else {
|
|
2326
|
-
const next = n._nextHeap;
|
|
2327
|
-
const dhh = heap._heap[height];
|
|
2328
|
-
const end = next ?? dhh;
|
|
2329
|
-
if (n === dhh) heap._heap[height] = next;
|
|
2330
|
-
else n._prevHeap._nextHeap = next;
|
|
2331
|
-
end._prevHeap = n._prevHeap;
|
|
2332
|
-
}
|
|
2333
|
-
n._prevHeap = n;
|
|
2334
|
-
n._nextHeap = undefined;
|
|
2335
|
-
}
|
|
2336
|
-
function markHeap(heap) {
|
|
2337
|
-
if (heap._marked) return;
|
|
2338
|
-
heap._marked = true;
|
|
2339
|
-
for (let i = 0; i <= heap._max; i++) {
|
|
2340
|
-
for (let el = heap._heap[i]; el !== undefined; el = el._nextHeap) {
|
|
2341
|
-
if (el._flags & REACTIVE_IN_HEAP) markNode(el);
|
|
2350
|
+
for (let i = 0; i < 2; i++) {
|
|
2351
|
+
if (parked) append(live._queues[i], own[i]);
|
|
2352
|
+
// The plain flush — no reveal, no landing, not parked — has nothing to
|
|
2353
|
+
// order around its own runs: they are the queue as they stand (the
|
|
2354
|
+
// same array, not a copy; `run` takes it whole). Every other seam
|
|
2355
|
+
// builds the ordered queue above.
|
|
2356
|
+
this._queues[i] =
|
|
2357
|
+
parked || lanes[i].length || this._queues[i].length
|
|
2358
|
+
? lanes[i].concat(parked ? [] : own[i], this._queues[i])
|
|
2359
|
+
: own[i];
|
|
2342
2360
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
markNode(link._sub, REACTIVE_CHECK);
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
function runHeap(heap, recompute) {
|
|
2354
|
-
heap._marked = false;
|
|
2355
|
-
for (heap._min = 0; heap._min <= heap._max; heap._min++) {
|
|
2356
|
-
let el = heap._heap[heap._min];
|
|
2357
|
-
while (el !== undefined) {
|
|
2358
|
-
if (el._flags & REACTIVE_IN_HEAP) recompute(el);
|
|
2359
|
-
else adjustHeight(el, heap);
|
|
2360
|
-
el = heap._heap[heap._min];
|
|
2361
|
+
if (deferredZombies.length !== 0) {
|
|
2362
|
+
// Survivors are the displayed frame of a held pass: they rerun for the
|
|
2363
|
+
// live write, parents before children.
|
|
2364
|
+
const zombies = deferredZombies.splice(0).sort((a, b) => a._height - b._height);
|
|
2365
|
+
for (let i = 0; i < zombies.length; i++) {
|
|
2366
|
+
if (!(zombies[i]._flags & REACTIVE_DISPOSED)) recompute(zombies[i]);
|
|
2367
|
+
}
|
|
2361
2368
|
}
|
|
2369
|
+
// Boundaries: readers that settled, landed or died this flush are
|
|
2370
|
+
// dropped; a fallback with none left reveals next round.
|
|
2371
|
+
boundarySeamHook?.();
|
|
2362
2372
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
const dep = d._dep;
|
|
2370
|
-
if (dep._fn && dep._height >= newHeight) newHeight = dep._height + 1;
|
|
2373
|
+
run(type) {
|
|
2374
|
+
const effects = this._queues[type - 1];
|
|
2375
|
+
if (effects.length) {
|
|
2376
|
+
this._queues[type - 1] = [];
|
|
2377
|
+
runQueue(effects, type);
|
|
2378
|
+
}
|
|
2371
2379
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2380
|
+
enqueue(type, fn, lane = passLane) {
|
|
2381
|
+
// Lane work's runs are the lane's: they run at its reveal, not with
|
|
2382
|
+
// the frame (which may park). A lane pass that read the frame's own
|
|
2383
|
+
// staging is the frame's (`recompute` passes null): its run waits with
|
|
2384
|
+
// the frame.
|
|
2385
|
+
if (type) (lane !== null ? lane : this)._queues[type - 1].push(fn);
|
|
2386
|
+
schedule();
|
|
2375
2387
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2388
|
+
/** Status reaching the root: pending is absorbed (the root mount defers —
|
|
2389
|
+
* dev records it for the ASYNC_OUTSIDE_LOADING_BOUNDARY FYI); an error is
|
|
2390
|
+
* unhandled (`false` → the caller halts).
|
|
2391
|
+
*
|
|
2392
|
+
* L2: this is where a transaction is registered. Pending that reaches an
|
|
2393
|
+
* observer (a render effect: the frame) is a frame that cannot show — the
|
|
2394
|
+
* flush parks, and the transaction waits on the flight while a frame
|
|
2395
|
+
* derives from it (`blocked`). Pending nobody observes holds nothing: a
|
|
2396
|
+
* memo in flight that no frame reads is not a frame waiting. (Base: the
|
|
2397
|
+
* queue's `_asyncReporters`, INV-3's one registration site.)
|
|
2398
|
+
*
|
|
2399
|
+
* A boundary between the observer and the root (boundaries.ts) is asked
|
|
2400
|
+
* first: a fallback catches what is not ready under it, and the root
|
|
2401
|
+
* never hears of it. */
|
|
2402
|
+
notify(node, mask, flags, error) {
|
|
2403
|
+
if (catchHook?.(node, flags & mask, error)) return true;
|
|
2404
|
+
if (mask & STATUS_PENDING) {
|
|
2405
|
+
if (flags & STATUS_PENDING) {
|
|
2406
|
+
if (_enforceLoadingBoundary) _hitUnhandledAsync = true;
|
|
2407
|
+
// A pass that went pending holds its frame: the async's own
|
|
2408
|
+
// synchronous frame is not ready (A15 reveal corollary: a reveal
|
|
2409
|
+
// that discovers a flight whose inputs are visible holds and joins
|
|
2410
|
+
// it; one whose inputs are held is a stale reader and never gets
|
|
2411
|
+
// here — `read` serves it the committed value). The frame's, not the
|
|
2412
|
+
// observer's: a render effect belongs to whoever dirtied its pass
|
|
2413
|
+
// (A15 shared-hole, #3407) — one held by T1 and re-run by T2's write
|
|
2414
|
+
// pends for T2's flight, and the two stay parallel; a frame that is
|
|
2415
|
+
// T1's already joined T1 through the write or the memo that made it
|
|
2416
|
+
// so. Outside a flush — root setup, a mount — the frame is the
|
|
2417
|
+
// mount's: its pending observers are one transaction, and a flight
|
|
2418
|
+
// started in it resumes it when it lands (maintainer, #3461: "part
|
|
2419
|
+
// of the same one"); what the mount published synchronously stays
|
|
2420
|
+
// published. Lane work going pending holds the lane, not the frame:
|
|
2421
|
+
// `blocked(lane)` finds it in the lane's staging ("if further async
|
|
2422
|
+
// downstream is hit it holds like its own transition").
|
|
2423
|
+
if (passLane === null) joinFuture(null);
|
|
2424
|
+
}
|
|
2425
|
+
return true;
|
|
2426
|
+
}
|
|
2427
|
+
return false;
|
|
2391
2428
|
}
|
|
2392
2429
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
if (self) {
|
|
2419
|
-
node._flags = flags | REACTIVE_DISPOSED;
|
|
2420
|
-
if (node._parent === null && node._root) unregisterRoot(node);
|
|
2421
|
-
}
|
|
2422
|
-
if (self && true) clearSignals(node);
|
|
2423
|
-
if (self && node._fn) {
|
|
2424
|
-
if (node._x !== null) node._x._inFlight = null;
|
|
2425
|
-
// A death that can unblock the future (L2, `futureBlocked`): a held
|
|
2426
|
-
// flight's — a transaction's or a lane's (its `_x._transaction`) — or a
|
|
2427
|
-
// stale reader's (REACTIVE_FRAME_READ — a frame that derived from one).
|
|
2428
|
-
// Nothing else re-examines a dead node, so make sure a seam comes.
|
|
2429
|
-
if (
|
|
2430
|
-
(node._statusFlags & STATUS_PENDING && node._x?._transaction != null) ||
|
|
2431
|
-
flags & REACTIVE_FRAME_READ
|
|
2432
|
-
)
|
|
2433
|
-
schedule();
|
|
2434
|
-
}
|
|
2435
|
-
let child = zombie ? node._x._pendingFirstChild : node._firstChild;
|
|
2436
|
-
if (zombie) node._x._pendingFirstChild = null;
|
|
2437
|
-
else node._firstChild = null;
|
|
2438
|
-
while (child) {
|
|
2439
|
-
const n = child;
|
|
2440
|
-
// Owner teardown is death regardless of the child's own lifecycle
|
|
2441
|
-
// (#3024): strip AUTO_DISPOSE so a post-disposal read freezes at the
|
|
2442
|
-
// last committed value instead of reawakening in a torn-down tree.
|
|
2443
|
-
// Runs before the recursion so already-dormant children (whose
|
|
2444
|
-
// disposeChildren call early-returns on REACTIVE_DISPOSED) die too.
|
|
2445
|
-
// Only unobserved()'s own node keeps its dormancy — it is never in
|
|
2446
|
-
// this loop; its children are rebuilt fresh on reawaken.
|
|
2447
|
-
n._config &= ~CONFIG_AUTO_DISPOSE;
|
|
2448
|
-
// Heap removal must not be gated on `_deps`: a dependency-free
|
|
2449
|
-
// computation queued by refresh() has a null dep list but still sits in
|
|
2450
|
-
// the dirty heap, and left there the post-disposal flush recomputes it —
|
|
2451
|
-
// recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
|
|
2452
|
-
// comes back to life (post-unmount runs, leaked cleanups, #2983).
|
|
2453
|
-
// deleteFromHeap self-guards on the in-heap flags (and tolerates plain
|
|
2454
|
-
// Owners, whose _flags is undefined), so no gate here.
|
|
2455
|
-
deleteFromHeap(n, dirtyQueue);
|
|
2456
|
-
clearDeps(n);
|
|
2457
|
-
// The chain is detached above so a node a cleanup links mid-drain lands
|
|
2458
|
-
// on the fresh head and survives. Pointing each drained child's prev at
|
|
2459
|
-
// itself routes its later splice onto the detached chain, never the head,
|
|
2460
|
-
// and keeps the dev owner-chain-head invariant honest for those children.
|
|
2461
|
-
child._prevSibling = child;
|
|
2462
|
-
disposeChildren(child, true);
|
|
2463
|
-
// Read after, not before: a sibling this disposal made dormant spliced
|
|
2464
|
-
// itself out of the detached chain, and a cleanup may then have linked
|
|
2465
|
-
// it at the fresh head, which rewrote the `_nextSibling` a pre-read
|
|
2466
|
-
// would still be holding.
|
|
2467
|
-
child = child._nextSibling;
|
|
2430
|
+
/** A15's stale readers: a frame whose LAST pass read a held node as
|
|
2431
|
+
* committed re-derives on the landed world — next round, after the commits.
|
|
2432
|
+
* One re-derived since (any later pass cleared the bit: in the transaction,
|
|
2433
|
+
* or without the held read) owes nothing. A run this transaction stashed
|
|
2434
|
+
* for it is void (#3322: an effect has one value slot, and the stale pass
|
|
2435
|
+
* overwrote the value that run was for) — the re-derivation's run replaces
|
|
2436
|
+
* it. Truthy when an effect re-derives (`land`). */
|
|
2437
|
+
function reruns(u) {
|
|
2438
|
+
let frame;
|
|
2439
|
+
for (let i = 0; i < u._reruns.length; i++) {
|
|
2440
|
+
const r = u._reruns[i];
|
|
2441
|
+
// An errored reader's display is its error (D-39): the landing gives it
|
|
2442
|
+
// nothing to re-derive, and a re-run would be a read that retries the
|
|
2443
|
+
// errored source (#3945).
|
|
2444
|
+
if (r._flags & REACTIVE_DISPOSED || r._statusFlags & STATUS_ERROR) continue;
|
|
2445
|
+
if (r._flags & REACTIVE_FRAME_READ || r._config & CONFIG_VERDICT) {
|
|
2446
|
+
frame ||= r._type;
|
|
2447
|
+
r._flags &= ~REACTIVE_FRAME_READ;
|
|
2448
|
+
r._modified = false;
|
|
2449
|
+
// Its frame is replaced by the re-derivation: a run one of its
|
|
2450
|
+
// children queued this flush (re-run by the same landing) is for the
|
|
2451
|
+
// frame being replaced — void too (#3404).
|
|
2452
|
+
for (let c = r._firstChild; c !== null; c = c._nextSibling) if (c._type) c._modified = false;
|
|
2453
|
+
enqueueSub(r);
|
|
2454
|
+
}
|
|
2468
2455
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
//
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
)
|
|
2482
|
-
|
|
2483
|
-
const
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
assertInvariant(
|
|
2488
|
-
prev !== null || node._parent._firstChild === node,
|
|
2489
|
-
"owner-chain-head",
|
|
2490
|
-
"head node is not parent._firstChild — a node was spliced while flagged live but not in the chain (see #3543)"
|
|
2491
|
-
);
|
|
2492
|
-
if (prev !== null) prev._nextSibling = next;
|
|
2493
|
-
else node._parent._firstChild = next;
|
|
2494
|
-
if (next !== null) next._prevSibling = prev;
|
|
2495
|
-
node._prevSibling = null;
|
|
2456
|
+
u._reruns.length = 0;
|
|
2457
|
+
return frame;
|
|
2458
|
+
}
|
|
2459
|
+
/** The landing: the held frame becomes the frame. */
|
|
2460
|
+
function land(u) {
|
|
2461
|
+
// Attribution hook: judged complete — its held writes commit next
|
|
2462
|
+
// (`_nodes` still lists them).
|
|
2463
|
+
if (attrHooks !== null) attrHooks.transitionSettled(u);
|
|
2464
|
+
const frame = reruns(u);
|
|
2465
|
+
if (u._marks !== null) releaseMarksHook(u._marks);
|
|
2466
|
+
// Old children die in the commits (cleanups first), then the stashed
|
|
2467
|
+
// effects run ahead of this flush's own. A node a lane took over since
|
|
2468
|
+
// (a guess written over the staged truth) is the lane's to land.
|
|
2469
|
+
for (let i = 0; i < u._nodes.length; i++) {
|
|
2470
|
+
const n = u._nodes[i];
|
|
2471
|
+
if (n._x._transaction === null || txOf(n) !== u) continue;
|
|
2472
|
+
n._x._transaction = null;
|
|
2473
|
+
commitPendingNode(n);
|
|
2496
2474
|
}
|
|
2497
|
-
|
|
2498
|
-
//
|
|
2499
|
-
//
|
|
2500
|
-
if (
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2475
|
+
// A cleanup those commits ran that wrote a node `u` still held joined
|
|
2476
|
+
// `u`, which has left `transactions`: the write is this landing's, and
|
|
2477
|
+
// the next flush must not park in it (#4089).
|
|
2478
|
+
if (flushTransaction === u) flushTransaction = null;
|
|
2479
|
+
// An effect re-derives next round (`reruns`): the held runs wait for it
|
|
2480
|
+
// (D-9, the A15 reveal corollary, #4071). Its re-derivation can discover a
|
|
2481
|
+
// flight — a stale reader whose mainline pass stopped reading it, now
|
|
2482
|
+
// reading the landed world — and a landing that showed its runs first
|
|
2483
|
+
// would reveal the held write beside a frame that can only show its old
|
|
2484
|
+
// value. Queued last in this effect phase, the release hands them to the
|
|
2485
|
+
// next round's own runs, ahead of the re-derivation's: they show with it,
|
|
2486
|
+
// or park with it in the hold its pending opens. The commits stand.
|
|
2487
|
+
if (frame) globalQueue._queues[1].push(() => releaseQueues(u));
|
|
2488
|
+
else releaseQueues(u);
|
|
2489
|
+
}
|
|
2490
|
+
/** The held runs join this flush's effect phase after its own runs (the
|
|
2491
|
+
* landing's re-passes, in height order): the frame's effects run as one
|
|
2492
|
+
* pass would have queued them — a shell re-derived at the landing before a
|
|
2493
|
+
* boundary swap held since the write (#3540's log order). */
|
|
2494
|
+
function releaseQueues(u) {
|
|
2495
|
+
for (let i = 0; i < 2; i++) {
|
|
2496
|
+
append(globalQueue._queues[i], u._queues[i]);
|
|
2497
|
+
u._queues[i] = [];
|
|
2506
2498
|
}
|
|
2507
2499
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
node._nextSibling = head;
|
|
2512
|
-
if (head !== null) head._prevSibling = node;
|
|
2513
|
-
parent._firstChild = node;
|
|
2500
|
+
/** `a.push(...b)` without the argument-count limit. */
|
|
2501
|
+
function append(a, b) {
|
|
2502
|
+
for (let i = 0; i < b.length; i++) a.push(b[i]);
|
|
2514
2503
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
enterDisposal();
|
|
2530
|
-
if (Array.isArray(disposal)) {
|
|
2531
|
-
// Unwind order (#3572, restores 1.x #1562): later registrations run
|
|
2532
|
-
// before earlier ones. Children have already been disposed by the caller,
|
|
2533
|
-
// so with LIFO a body that registers cleanup before creating its children
|
|
2534
|
-
// tears down after them — the same order a per-component owner gives.
|
|
2535
|
-
for (let i = disposal.length - 1; i >= 0; i--) {
|
|
2536
|
-
const callable = disposal[i];
|
|
2537
|
-
callable.call(callable);
|
|
2538
|
-
}
|
|
2539
|
-
} else {
|
|
2540
|
-
disposal.call(disposal);
|
|
2504
|
+
/** Nodes staged this flush (`_pendingValue` set), committed at its end. */
|
|
2505
|
+
const pendingNodes = [];
|
|
2506
|
+
/** Passes that read a staging of this flush as the screen — lane work
|
|
2507
|
+
* (core.ts `stagedRead`), a verdict reader before the frame's verdict
|
|
2508
|
+
* (verdict.ts): if the frame parks they read a held write — re-derived next
|
|
2509
|
+
* round (lanes.ts; a lane's runs wait that round). */
|
|
2510
|
+
const stagedReaders = [];
|
|
2511
|
+
/** A15's stale reader: `c` was served the committed value of a node `t`
|
|
2512
|
+
* holds (a render effect, a verdict reader, lane work) and is re-derived
|
|
2513
|
+
* after `t`'s landing (`_reruns`). Once per pass. */
|
|
2514
|
+
function staleReader(c, t) {
|
|
2515
|
+
if (!(c._flags & REACTIVE_FRAME_READ)) {
|
|
2516
|
+
c._flags |= REACTIVE_FRAME_READ;
|
|
2517
|
+
t._reruns.push(c);
|
|
2541
2518
|
}
|
|
2542
|
-
exitDisposal();
|
|
2543
2519
|
}
|
|
2544
|
-
function
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
if (
|
|
2548
|
-
return formatId(counter.id, consume ? counter._childCount++ : counter._childCount);
|
|
2549
|
-
throw new Error("Cannot get child id from owner without an id");
|
|
2520
|
+
function queuePendingNode(node) {
|
|
2521
|
+
lastStagedNodeName = node._name ?? null;
|
|
2522
|
+
pendingNodes.push(node);
|
|
2523
|
+
if (!globalQueue._running) markUnflushedStaged(); // A28
|
|
2550
2524
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2525
|
+
// Dev-only attribution for the flush loop guard (#3140): when the guard
|
|
2526
|
+
// trips, naming what the loop kept chewing on lets the app author attribute
|
|
2527
|
+
// the runaway without patching dist.
|
|
2528
|
+
let lastStagedNodeName = null;
|
|
2529
|
+
/** §12d: bumped by every recompute and every new subscriber edge. A node's
|
|
2530
|
+
* staged-rewrite skip is sound only while NOTHING recomputed or linked since
|
|
2531
|
+
* its last notify — a mid-batch pull can clean a marked subscriber, and a
|
|
2532
|
+
* skipped re-write would leave it stale. */
|
|
2533
|
+
let notifyEpoch = 0;
|
|
2534
|
+
function bumpNotifyEpoch() {
|
|
2535
|
+
notifyEpoch++;
|
|
2559
2536
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2537
|
+
function insertSubs(node) {
|
|
2538
|
+
// §12d: stamp before walking — setSignal's staged-rewrite fast path skips
|
|
2539
|
+
// the next walk for this node while the epoch holds (marking is idempotent).
|
|
2540
|
+
node._notifiedAt = notifyEpoch;
|
|
2541
|
+
// Presence bits gate the optional-slot probes (see constants.ts): one
|
|
2542
|
+
// masked read of the always-present _config instead of missing-property
|
|
2543
|
+
// lookups in the hottest notify loop.
|
|
2544
|
+
const cfg = node._config;
|
|
2545
|
+
const hasSnapshot = (cfg & CONFIG_HAS_SNAPSHOT) !== 0 && node._x?._snapshotValue !== undefined;
|
|
2546
|
+
// Observe-tier fan-out: this walk visits every subscriber edge anyway, so
|
|
2547
|
+
// the graph-size count is one local increment here and no field anywhere.
|
|
2548
|
+
let fanOut = 0;
|
|
2549
|
+
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
2550
|
+
const sub = s._sub;
|
|
2551
|
+
fanOut++;
|
|
2552
|
+
// Missed-wake latch (#3037): this write is landing while the subscriber
|
|
2553
|
+
// is mid-recompute (a nested pull committing beneath its reads), and the
|
|
2554
|
+
// heap refuses RECOMPUTING nodes. A gen-current link means the pass
|
|
2555
|
+
// already validated this dep — the value it read is now stale — so latch
|
|
2556
|
+
// for recompute's tail to reschedule. Untouched links need no latch (the
|
|
2557
|
+
// pass either re-reads them fresh or trims them), and neither does the
|
|
2558
|
+
// tail link: it is the read IN FLIGHT — read() links before it pulls, so
|
|
2559
|
+
// this very commit is what that read returns.
|
|
2560
|
+
if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
|
|
2561
|
+
sub._flags |= REACTIVE_MISSED_WAKE;
|
|
2562
|
+
if (hasSnapshot && sub._config & CONFIG_IN_SNAPSHOT_SCOPE) {
|
|
2563
|
+
sub._flags |= REACTIVE_SNAPSHOT_STALE;
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
enqueueSub(sub);
|
|
2567
|
+
}
|
|
2568
|
+
if (fanOut >= GRAPH_SIZE_WARN_AT) noteFanOut(node, fanOut);
|
|
2570
2569
|
}
|
|
2571
|
-
/**
|
|
2572
|
-
*
|
|
2573
|
-
*
|
|
2574
|
-
*
|
|
2575
|
-
*
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2570
|
+
/** Lane work re-staged (lanes.ts; `recompute`): the lane's members among
|
|
2571
|
+
* its subscribers re-derive as the lane's work (REACTIVE_LANE_DIRTY), their
|
|
2572
|
+
* runs held with it — not as stale readers republishing the committed view
|
|
2573
|
+
* (#3460). After `insertSubs`. A render effect on no lane is marked too:
|
|
2574
|
+
* inert for its pass (`recompute` seats a lane only from its own `_x`), it
|
|
2575
|
+
* is a live write if the effect becomes a zombie later this flush — an
|
|
2576
|
+
* on-screen reader whose removal an adopted write stages follows the lane
|
|
2577
|
+
* (the lane-membership ruling, 2026-10-05; `_update`). */
|
|
2578
|
+
function laneDirty(node, l) {
|
|
2579
|
+
for (let s = node._subs; s !== null; s = s._nextSub) {
|
|
2580
|
+
const t = s._sub._x?._transaction;
|
|
2581
|
+
if (t?._lane ? sameLane(t, l) : s._sub._type) s._sub._flags |= REACTIVE_LANE_DIRTY;
|
|
2582
|
+
}
|
|
2579
2583
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
|
|
2584
|
+
/** One lane, or two of one link group (#3335): one reveal unit. */
|
|
2585
|
+
function sameLane(a, b) {
|
|
2586
|
+
return a === b || (a._links !== null && a._links === b._links);
|
|
2584
2587
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2588
|
+
function commitPendingNode(n) {
|
|
2589
|
+
const c = n;
|
|
2590
|
+
// L2: the commit is where a pass, held or not, stops being uncommitted —
|
|
2591
|
+
// unless it has no answer yet. A pass still in flight (pending, nothing
|
|
2592
|
+
// staged) stays uncommitted: a re-pass disposes its children on the spot,
|
|
2593
|
+
// and the frame it parked stays the frame until a pass of this node
|
|
2594
|
+
// commits a value.
|
|
2595
|
+
if (!c._fn) {
|
|
2596
|
+
n._config &= -385;
|
|
2597
|
+
if (n._pendingValue !== NOT_PENDING) {
|
|
2598
|
+
n._value = n._pendingValue;
|
|
2599
|
+
n._pendingValue = NOT_PENDING;
|
|
2600
|
+
}
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
// Computeds only from here (`_statusFlags` is not a signal field). A
|
|
2604
|
+
// commit beneath a flight publishes its inputs (A15 reveal corollary,
|
|
2605
|
+
// #3305): a reveal of it must observe, not show the pre-flight value.
|
|
2606
|
+
const inFlight = n._pendingValue === NOT_PENDING && (c._statusFlags & STATUS_PENDING) !== 0;
|
|
2607
|
+
const err = n._config & CONFIG_STAGED_ERROR;
|
|
2608
|
+
// An answer commits: the error it replaces leaves the display (D-41).
|
|
2609
|
+
if (!inFlight && c._x) c._x._shownError = undefined;
|
|
2610
|
+
n._config = inFlight ? (n._config & -1048833) | CONFIG_INPUTS_PUBLISHED : n._config & -3147137;
|
|
2611
|
+
// D-41: the staged answer is an error — published now, when a value would
|
|
2612
|
+
// show, through the ordinary error path (`stageError`). Its readers first
|
|
2613
|
+
// see it in the cycle after this seam: a tracked re-read there is not a
|
|
2614
|
+
// retry (`read`), or a boundary re-derived by it would retry it forever.
|
|
2615
|
+
if (err) {
|
|
2616
|
+
const e = n._pendingValue;
|
|
2617
|
+
n._pendingValue = NOT_PENDING;
|
|
2618
|
+
n._time = clock + 1;
|
|
2619
|
+
notifyStatus(c, STATUS_ERROR, e);
|
|
2620
|
+
} else if (n._pendingValue !== NOT_PENDING) {
|
|
2621
|
+
n._value = n._pendingValue;
|
|
2622
|
+
n._pendingValue = NOT_PENDING;
|
|
2623
|
+
// A node born staged (recompute) initializes at this commit.
|
|
2624
|
+
c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
2625
|
+
// A quiet re-ask's classification survives its landing and dies with
|
|
2626
|
+
// the landed value's commit (A19 exc. 2, #3178): verdict-quiet through
|
|
2627
|
+
// the reveal. (The sweep of a node still in flight is not a landing.)
|
|
2628
|
+
// The A28 stash goes with it.
|
|
2629
|
+
if (c._x != null) ((c._x._reask = false), (c._x._flushed = NOT_PENDING));
|
|
2630
|
+
// An effect with a staged value was born held (L2): its first run is
|
|
2631
|
+
// this commit's, not its creation's (A29) — queue it now.
|
|
2632
|
+
if (n._type && n._type !== EFFECT_TRACKED) {
|
|
2633
|
+
n._modified = true;
|
|
2634
|
+
globalQueue.enqueue(n._type, (n._boundRunEffect ??= GlobalQueue._runEffect.bind(null, c)));
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
// The committed value is the first observable answer for a loading-window
|
|
2638
|
+
// node — the window closes here, not at compute time (#2990). Unconditional
|
|
2639
|
+
// store to an always-present computed slot.
|
|
2640
|
+
c._loading = false;
|
|
2641
|
+
c._flags &= ~REACTIVE_MANUAL_WRITE;
|
|
2642
|
+
// The dependencies of the pass that produced the value are the frame's now:
|
|
2643
|
+
// the previous frame's tail goes (A30, #3410; `recompute` left it for a
|
|
2644
|
+
// staged pass). Only after a clean pass: `_error` is cleared by a clean
|
|
2645
|
+
// pass or by the node's own landing (whose pass was clean), so a set
|
|
2646
|
+
// `_error` means the last pass threw, kept its full list, and `_depsTail`
|
|
2647
|
+
// marks where it stopped.
|
|
2648
|
+
if (c._x?._error == null) trimStaleDeps(c);
|
|
2649
|
+
if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
2650
|
+
// L2: the children this commit publishes are the frame's now — the frame
|
|
2651
|
+
// they replace, parked by the pass (`recompute`), goes.
|
|
2652
|
+
if (
|
|
2653
|
+
!inFlight &&
|
|
2654
|
+
c._x != null &&
|
|
2655
|
+
(c._x._pendingFirstChild !== null || c._x._pendingDisposal !== null)
|
|
2656
|
+
)
|
|
2657
|
+
GlobalQueue._dispose(c, false, true);
|
|
2603
2658
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
* @example
|
|
2614
|
-
* ```ts
|
|
2615
|
-
* function defer<T>(fn: () => T) {
|
|
2616
|
-
* const owner = getOwner();
|
|
2617
|
-
* queueMicrotask(() => runWithOwner(owner, fn));
|
|
2618
|
-
* }
|
|
2619
|
-
* ```
|
|
2620
|
-
*/
|
|
2621
|
-
function getOwner() {
|
|
2622
|
-
return context;
|
|
2659
|
+
function commitPendingNodes() {
|
|
2660
|
+
for (let i = 0; i < pendingNodes.length; i++) {
|
|
2661
|
+
const n = pendingNodes[i];
|
|
2662
|
+
// A node a lane pass re-staged this round is the lane's to reveal; one
|
|
2663
|
+
// a transaction holds (born held by a mainline pass, `passTx`) is that
|
|
2664
|
+
// transaction's to commit, at its landing.
|
|
2665
|
+
if (!(n._config & (CONFIG_OVERRIDE | CONFIG_HELD))) commitPendingNode(n);
|
|
2666
|
+
}
|
|
2667
|
+
pendingNodes.length = 0;
|
|
2623
2668
|
}
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2669
|
+
const globalQueue = new GlobalQueue();
|
|
2670
|
+
function flush(fn) {
|
|
2671
|
+
if (actionDepth > 0) {
|
|
2672
|
+
{
|
|
2673
|
+
throw new Error(
|
|
2674
|
+
"[FLUSH_IN_ACTION] flush() inside an action body is not allowed. An action's writes are held in its " +
|
|
2675
|
+
"transaction and commit when the action settles: flush() cannot reveal them, and draining here would " +
|
|
2676
|
+
"detach the writes that follow from the transaction. Remove the flush(); to observe the result, read " +
|
|
2677
|
+
"after the action resolves."
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (fn) {
|
|
2682
|
+
syncDepth++;
|
|
2683
|
+
try {
|
|
2684
|
+
return fn();
|
|
2685
|
+
} finally {
|
|
2686
|
+
// Decrement even if the drain throws (a throwing effect): a leaked
|
|
2687
|
+
// syncDepth would stop `schedule()` from ever queuing a microtask again.
|
|
2688
|
+
try {
|
|
2689
|
+
flush();
|
|
2690
|
+
} finally {
|
|
2691
|
+
syncDepth--;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
if (globalQueue._running) {
|
|
2696
|
+
if (inTrackedQueueCallback) {
|
|
2697
|
+
throw new Error(
|
|
2698
|
+
"Cannot call flush() from inside onSettled or createTrackedEffect. flush() is not reentrant there. " +
|
|
2699
|
+
"Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush())."
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2702
|
+
if (inEffectCallback) {
|
|
2703
|
+
const message =
|
|
2704
|
+
"[FLUSH_IN_EFFECT_CALLBACK] flush() called from inside an effect callback is a no-op: the flush that runs effects is already in progress. " +
|
|
2705
|
+
"Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush()).";
|
|
2706
|
+
reportDiagnostic(
|
|
2707
|
+
emitDiagnostic({
|
|
2708
|
+
code: "FLUSH_IN_EFFECT_CALLBACK",
|
|
2709
|
+
kind: "lifecycle",
|
|
2710
|
+
severity: "warn",
|
|
2711
|
+
message
|
|
2712
|
+
})
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
if (halted) return;
|
|
2718
|
+
let count = 0;
|
|
2719
|
+
// Attribution: whether this call drained anything, so `flushEnd` fires once
|
|
2720
|
+
// per real drain and never for a no-op call. The declaration is dead in
|
|
2721
|
+
// prod (its only write is behind true) and rollup drops it.
|
|
2722
|
+
let drained = false;
|
|
2723
|
+
// The drain's opening instant, under the loop's own condition so it fires
|
|
2724
|
+
// exactly when `flushEnd` below will. Outside every try (see the rule in
|
|
2725
|
+
// attribution-hooks.ts).
|
|
2726
|
+
if (attrHooks !== null && scheduled) attrHooks.flushStart();
|
|
2727
|
+
while (scheduled) {
|
|
2728
|
+
if (++count === 1e5) {
|
|
2729
|
+
rethrowCleanupError(true);
|
|
2730
|
+
// Attribution beats a bare guard (#3140): say what kept the loop alive.
|
|
2731
|
+
throw new Error(
|
|
2732
|
+
`Potential Infinite Loop Detected. Kept alive by scheduled work${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
globalQueue.flush();
|
|
2736
|
+
drained = true;
|
|
2737
|
+
}
|
|
2738
|
+
// Outside every try in this function (see the rule in attribution-hooks.ts):
|
|
2739
|
+
// the drain loop above is the one place all scheduled work funnels through,
|
|
2740
|
+
// so this is the "committed and effects ran" instant for everything the
|
|
2741
|
+
// loop processed.
|
|
2742
|
+
if (drained && attrHooks !== null) attrHooks.flushEnd();
|
|
2743
|
+
// A cleanup that threw during the drain, now that the drain is complete
|
|
2744
|
+
// (owner.ts).
|
|
2745
|
+
rethrowCleanupError();
|
|
2746
|
+
}
|
|
2747
|
+
/** Dev-only: the DOM event current when the running effect queue started,
|
|
2748
|
+
* or `ANY_EVENT` outside one. Read once per queue rather than once per
|
|
2749
|
+
* effect (the getter is costly under jsdom); no event starts or ends between
|
|
2750
|
+
* two effects of one synchronous queue (#4235). */
|
|
2751
|
+
let queueEvent = ANY_EVENT;
|
|
2752
|
+
function runQueue(queue, type) {
|
|
2753
|
+
{
|
|
2754
|
+
const prev = queueEvent;
|
|
2755
|
+
queueEvent = dispatchingEvent();
|
|
2756
|
+
try {
|
|
2757
|
+
for (let i = 0; i < queue.length; i++) queue[i](type);
|
|
2758
|
+
} finally {
|
|
2759
|
+
queueEvent = prev;
|
|
2760
|
+
}
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2637
2763
|
}
|
|
2764
|
+
|
|
2638
2765
|
/**
|
|
2639
|
-
*
|
|
2640
|
-
*
|
|
2641
|
-
*
|
|
2642
|
-
*
|
|
2643
|
-
*
|
|
2644
|
-
* ```ts
|
|
2645
|
-
* function onSettleSafe(fn: () => void) {
|
|
2646
|
-
* const owner = getOwner();
|
|
2647
|
-
* queueMicrotask(() => {
|
|
2648
|
-
* if (owner && isDisposed(owner)) return; // component unmounted; skip
|
|
2649
|
-
* runWithOwner(owner, fn);
|
|
2650
|
-
* });
|
|
2651
|
-
* }
|
|
2652
|
-
* ```
|
|
2766
|
+
* Schedule one subscriber to re-run on the next flush: inserted into the
|
|
2767
|
+
* dirty heap with the `_min` cursor pulled down. Tracked effects ride the
|
|
2768
|
+
* heap too — the heap visit is their (empty) compute phase, which hands the
|
|
2769
|
+
* callback to the user queue once the pass has committed (see
|
|
2770
|
+
* GlobalQueue._update, #3291).
|
|
2653
2771
|
*/
|
|
2654
|
-
function
|
|
2655
|
-
|
|
2772
|
+
function enqueueSub(node) {
|
|
2773
|
+
if (dirtyQueue._min > node._height) dirtyQueue._min = node._height;
|
|
2774
|
+
insertIntoHeap(node, dirtyQueue);
|
|
2656
2775
|
}
|
|
2657
|
-
function
|
|
2658
|
-
|
|
2776
|
+
function actualInsertIntoHeap(n, heap) {
|
|
2777
|
+
const parentHeight =
|
|
2778
|
+
(n._parent?._root ? n._parent._parentComputed?._height : n._parent?._height) ?? -1;
|
|
2779
|
+
if (parentHeight >= n._height) n._height = parentHeight + 1;
|
|
2780
|
+
const height = n._height;
|
|
2781
|
+
const heapAtHeight = heap._heap[height];
|
|
2782
|
+
if (heapAtHeight === undefined) heap._heap[height] = n;
|
|
2783
|
+
else {
|
|
2784
|
+
const tail = heapAtHeight._prevHeap;
|
|
2785
|
+
tail._nextHeap = n;
|
|
2786
|
+
n._prevHeap = tail;
|
|
2787
|
+
heapAtHeight._prevHeap = n;
|
|
2788
|
+
}
|
|
2789
|
+
if (height > heap._max) heap._max = height;
|
|
2659
2790
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
//
|
|
2680
|
-
|
|
2681
|
-
// properties-store allocation per root — on a 100×4 table teardown that
|
|
2682
|
-
// was 12 KB and most of disposal's time (2026-10-17).
|
|
2683
|
-
_flags: REACTIVE_NONE,
|
|
2684
|
-
_root: true,
|
|
2685
|
-
_parentComputed: parent?._root ? parent._parentComputed : parent,
|
|
2686
|
-
_firstChild: null,
|
|
2687
|
-
_nextSibling: null,
|
|
2688
|
-
_prevSibling: null,
|
|
2689
|
-
_disposal: null,
|
|
2690
|
-
_context: parent?._context || defaultContext,
|
|
2691
|
-
_childCount: 0,
|
|
2692
|
-
_x: null,
|
|
2693
|
-
_parent: parent,
|
|
2694
|
-
dispose: disposeRootSelf,
|
|
2695
|
-
_name: undefined,
|
|
2696
|
-
_loc: undefined
|
|
2697
|
-
};
|
|
2698
|
-
if (parent && parent._config & CONFIG_CHILDREN_FORBIDDEN) {
|
|
2699
|
-
emitDiagnostic({
|
|
2700
|
-
code: "PRIMITIVE_IN_FORBIDDEN_SCOPE",
|
|
2701
|
-
kind: "lifecycle",
|
|
2702
|
-
severity: "error",
|
|
2703
|
-
message: PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE,
|
|
2704
|
-
ownerId: parent.id,
|
|
2705
|
-
ownerName: parent._name
|
|
2706
|
-
});
|
|
2707
|
-
throw new Error(PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE);
|
|
2791
|
+
function insertIntoHeap(n, heap) {
|
|
2792
|
+
let flags = n._flags;
|
|
2793
|
+
// RECOMPUTING refusals are not always losses: a genuinely missed wake (a
|
|
2794
|
+
// write to a link this pass already validated) is latched link-side in
|
|
2795
|
+
// insertSubs as REACTIVE_MISSED_WAKE for recompute's tail (#3037).
|
|
2796
|
+
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS)) return;
|
|
2797
|
+
if (flags & REACTIVE_CHECK) {
|
|
2798
|
+
n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
2799
|
+
} else {
|
|
2800
|
+
n._flags = flags | REACTIVE_IN_HEAP;
|
|
2801
|
+
// An unmarked node entering an already-marked heap is marked on the
|
|
2802
|
+
// spot, keeping the markHeap memo valid. `_marked` is only reset by
|
|
2803
|
+
// runHeap, so a write between two mid-tick pulls (read-time markHeap +
|
|
2804
|
+
// updateIfNecessary) would otherwise leave this node unmarked and every
|
|
2805
|
+
// downstream pull stale until the next flush (#2922: the second
|
|
2806
|
+
// `latest()` returned the first write's value). Invalidating the memo
|
|
2807
|
+
// instead re-walked the WHOLE heap on the next pull — with N effects
|
|
2808
|
+
// parked in the heap for a synchronous mount (each row writing a ref
|
|
2809
|
+
// signal its effect subscribes to), mounting N rows was O(N²) (#3350).
|
|
2810
|
+
// markNode's own guard skips an already-DIRTY node.
|
|
2811
|
+
if (heap._marked) markNode(n);
|
|
2708
2812
|
}
|
|
2709
|
-
|
|
2710
|
-
if (parent) linkChild(parent, owner);
|
|
2711
|
-
else registerRoot(owner);
|
|
2712
|
-
DEV.hooks.onOwner?.(owner);
|
|
2713
|
-
return owner;
|
|
2813
|
+
if (!(flags & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(n, heap);
|
|
2714
2814
|
}
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2815
|
+
function insertIntoHeapHeight(n, heap) {
|
|
2816
|
+
let flags = n._flags;
|
|
2817
|
+
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_IN_HEAP_HEIGHT)) return;
|
|
2818
|
+
n._flags = flags | REACTIVE_IN_HEAP_HEIGHT;
|
|
2819
|
+
actualInsertIntoHeap(n, heap);
|
|
2820
|
+
}
|
|
2821
|
+
function deleteFromHeap(n, heap) {
|
|
2822
|
+
const flags = n._flags;
|
|
2823
|
+
if (!(flags & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT))) return;
|
|
2824
|
+
n._flags = flags & -25;
|
|
2825
|
+
const height = n._height;
|
|
2826
|
+
if (n._prevHeap === n) heap._heap[height] = undefined;
|
|
2827
|
+
else {
|
|
2828
|
+
const next = n._nextHeap;
|
|
2829
|
+
const dhh = heap._heap[height];
|
|
2830
|
+
const end = next ?? dhh;
|
|
2831
|
+
if (n === dhh) heap._heap[height] = next;
|
|
2832
|
+
else n._prevHeap._nextHeap = next;
|
|
2833
|
+
end._prevHeap = n._prevHeap;
|
|
2834
|
+
}
|
|
2835
|
+
n._prevHeap = n;
|
|
2836
|
+
n._nextHeap = undefined;
|
|
2837
|
+
}
|
|
2838
|
+
function markHeap(heap) {
|
|
2839
|
+
if (heap._marked) return;
|
|
2840
|
+
heap._marked = true;
|
|
2841
|
+
for (let i = 0; i <= heap._max; i++) {
|
|
2842
|
+
for (let el = heap._heap[i]; el !== undefined; el = el._nextHeap) {
|
|
2843
|
+
if (el._flags & REACTIVE_IN_HEAP) markNode(el);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
function markNode(el, newState = REACTIVE_DIRTY) {
|
|
2848
|
+
const flags = el._flags;
|
|
2849
|
+
if ((flags & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= newState) return;
|
|
2850
|
+
el._flags = (flags & -4) | newState;
|
|
2851
|
+
for (let link = el._subs; link !== null; link = link._nextSub) {
|
|
2852
|
+
markNode(link._sub, REACTIVE_CHECK);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
function runHeap(heap, recompute) {
|
|
2856
|
+
heap._marked = false;
|
|
2857
|
+
for (heap._min = 0; heap._min <= heap._max; heap._min++) {
|
|
2858
|
+
let el = heap._heap[heap._min];
|
|
2859
|
+
while (el !== undefined) {
|
|
2860
|
+
if (el._flags & REACTIVE_IN_HEAP) recompute(el);
|
|
2861
|
+
else adjustHeight(el, heap);
|
|
2862
|
+
el = heap._heap[heap._min];
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
heap._max = 0;
|
|
2866
|
+
}
|
|
2867
|
+
function adjustHeight(el, heap) {
|
|
2868
|
+
deleteFromHeap(el, heap);
|
|
2869
|
+
let newHeight = el._height;
|
|
2870
|
+
for (let d = el._deps; d; d = d._nextDep) {
|
|
2871
|
+
const dep = d._dep;
|
|
2872
|
+
if (dep._fn && dep._height >= newHeight) newHeight = dep._height + 1;
|
|
2873
|
+
}
|
|
2874
|
+
if (el._height !== newHeight) {
|
|
2875
|
+
el._height = newHeight;
|
|
2876
|
+
for (let s = el._subs; s !== null; s = s._nextSub) insertIntoHeapHeight(s._sub, heap);
|
|
2877
|
+
}
|
|
2756
2878
|
}
|
|
2757
2879
|
|
|
2758
2880
|
/** The store's shared slot-node release handler (store/store.ts installs it
|
|
@@ -2777,8 +2899,10 @@ function unlinkSubs(link) {
|
|
|
2777
2899
|
if (nextSub === null) {
|
|
2778
2900
|
// Store slot nodes dispatch to the ONE shared hook — no per-node
|
|
2779
2901
|
// unobserved closure, no NodeExtension to hold it.
|
|
2902
|
+
// A user hook that throws is collected like a cleanup (owner.ts): the
|
|
2903
|
+
// unlink and the teardown around it carry on.
|
|
2780
2904
|
if (dep._config & CONFIG_SLOT_NODE) slotUnobservedHook(dep);
|
|
2781
|
-
else dep._x?._unobserved
|
|
2905
|
+
else if (dep._x?._unobserved) runCleanup(dep._x._unobserved);
|
|
2782
2906
|
// No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
|
|
2783
2907
|
// A pending node is exempt: its in-flight async work is an observer —
|
|
2784
2908
|
// tearing down would orphan the work and re-execute it on the next
|
|
@@ -2857,7 +2981,7 @@ function unobserved(el) {
|
|
|
2857
2981
|
* reclamation still happens within one microtask — the enqueue site arms
|
|
2858
2982
|
* schedule(), so a flush is guaranteed even when no other work is queued.
|
|
2859
2983
|
*/
|
|
2860
|
-
const dormantNodes = new
|
|
2984
|
+
const dormantNodes = new NewSet();
|
|
2861
2985
|
function sweepDormant() {
|
|
2862
2986
|
if (dormantNodes.size === 0) return;
|
|
2863
2987
|
for (const el of dormantNodes) {
|
|
@@ -2925,7 +3049,7 @@ function link(dep, sub) {
|
|
|
2925
3049
|
// to clear it, stranding the Set members' pending forever (#2893).
|
|
2926
3050
|
function addPendingSource(el, source) {
|
|
2927
3051
|
if (el._x?._pendingSources?.has(source)) return false;
|
|
2928
|
-
(ext(el)._pendingSources ??= new
|
|
3052
|
+
(ext(el)._pendingSources ??= new NewSet()).add(source);
|
|
2929
3053
|
return true;
|
|
2930
3054
|
}
|
|
2931
3055
|
function removePendingSource(el, source) {
|
|
@@ -3008,7 +3132,7 @@ function releaseIfSettledUnobserved(node) {
|
|
|
3008
3132
|
// Collect-then-release so unobserved() never unlinks under the walk.
|
|
3009
3133
|
function releaseSettledDependents(el) {
|
|
3010
3134
|
let candidates;
|
|
3011
|
-
const visited = new
|
|
3135
|
+
const visited = new NewSet();
|
|
3012
3136
|
const visit = node => {
|
|
3013
3137
|
if (visited.has(node)) return;
|
|
3014
3138
|
visited.add(node);
|
|
@@ -3048,10 +3172,12 @@ function stageError(el, error, inPass) {
|
|
|
3048
3172
|
// Staged whether or not the flush has joined a hold yet: a pass that runs
|
|
3049
3173
|
// before its async sibling joins is the seam's to decide, like a value
|
|
3050
3174
|
// (#4109). A flush that parks nothing publishes it at its commit (S-38).
|
|
3175
|
+
// Not a projection's derive: its readers learn its error through the
|
|
3176
|
+
// family, which the commit's publish never reaches (#4189).
|
|
3051
3177
|
if (
|
|
3052
3178
|
!(
|
|
3053
3179
|
el._config & CONFIG_HELD ||
|
|
3054
|
-
(inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running)
|
|
3180
|
+
(inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running && !el.fam)
|
|
3055
3181
|
) ||
|
|
3056
3182
|
passLane
|
|
3057
3183
|
)
|
|
@@ -3072,7 +3198,7 @@ function stageError(el, error, inPass) {
|
|
|
3072
3198
|
}
|
|
3073
3199
|
function settleErroredDependents(el, error) {
|
|
3074
3200
|
let scheduled = false;
|
|
3075
|
-
const visited = new
|
|
3201
|
+
const visited = new NewSet();
|
|
3076
3202
|
const visit = node => {
|
|
3077
3203
|
if (visited.has(node)) return;
|
|
3078
3204
|
visited.add(node);
|
|
@@ -3149,7 +3275,7 @@ function settlePendingSource(el, source = el) {
|
|
|
3149
3275
|
removePendingSource(el, source);
|
|
3150
3276
|
let scheduled = false;
|
|
3151
3277
|
let released;
|
|
3152
|
-
const visited = new
|
|
3278
|
+
const visited = new NewSet();
|
|
3153
3279
|
const settle = node => {
|
|
3154
3280
|
if (
|
|
3155
3281
|
visited.has(node) ||
|
|
@@ -3216,6 +3342,37 @@ function releaseFlightTeardown(el) {
|
|
|
3216
3342
|
teardown();
|
|
3217
3343
|
}
|
|
3218
3344
|
}
|
|
3345
|
+
/**
|
|
3346
|
+
* Internal (not a package export): run `fn` when the current pass's flight
|
|
3347
|
+
* is superseded, at the same instant as the iterator close above, instead of
|
|
3348
|
+
* when the replacing pass commits (owner cleanups wait for that). Chains onto
|
|
3349
|
+
* the node's teardown slot; fires once with it. A no-op unless the ambient
|
|
3350
|
+
* owner is a computed in its own pass. `consumeIterator` fills the slot
|
|
3351
|
+
* before its first pull, so a source opened in an async generator's body
|
|
3352
|
+
* (`for await`) composes after the generator's close.
|
|
3353
|
+
*/
|
|
3354
|
+
function onSupersede(fn) {
|
|
3355
|
+
const el = context;
|
|
3356
|
+
if (el === null || el._fn === undefined || !(el._flags & REACTIVE_RECOMPUTING_DEPS)) return;
|
|
3357
|
+
const fresh = el._x === null;
|
|
3358
|
+
const x = ext(el);
|
|
3359
|
+
// Allocating the extension mid-run turns `_x?._inFlight` from undefined into
|
|
3360
|
+
// null, which recompute would read as a flight this body registered itself.
|
|
3361
|
+
// Restore undefined here, so the fix costs nothing for code that never
|
|
3362
|
+
// calls onSupersede.
|
|
3363
|
+
if (fresh) x._inFlight = undefined;
|
|
3364
|
+
const prev = x._flightTeardown;
|
|
3365
|
+
x._flightTeardown =
|
|
3366
|
+
prev === null
|
|
3367
|
+
? fn
|
|
3368
|
+
: () => {
|
|
3369
|
+
try {
|
|
3370
|
+
prev();
|
|
3371
|
+
} finally {
|
|
3372
|
+
fn();
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3219
3376
|
function handleAsync(
|
|
3220
3377
|
el,
|
|
3221
3378
|
result,
|
|
@@ -3319,8 +3476,14 @@ function handleAsync(
|
|
|
3319
3476
|
// D-41: a held node's error is staged (`stageError`). A derived store's
|
|
3320
3477
|
// readers learn its error through the family, not the derive's
|
|
3321
3478
|
// dependents: it publishes now.
|
|
3322
|
-
if (stillPending || setter || !stageError(el, error))
|
|
3479
|
+
if (stillPending || setter || !stageError(el, error)) {
|
|
3323
3480
|
notifyStatus(el, stillPending ? STATUS_PENDING : STATUS_ERROR, error);
|
|
3481
|
+
// The error is this pass's answer, published now: it replaces the shown
|
|
3482
|
+
// frame as a value's commit would (`commitPendingNode`), so the frame
|
|
3483
|
+
// the pass parked goes instead of waiting for a later success. A no-op
|
|
3484
|
+
// when nothing is parked.
|
|
3485
|
+
if (!stillPending) disposeChildren(el, false, true);
|
|
3486
|
+
}
|
|
3324
3487
|
// A NotReady rejection is a landing into another pending source. The
|
|
3325
3488
|
// rejected flight will never settle its self entry, so transfer ownership
|
|
3326
3489
|
// after notifyStatus has propagated the replacement source.
|
|
@@ -3435,7 +3598,11 @@ function handleAsync(
|
|
|
3435
3598
|
cleanup(close, owner);
|
|
3436
3599
|
// Flight-identity cancellation (#3122): the registration above is the
|
|
3437
3600
|
// owner-death backstop; the teardown slot fires at the _inFlight release
|
|
3438
|
-
// sites so supersede stops this stream immediately.
|
|
3601
|
+
// sites so supersede stops this stream immediately. Assigned, not
|
|
3602
|
+
// chained (chaining here costs every app bytes): it replaces any
|
|
3603
|
+
// `onSupersede` teardown this run registered earlier, which then falls
|
|
3604
|
+
// back to the owner-cleanup channel. A returned `from()` is unaffected:
|
|
3605
|
+
// `close` calls its `return()`, which runs its own teardown.
|
|
3439
3606
|
ext(el)._flightTeardown = close;
|
|
3440
3607
|
// Release check before each next pull: an unobserved lazy node must tear
|
|
3441
3608
|
// down (its close above runs via disposal, closing the iterator) instead
|
|
@@ -3598,12 +3765,12 @@ function handleAsync(
|
|
|
3598
3765
|
handleError(syncError);
|
|
3599
3766
|
throw syncError;
|
|
3600
3767
|
} else if (!resolved) {
|
|
3601
|
-
//
|
|
3602
|
-
// work on a loadingValue node is
|
|
3603
|
-
// boundaries
|
|
3604
|
-
// lands through asyncWrite.
|
|
3605
|
-
|
|
3606
|
-
|
|
3768
|
+
// Not landed: the shared tail below suspends, or — loading window —
|
|
3769
|
+
// serves commit #0 (first-flight work on a loadingValue node is
|
|
3770
|
+
// loading-class, invisible to boundaries; the flight itself is already
|
|
3771
|
+
// registered in _inFlight and lands through asyncWrite). A promise is
|
|
3772
|
+
// never an iterator, so nothing between here and there runs.
|
|
3773
|
+
liveLanded = false;
|
|
3607
3774
|
} else if (!flattenIfIterable(syncValue)) {
|
|
3608
3775
|
// Synchronously-resolved promise: the first real answer landed.
|
|
3609
3776
|
el._loading = false;
|
|
@@ -3615,7 +3782,7 @@ function handleAsync(
|
|
|
3615
3782
|
if (iterator) flattenIfIterable(result);
|
|
3616
3783
|
if (liveLanded !== null) {
|
|
3617
3784
|
if (!liveLanded) {
|
|
3618
|
-
// Loading window: serve commit #0
|
|
3785
|
+
// Loading window: serve commit #0 instead of suspending.
|
|
3619
3786
|
if (el._loading) return el._value;
|
|
3620
3787
|
throw new NotReadyError(context);
|
|
3621
3788
|
}
|
|
@@ -3627,7 +3794,7 @@ function handleAsync(
|
|
|
3627
3794
|
}
|
|
3628
3795
|
function clearStatus(el, clearUninitialized = false) {
|
|
3629
3796
|
if (el._x?._pendingSources) clearPendingSources(el);
|
|
3630
|
-
if (el._x?._blocked)
|
|
3797
|
+
if (el._x?._blocked) el._x._blocked = false;
|
|
3631
3798
|
el._statusFlags = clearUninitialized ? 0 : el._statusFlags & STATUS_UNINITIALIZED;
|
|
3632
3799
|
if (el._x?._error) setPendingError(el);
|
|
3633
3800
|
const notify = statusNotifierOf(el);
|
|
@@ -3667,7 +3834,7 @@ function landStatus(el, clearUninitialized = false) {
|
|
|
3667
3834
|
setPendingError(el, sources.values().next().value);
|
|
3668
3835
|
} else clearStatus(el, clearUninitialized);
|
|
3669
3836
|
}
|
|
3670
|
-
function notifyStatus(el, status, error
|
|
3837
|
+
function notifyStatus(el, status, error) {
|
|
3671
3838
|
// Wrap regular errors to track source node
|
|
3672
3839
|
if (
|
|
3673
3840
|
status === STATUS_ERROR &&
|
|
@@ -3678,21 +3845,19 @@ function notifyStatus(el, status, error, blockStatus) {
|
|
|
3678
3845
|
const pendingSource =
|
|
3679
3846
|
status === STATUS_PENDING && error instanceof NotReadyError ? error.source : undefined;
|
|
3680
3847
|
// (CARVE 2: the lane assignment went with the engine.)
|
|
3681
|
-
{
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
el._statusFlags
|
|
3693
|
-
|
|
3694
|
-
ext(el)._error = error;
|
|
3695
|
-
}
|
|
3848
|
+
if (status === STATUS_PENDING && pendingSource) {
|
|
3849
|
+
// D-41: a re-ask of a shown error keeps it the display (`_shownError`).
|
|
3850
|
+
if (el._statusFlags & STATUS_ERROR) el._x._shownError = el._x._error;
|
|
3851
|
+
addPendingSource(el, pendingSource);
|
|
3852
|
+
el._statusFlags = STATUS_PENDING | (el._statusFlags & STATUS_UNINITIALIZED);
|
|
3853
|
+
// Preserve the current source on this propagation so readers park on
|
|
3854
|
+
// the distinct pending source.
|
|
3855
|
+
setPendingError(el, pendingSource, error);
|
|
3856
|
+
} else {
|
|
3857
|
+
clearPendingSources(el);
|
|
3858
|
+
el._statusFlags =
|
|
3859
|
+
status | (status !== STATUS_ERROR ? el._statusFlags & STATUS_UNINITIALIZED : 0);
|
|
3860
|
+
ext(el)._error = error;
|
|
3696
3861
|
}
|
|
3697
3862
|
// Lanes: a written guess is an optimistic boundary (A17 — "the optimistic
|
|
3698
3863
|
// future value until we know otherwise"). Its own source in flight is its
|
|
@@ -3706,9 +3871,7 @@ function notifyStatus(el, status, error, blockStatus) {
|
|
|
3706
3871
|
}
|
|
3707
3872
|
const elNotify = statusNotifierOf(el);
|
|
3708
3873
|
if (elNotify) {
|
|
3709
|
-
|
|
3710
|
-
elNotify.call(el);
|
|
3711
|
-
}
|
|
3874
|
+
elNotify.call(el);
|
|
3712
3875
|
return;
|
|
3713
3876
|
}
|
|
3714
3877
|
propagateStatus(el, status, error);
|
|
@@ -3718,7 +3881,7 @@ function notifyStatus(el, status, error, blockStatus) {
|
|
|
3718
3881
|
* family's leaves, which are not the derive's dependents — the family
|
|
3719
3882
|
* propagates the derive's status from each leaf to the leaf's readers
|
|
3720
3883
|
* (store/projection.ts `wakeFamily`), exactly as a memo's would reach its. */
|
|
3721
|
-
function propagateStatus(el, status, error
|
|
3884
|
+
function propagateStatus(el, status, error) {
|
|
3722
3885
|
const pendingSource =
|
|
3723
3886
|
status === STATUS_PENDING && error instanceof NotReadyError ? error.source : undefined;
|
|
3724
3887
|
forEachDependent(el, (sub, link) => {
|
|
@@ -3775,23 +3938,21 @@ function propagateStatus(el, status, error, downstreamBlockStatus) {
|
|
|
3775
3938
|
// bookkeeping (loading window, uninitialized clear) runs with the flush
|
|
3776
3939
|
// — with the lane, when the pass propagating is the lane's work and
|
|
3777
3940
|
// the dependent's pending is the lane's own flight (`laneStage`).
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
)
|
|
3793
|
-
joinFuture(txOf(sub));
|
|
3794
|
-
}
|
|
3941
|
+
// (No answer: a pending propagation leaves a written guess a guess.)
|
|
3942
|
+
if (laneStageHook && passLane !== null) laneStageHook(sub, passLane, true, el);
|
|
3943
|
+
else {
|
|
3944
|
+
queuePendingNode(sub);
|
|
3945
|
+
// A15 (#3443): pending propagates onto a held memo without
|
|
3946
|
+
// recomputing it, and the propagation itself enters the memo's
|
|
3947
|
+
// transaction — the flight flows into a memo that transaction
|
|
3948
|
+
// holds, so the write that started it is held with it. A render
|
|
3949
|
+
// effect's membership is its pass's (`notify`), never sticky.
|
|
3950
|
+
if (
|
|
3951
|
+
status === STATUS_PENDING &&
|
|
3952
|
+
(sub._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD &&
|
|
3953
|
+
!sub._type
|
|
3954
|
+
)
|
|
3955
|
+
joinFuture(txOf(sub));
|
|
3795
3956
|
}
|
|
3796
3957
|
notifyStatus(sub, status, error);
|
|
3797
3958
|
}
|
|
@@ -3857,7 +4018,11 @@ const PRIMITIVE_IN_EFFECT_CALLBACK_MESSAGE =
|
|
|
3857
4018
|
* created there is inert and harmless. Tracked effects and onSettled are
|
|
3858
4019
|
* children-forbidden and refuse through PRIMITIVE_IN_FORBIDDEN_SCOPE. */
|
|
3859
4020
|
function assertNotInEffectCallback() {
|
|
3860
|
-
if (
|
|
4021
|
+
if (
|
|
4022
|
+
inEffectCallback &&
|
|
4023
|
+
context === null &&
|
|
4024
|
+
(effectCallbackEvent === ANY_EVENT || dispatchingEvent() === effectCallbackEvent)
|
|
4025
|
+
) {
|
|
3861
4026
|
emitDiagnostic({
|
|
3862
4027
|
code: "PRIMITIVE_IN_EFFECT_CALLBACK",
|
|
3863
4028
|
kind: "lifecycle",
|
|
@@ -3890,7 +4055,7 @@ function ownerInSnapshotScope(owner) {
|
|
|
3890
4055
|
}
|
|
3891
4056
|
function setSnapshotCapture(active) {
|
|
3892
4057
|
snapshotCaptureActive = active;
|
|
3893
|
-
if (active && !snapshotSources) snapshotSources = new
|
|
4058
|
+
if (active && !snapshotSources) snapshotSources = new NewSet();
|
|
3894
4059
|
}
|
|
3895
4060
|
function markSnapshotScope(owner) {
|
|
3896
4061
|
owner._snapshotScope = true;
|
|
@@ -4043,7 +4208,9 @@ function recompute(el, create = false) {
|
|
|
4043
4208
|
// and own nothing to park.
|
|
4044
4209
|
if (isEffect === EFFECT_TRACKED || el._config & CONFIG_STAGED) {
|
|
4045
4210
|
// A lazy node only those children read is released when this pass
|
|
4046
|
-
// ends, if the pass doesn't read it again (#4142).
|
|
4211
|
+
// ends, if the pass doesn't read it again (#4142). A cleanup or
|
|
4212
|
+
// `unobserved` hook that throws here neither stops the pass nor skips
|
|
4213
|
+
// that drain: its error is rethrown when the flush ends (owner.ts).
|
|
4047
4214
|
const prev = deferredReleases;
|
|
4048
4215
|
deferReleases((released = []));
|
|
4049
4216
|
disposeChildren(el);
|
|
@@ -4178,7 +4345,15 @@ function recompute(el, create = false) {
|
|
|
4178
4345
|
// (markNode(c) in read()) marks the running node as part of ordinary
|
|
4179
4346
|
// bookkeeping, and those marks are correctly discarded here.
|
|
4180
4347
|
missedWake = (el._flags & REACTIVE_MISSED_WAKE) !== 0;
|
|
4181
|
-
|
|
4348
|
+
// A mount's staged read joins it — except a nested derivation left
|
|
4349
|
+
// pending: no render effect reaches it yet, so it holds nothing and
|
|
4350
|
+
// reaches no boundary (Reader, #4203). A reader that does is caught.
|
|
4351
|
+
joined = !!(
|
|
4352
|
+
el._flags &
|
|
4353
|
+
(mount && (prevLane || isEffect || !(el._statusFlags & STATUS_PENDING))
|
|
4354
|
+
? REACTIVE_JOINED | REACTIVE_STAGED_READ
|
|
4355
|
+
: REACTIVE_JOINED)
|
|
4356
|
+
);
|
|
4182
4357
|
// A verdict reader is one for as long as it probes, or reads a memo
|
|
4183
4358
|
// that does (`read`): a pass that did neither (REACTIVE_PROBED, the same
|
|
4184
4359
|
// bit) is an ordinary derivation again.
|
|
@@ -4637,6 +4812,12 @@ function joinPass(c, el) {
|
|
|
4637
4812
|
if (el._statusFlags & STATUS_UNINITIALIZED || c._type !== EFFECT_RENDER)
|
|
4638
4813
|
joinPassTx(txOf(el), (!c._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c));
|
|
4639
4814
|
}
|
|
4815
|
+
/** `joinPass` into the hold `t` (a folded guess's: store/optimistic.ts
|
|
4816
|
+
* `foldedRead`, D-34) — a reader is never a render effect there. */
|
|
4817
|
+
function joinPassAt(c, t) {
|
|
4818
|
+
c._flags |= REACTIVE_JOINED;
|
|
4819
|
+
joinPassTx(t, (!c._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c));
|
|
4820
|
+
}
|
|
4640
4821
|
/** A15's stale reader (shared-hole and reveal corollaries): a render effect
|
|
4641
4822
|
* reading a node held by a transaction that is not the flush's. It is served
|
|
4642
4823
|
* the committed value — a pending one's too — publishes mainline, and is
|
|
@@ -4803,7 +4984,7 @@ function setEffectStatusNotify(fn) {
|
|
|
4803
4984
|
* Presence doubles as the "display consumer" membership test in the status
|
|
4804
4985
|
* walks, exactly as the per-node field did when every effect carried one. */
|
|
4805
4986
|
function statusNotifierOf(el) {
|
|
4806
|
-
return el._type ?
|
|
4987
|
+
return el._type ? effectStatusNotify : null;
|
|
4807
4988
|
}
|
|
4808
4989
|
const lazyOptions = { lazy: true };
|
|
4809
4990
|
function setupComputedNode(self, options) {
|
|
@@ -4938,8 +5119,9 @@ function exitCallback() {
|
|
|
4938
5119
|
* its owner (a list pass removing a row, a branch switching away), so its
|
|
4939
5120
|
* writes are imperative like an effect callback's (#4165). Kept apart from
|
|
4940
5121
|
* `callbackDepth` because its sites cannot use try/finally (the frame would
|
|
4941
|
-
* survive into prod)
|
|
4942
|
-
*
|
|
5122
|
+
* survive into prod). A throwing cleanup is caught (owner.ts `runCleanup`);
|
|
5123
|
+
* anything else escaping a teardown leaves it raised, and dev.ts resets it on
|
|
5124
|
+
* the next microtask, which teardown — synchronous — never spans.
|
|
4943
5125
|
*/
|
|
4944
5126
|
let disposalDepth = 0;
|
|
4945
5127
|
function enterDisposal() {
|
|
@@ -4951,11 +5133,31 @@ function exitDisposal() {
|
|
|
4951
5133
|
function resetDisposalDepth() {
|
|
4952
5134
|
disposalDepth = 0;
|
|
4953
5135
|
}
|
|
4954
|
-
|
|
5136
|
+
/**
|
|
5137
|
+
* Dev-only: the DOM event being dispatched when the open strict-read window
|
|
5138
|
+
* opened (`globalThis.event`; null between dispatches and off the main
|
|
5139
|
+
* thread's Window), or `ANY_EVENT` when the window did not record one. A DOM
|
|
5140
|
+
* call made in an effect callback that dispatches events synchronously
|
|
5141
|
+
* (`el.focus()`, `el.click()`, `dialog.showModal()`) runs that event's
|
|
5142
|
+
* handlers inside the window; a handler is a one-shot snapshot whoever
|
|
5143
|
+
* dispatched the event, so a read made while a different event is current is
|
|
5144
|
+
* outside the window (#4235). Only queued effect runs record the event, read
|
|
5145
|
+
* once per queue (the getter is costly under jsdom); `strictReadActive` is
|
|
5146
|
+
* the check.
|
|
5147
|
+
*/
|
|
5148
|
+
let strictReadEvent;
|
|
5149
|
+
// `?? null`: no current event must not read as a missing argument below.
|
|
5150
|
+
const dispatchingEvent = () => globalThis.event ?? null;
|
|
5151
|
+
function setStrictRead(v, event = ANY_EVENT) {
|
|
4955
5152
|
const prev = strictRead;
|
|
4956
5153
|
strictRead = v;
|
|
5154
|
+
strictReadEvent = event;
|
|
4957
5155
|
return prev;
|
|
4958
5156
|
}
|
|
5157
|
+
/** Dev-only: is the strict-read window open for a read made now? */
|
|
5158
|
+
function strictReadActive() {
|
|
5159
|
+
return !!strictRead && (strictReadEvent === ANY_EVENT || dispatchingEvent() === strictReadEvent);
|
|
5160
|
+
}
|
|
4959
5161
|
/**
|
|
4960
5162
|
* Runs `fn` outside of any reactive tracking — reads inside `fn` will not
|
|
4961
5163
|
* subscribe the current scope. Returns whatever `fn` returns.
|
|
@@ -4994,9 +5196,11 @@ function setStrictRead(v) {
|
|
|
4994
5196
|
function untrack(fn, strictReadLabel) {
|
|
4995
5197
|
const prevTracking = tracking;
|
|
4996
5198
|
const prevStrictRead = strictRead;
|
|
5199
|
+
const prevStrictReadEvent = strictReadEvent;
|
|
4997
5200
|
tracking = false;
|
|
4998
5201
|
{
|
|
4999
5202
|
strictRead = strictReadLabel || false;
|
|
5203
|
+
strictReadEvent = ANY_EVENT;
|
|
5000
5204
|
untrackDepth++;
|
|
5001
5205
|
}
|
|
5002
5206
|
try {
|
|
@@ -5006,6 +5210,7 @@ function untrack(fn, strictReadLabel) {
|
|
|
5006
5210
|
tracking = prevTracking;
|
|
5007
5211
|
{
|
|
5008
5212
|
strictRead = prevStrictRead;
|
|
5213
|
+
strictReadEvent = prevStrictReadEvent;
|
|
5009
5214
|
untrackDepth--;
|
|
5010
5215
|
}
|
|
5011
5216
|
}
|
|
@@ -5202,11 +5407,17 @@ function read(el) {
|
|
|
5202
5407
|
if (!c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN)
|
|
5203
5408
|
return el._value;
|
|
5204
5409
|
// (A plain signal is never uninitialized — the carve-out is `serve`'s.)
|
|
5205
|
-
|
|
5410
|
+
// Lane work over a folded lane (D-34), once the frame has a
|
|
5411
|
+
// transaction, reads the screen too.
|
|
5412
|
+
if (
|
|
5413
|
+
((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) &&
|
|
5414
|
+
stagedScreen(c)
|
|
5415
|
+
)
|
|
5416
|
+
return el._value;
|
|
5206
5417
|
stagedRead(c);
|
|
5207
5418
|
return el._pendingValue;
|
|
5208
5419
|
}
|
|
5209
|
-
if (strictRead && owner._statusFlags & STATUS_PENDING)
|
|
5420
|
+
if (strictRead && owner._statusFlags & STATUS_PENDING && strictReadActive())
|
|
5210
5421
|
throwPendingUntrackedRead(strictRead, {
|
|
5211
5422
|
ownerId: c?.id,
|
|
5212
5423
|
ownerName: c?._name,
|
|
@@ -5327,7 +5538,7 @@ function read(el) {
|
|
|
5327
5538
|
// Ahead of the snapshot serve below: a component body's direct read is
|
|
5328
5539
|
// wrong in the same way whether the pass is hydrating or not, and the
|
|
5329
5540
|
// hydration pass is the console nobody is watching (#3675).
|
|
5330
|
-
if (strictRead)
|
|
5541
|
+
if (strictRead && strictReadActive())
|
|
5331
5542
|
warnStrictReadUntracked(strictRead, {
|
|
5332
5543
|
ownerId: c?.id,
|
|
5333
5544
|
ownerName: c?._name,
|
|
@@ -5406,7 +5617,10 @@ function serve(el, c) {
|
|
|
5406
5617
|
}
|
|
5407
5618
|
/** A pass read a staging of this flush: it derives from what the flush may
|
|
5408
5619
|
* yet hold (REACTIVE_STAGED_READ — the seam decides). Lane work too (plan sec. 28, a
|
|
5409
|
-
* lane sees the screen plus its own guesses)
|
|
5620
|
+
* lane sees the screen plus its own guesses) — a folded lane's only before
|
|
5621
|
+
* the frame has a transaction (after, it reads the screen like a verdict
|
|
5622
|
+
* reader, `stagedScreen` — a list pass never pairs a held length with the
|
|
5623
|
+
* guess's rows, D-34): the staging is the screen if
|
|
5410
5624
|
* the frame commits — one pass, the common case — and a held write if it
|
|
5411
5625
|
* parks, which the seam repairs (`stagedReaders`): the pass re-derives on
|
|
5412
5626
|
* the committed world and its lane's runs wait that round, so the held
|
|
@@ -5751,7 +5965,7 @@ export {
|
|
|
5751
5965
|
CONFIG_STAGED as Z,
|
|
5752
5966
|
merge as _,
|
|
5753
5967
|
NotReadyError as a,
|
|
5754
|
-
|
|
5968
|
+
resetUnhandledAsync as a$,
|
|
5755
5969
|
REACTIVE_JOINED as a0,
|
|
5756
5970
|
REACTIVE_STAGED_READ as a1,
|
|
5757
5971
|
stagedRead as a2,
|
|
@@ -5762,133 +5976,140 @@ export {
|
|
|
5762
5976
|
verdict as a7,
|
|
5763
5977
|
link as a8,
|
|
5764
5978
|
pullComputed as a9,
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5979
|
+
signal as aA,
|
|
5980
|
+
setSignal as aB,
|
|
5981
|
+
cleanup as aC,
|
|
5982
|
+
read as aD,
|
|
5983
|
+
DEV as aE,
|
|
5984
|
+
CONFIG_TRANSPARENT as aF,
|
|
5985
|
+
NoOwnerError as aG,
|
|
5986
|
+
ContextNotFoundError as aH,
|
|
5987
|
+
createEffectNode as aI,
|
|
5988
|
+
recompute as aJ,
|
|
5989
|
+
EFFECT_USER as aK,
|
|
5990
|
+
reportDiagnostic as aL,
|
|
5991
|
+
computed as aM,
|
|
5992
|
+
staleValues as aN,
|
|
5993
|
+
CONFIG_AUTO_DISPOSE as aO,
|
|
5994
|
+
EFFECT_TRACKED as aP,
|
|
5995
|
+
heldRunHook as aQ,
|
|
5996
|
+
unwrapStatusError as aR,
|
|
5997
|
+
setStrictRead as aS,
|
|
5998
|
+
queueEvent as aT,
|
|
5999
|
+
StatusError as aU,
|
|
6000
|
+
trimStaleDeps as aV,
|
|
6001
|
+
haltReactivityAt as aW,
|
|
6002
|
+
strictReadEvent as aX,
|
|
6003
|
+
setTrackedQueueCallback as aY,
|
|
6004
|
+
setEffectStatusNotify as aZ,
|
|
6005
|
+
_hitUnhandledAsync as a_,
|
|
5792
6006
|
strictRead as aa,
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
6007
|
+
strictReadActive as ab,
|
|
6008
|
+
warnStrictReadUntracked as ac,
|
|
6009
|
+
staleReader as ad,
|
|
6010
|
+
globalQueue as ae,
|
|
6011
|
+
joinPass as af,
|
|
6012
|
+
markLateLinker as ag,
|
|
6013
|
+
spectating as ah,
|
|
6014
|
+
unflushedValue as ai,
|
|
6015
|
+
CONFIG_STAGED_ERROR as aj,
|
|
6016
|
+
stagedReaders as ak,
|
|
6017
|
+
bornInto as al,
|
|
6018
|
+
enterAction as am,
|
|
6019
|
+
setEffectCallback as an,
|
|
6020
|
+
exitAction as ao,
|
|
6021
|
+
isThenable as ap,
|
|
6022
|
+
actionDepth as aq,
|
|
6023
|
+
flush as ar,
|
|
6024
|
+
setQuestion as as,
|
|
6025
|
+
emitDiagnostic as at,
|
|
6026
|
+
enterCallback as au,
|
|
6027
|
+
exitCallback as av,
|
|
6028
|
+
getOwner as aw,
|
|
6029
|
+
inEffectCallback as ax,
|
|
6030
|
+
effectCallbackEvent as ay,
|
|
6031
|
+
setExternalHooks as az,
|
|
5818
6032
|
CONFIG_AUTHORITATIVE as b,
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
6033
|
+
resetErrorHalt as b$,
|
|
6034
|
+
ROOT_ERROR_HOOK as b0,
|
|
6035
|
+
registerGraph as b1,
|
|
6036
|
+
runWithOwner as b2,
|
|
6037
|
+
setMemo as b3,
|
|
6038
|
+
$REFRESH as b4,
|
|
6039
|
+
markRefresh as b5,
|
|
6040
|
+
createRoot as b6,
|
|
6041
|
+
getObserver as b7,
|
|
6042
|
+
dispose as b8,
|
|
6043
|
+
TimeoutError as b9,
|
|
6044
|
+
handleAsync as bA,
|
|
6045
|
+
addPendingSource as bB,
|
|
6046
|
+
propagateStatus as bC,
|
|
6047
|
+
forEachDependent as bD,
|
|
6048
|
+
settlePendingSource as bE,
|
|
6049
|
+
joinPassAt as bF,
|
|
6050
|
+
notifyEpoch as bG,
|
|
6051
|
+
SUPPORTS_PROXY as bH,
|
|
6052
|
+
releaseMarksHook as bI,
|
|
6053
|
+
installMarkHooks as bJ,
|
|
6054
|
+
CONFIG_LAGS as bK,
|
|
6055
|
+
REACTIVE_REASK as bL,
|
|
6056
|
+
onSupersede as bM,
|
|
6057
|
+
haltReactivity as bN,
|
|
6058
|
+
heldUnder as bO,
|
|
6059
|
+
unhold as bP,
|
|
6060
|
+
laneStageHook as bQ,
|
|
6061
|
+
REACTIVE_ZOMBIE as bR,
|
|
6062
|
+
installBoundaryHooks as bS,
|
|
6063
|
+
CAUGHT_REARMED as bT,
|
|
6064
|
+
OBSERVE as bU,
|
|
6065
|
+
clearSnapshots as bV,
|
|
6066
|
+
enforceLoadingBoundary as bW,
|
|
6067
|
+
getNextChildId as bX,
|
|
6068
|
+
markSnapshotScope as bY,
|
|
6069
|
+
peekNextChildId as bZ,
|
|
6070
|
+
releaseSnapshotScope as b_,
|
|
6071
|
+
untrack as ba,
|
|
6072
|
+
dirtyQueue as bb,
|
|
6073
|
+
CONFIG_REDERIVE as bc,
|
|
6074
|
+
passTx as bd,
|
|
6075
|
+
CONFIG_FRESH_READ as be,
|
|
6076
|
+
createOwner as bf,
|
|
6077
|
+
writeHook as bg,
|
|
6078
|
+
REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE as bh,
|
|
6079
|
+
ownedScopeWriteMessage as bi,
|
|
6080
|
+
isEqual as bj,
|
|
6081
|
+
CONFIG_MANUAL_WRITE as bk,
|
|
6082
|
+
snapshotCaptureActive as bl,
|
|
6083
|
+
captureWriteSnapshot as bm,
|
|
6084
|
+
notePromotedWrite as bn,
|
|
6085
|
+
slotSignal as bo,
|
|
6086
|
+
stagedScreen as bp,
|
|
6087
|
+
CONFIG_PROMOTED as bq,
|
|
6088
|
+
setSlotUnobserved as br,
|
|
6089
|
+
installStoreHooks as bs,
|
|
6090
|
+
joinPassTx as bt,
|
|
6091
|
+
asyncTailFlights as bu,
|
|
6092
|
+
untrackDepth as bv,
|
|
6093
|
+
checkPostAwaitRead as bw,
|
|
6094
|
+
disposalDepth as bx,
|
|
6095
|
+
insertIntoHeap as by,
|
|
6096
|
+
isDisposed as bz,
|
|
5883
6097
|
CONFIG_OVERRIDE as c,
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
6098
|
+
setConsoleFooter as c0,
|
|
6099
|
+
setSnapshotCapture as c1,
|
|
6100
|
+
setWriteHook as c2,
|
|
6101
|
+
setAttributionHooks as c3,
|
|
6102
|
+
liveRootOwners as c4,
|
|
6103
|
+
ownerPath as c5,
|
|
6104
|
+
isSuppressed as c6,
|
|
6105
|
+
blockersOf as c7,
|
|
6106
|
+
CONFIG_PLUMBING as c8,
|
|
6107
|
+
anyExcluded as c9,
|
|
6108
|
+
isExcluded as ca,
|
|
6109
|
+
CONFIG_WIDE as cb,
|
|
6110
|
+
GRAPH_SIZE_WARN_AT as cc,
|
|
6111
|
+
noteFanOut as cd,
|
|
6112
|
+
CONFIG_SUPERSEDES as ce,
|
|
5892
6113
|
newTransaction as d,
|
|
5893
6114
|
enqueueSub as e,
|
|
5894
6115
|
CONFIG_VERDICT as f,
|