@solidjs/signals 2.0.0-rc.16 → 2.0.0-rc.17

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.
Files changed (39) hide show
  1. package/dist/dev-shared.js +1719 -1603
  2. package/dist/dev.attribution.js +13 -13
  3. package/dist/dev.js +182 -45
  4. package/dist/observe/boundaries.js +2 -2
  5. package/dist/observe/core/async.js +3 -1
  6. package/dist/observe/core/context.js +2 -0
  7. package/dist/observe/core/core.js +26 -10
  8. package/dist/observe/core/effect.js +2 -0
  9. package/dist/observe/core/graph.js +10 -8
  10. package/dist/observe/core/lanes.js +25 -14
  11. package/dist/observe/core/owner.js +73 -47
  12. package/dist/observe/core/scheduler.js +189 -173
  13. package/dist/observe/core/verdict.js +3 -0
  14. package/dist/observe/map.js +2 -2
  15. package/dist/observe/signals.js +6 -1
  16. package/dist/observe/store/affects.js +13 -13
  17. package/dist/observe/store/optimistic.js +112 -71
  18. package/dist/observe/store/store.js +25 -16
  19. package/dist/prod/boundaries.js +2 -2
  20. package/dist/prod/core/async.js +3 -1
  21. package/dist/prod/core/context.js +2 -0
  22. package/dist/prod/core/core.js +31 -15
  23. package/dist/prod/core/effect.js +2 -0
  24. package/dist/prod/core/graph.js +10 -8
  25. package/dist/prod/core/heap.js +1 -1
  26. package/dist/prod/core/lanes.js +25 -14
  27. package/dist/prod/core/owner.js +70 -44
  28. package/dist/prod/core/scheduler.js +43 -27
  29. package/dist/prod/core/verdict.js +4 -1
  30. package/dist/prod/map.js +2 -2
  31. package/dist/prod/signals.js +18 -13
  32. package/dist/prod/store/optimistic.js +112 -71
  33. package/dist/prod/store/store.js +18 -7
  34. package/dist/types/core/core.d.ts +10 -3
  35. package/dist/types/core/dev.d.ts +9 -1
  36. package/dist/types/core/error.d.ts +9 -1
  37. package/dist/types/core/owner.d.ts +4 -0
  38. package/dist/types/core/scheduler.d.ts +23 -4
  39. package/package.json +1 -1
@@ -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;
@@ -688,6 +703,19 @@ function locationLine(loc) {
688
703
  return `${location.origin}/${loc}`;
689
704
  return loc;
690
705
  }
706
+ /**
707
+ * The locating lines of a console report: `\n in <App> › <TodoRow> › effect`
708
+ * (the owner path) and `\n at src/App.tsx:18:3` (see `locationLine`), each
709
+ * when the entry carries it; `""` when it carries neither. Shared by
710
+ * `reportDiagnostic` and the sites that print their own console entry (the
711
+ * halt, which logs its cause beside the message).
712
+ */
713
+ function locationLines(entry) {
714
+ let text = "";
715
+ if (entry.ownerPath) text += `\n in ${entry.ownerPath.join(" › ")}`;
716
+ if (entry.loc) text += `\n at ${locationLine(entry.loc)}`;
717
+ return text;
718
+ }
691
719
  /**
692
720
  * The console face of a diagnostic — ONE entry per finding: the message, the
693
721
  * owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
@@ -704,9 +732,7 @@ function locationLine(loc) {
704
732
  */
705
733
  function reportDiagnostic(entry) {
706
734
  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)}`;
735
+ let text = entry.message + locationLines(entry);
710
736
  const footer = takeFooter(entry);
711
737
  if (footer) text += `\n${footer}`;
712
738
  const element = eventSubjects.get(entry)?._devElement;
@@ -989,9 +1015,10 @@ function checkPostAwaitRead(dep, holder, key, nodeName, throwsPending) {
989
1015
  if (callbackDepth !== 0) return;
990
1016
  if (disposalDepth !== 0) {
991
1017
  // Teardown is synchronous, so a depth still raised on the next microtask
992
- // was left behind by a cleanup that threw (owner.ts brackets it without
993
- // try/finally, which would survive into prod). Clear it then, or every
994
- // later check would stay silent.
1018
+ // was left behind by a throw that escaped it (owner.ts brackets it
1019
+ // without try/finally, which would survive into prod; a cleanup's own
1020
+ // throw is caught there). Clear it then, or every later check would stay
1021
+ // silent.
995
1022
  if (!disposalHealQueued) {
996
1023
  disposalHealQueued = true;
997
1024
  queueMicrotask(healDisposalDepth);
@@ -1112,1647 +1139,1704 @@ function devCheckQuiescent(isQueuedForCommit) {
1112
1139
  return;
1113
1140
  }
1114
1141
 
1115
- // Optional-module hooks: module bindings, not `GlobalQueue` statics, so a
1116
- // bundle that never imports the installing module never assigns them — its
1117
- // minifier folds every guarded call site (and the branch behind it) away.
1118
- // Each module installs its own set at evaluation (pay-for-use).
1119
- //
1120
- // Lanes (lanes.ts; installed when `createOptimistic` — or verdict.ts — is
1121
- // imported; every call site is behind a bit or a `passLane` nothing else sets).
1122
- let laneReadHook = null;
1123
- let laneStageHook = null;
1124
- let laneOutcomeHook = null;
1125
- let laneWriteHook = null;
1126
- let applyGuessesHook = null;
1127
- let laneSeamsHook = null;
1128
- let laneCorrectionsHook = null;
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;
1142
+ // L2 (step 2): the parked frame. A pass parks the previous frame's children
1143
+ // and `_disposal` list on `_x._pendingFirstChild` / `_x._pendingDisposal`
1144
+ // (`parkChildren`, core.ts); they keep reacting until the node's commit
1145
+ // disposes them (`zombie` walk below) or the owner dies. `markDisposal` flags
1146
+ // the parked subtree REACTIVE_ZOMBIE: the splice guard and the reawaken path
1147
+ // key off it, and the heap defers a zombie's recompute past the seam (no
1148
+ // second heap — the deferral list in scheduler.ts).
1149
+ function markDisposal(el) {
1150
+ let child = el._firstChild;
1151
+ while (child) {
1152
+ child._flags |= REACTIVE_ZOMBIE;
1153
+ markDisposal(child);
1154
+ child = child._nextSibling;
1155
+ }
1151
1156
  }
1152
- // Boundaries (boundaries.ts): the display consumers between an observer
1153
- // and the root. `catch` — status from a frame reader, nearest boundary
1154
- // first (true: caught, the root never hears of it; a clear — flags 0 —
1155
- // settles the reader there); `fresh` — a pass that read a hold (true: a
1156
- // first pass a loading boundary that has not shown content caught,
1157
- // `joinPass`; `held`: the flush's hold owns it, nothing catches it);
1158
- // `hidden` — a frame reader behind a fallback (the boundary showing it)
1159
- // is not on screen and holds nothing; `boundarySeam` — the seam's sweep (readers
1160
- // gone or settled without a pass reveal; an `on` re-arm resolves);
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;
1157
+ function dispose(node) {
1158
+ // Direct disposal is death, not dormancy: strip the observation lifecycle
1159
+ // so a later read freezes at the last committed value instead of
1160
+ // reawakening the node (#3024). The teardown itself (heap removal — a node
1161
+ // left queued would be recomputed and resurrected by the next flush (#2983)
1162
+ // — dep unlinking, child disposal) is exactly unobserved()'s body; only
1163
+ // this flag distinguishes death from dormancy.
1164
+ node._config &= ~CONFIG_AUTO_DISPOSE;
1165
+ unobserved(node);
1176
1166
  }
1177
- /** Store (store/store.ts): `park` — the flush parked into `t`: the store
1178
- * materializes and holds the container node of every staging that had none
1179
- * (a staging needs the node as its home only when something can read the
1180
- * frame through it; a hold can); `commit` — fold the pending backings whose
1181
- * container nodes this flush committed (the owned-raw model's one mutation
1182
- * point). */
1183
- let storeParkHook = null;
1184
- let storeCommitHook = null;
1185
- function installStoreHooks(park, commit) {
1186
- storeParkHook = park;
1187
- storeCommitHook = commit;
1167
+ // The first error a cleanup threw, boxed so a thrown `undefined` still
1168
+ // counts. A throwing cleanup stops neither its siblings nor the rest of the
1169
+ // tree: every cleanup runs. The error is rethrown once the teardown is over:
1170
+ // by a root's dispose call outside a flush (`disposeRootSelf`), otherwise when
1171
+ // the flush ends (`flush`; `runCleanup` schedules one), so a flush is never
1172
+ // left half-applied. A halt drops it: the error that halted is the one
1173
+ // reported, and a stale one must not surface from a later, unrelated dispose.
1174
+ let cleanupError;
1175
+ /** Runs a cleanup or `unobserved` hook; a throw is collected, not raised. @internal */
1176
+ function runCleanup(fn) {
1177
+ try {
1178
+ fn();
1179
+ } catch (e) {
1180
+ cleanupError ||= [e];
1181
+ schedule();
1182
+ }
1188
1183
  }
1189
- /** `affects()` marks (affects.ts): the releases at a landing and at the seam
1190
- * (ambient marks). */
1191
- let releaseMarksHook = null;
1192
- let releaseAmbientMarksHook = null;
1193
- function installMarkHooks(release, releaseAmbient) {
1194
- releaseMarksHook = release;
1195
- releaseAmbientMarksHook = releaseAmbient;
1184
+ /** Rethrows the error `runCleanup` collected, once; `drop` discards it. @internal */
1185
+ function rethrowCleanupError(drop) {
1186
+ const error = cleanupError;
1187
+ cleanupError = undefined;
1188
+ if (error && !drop) throw error[0];
1196
1189
  }
1197
- /** Verdicts (verdict.ts). */
1198
- let observeFlightHook = null;
1199
- function installVerdictHooks(observeFlight) {
1200
- observeFlightHook = observeFlight;
1190
+ function disposeChildren(node, self = false, zombie) {
1191
+ const flags = node._flags;
1192
+ if (flags & REACTIVE_DISPOSED) return;
1193
+ // A parked frame dies with its owner (#3024): the commit that would retire
1194
+ // it never comes once the node is DISPOSED, so it drains here or its
1195
+ // cleanups never run and it stays subscribed, rerunning in a torn-down
1196
+ // tree (#3561). Death only (`self`): a re-pass leaves the frame reacting
1197
+ // until the commit.
1198
+ if (
1199
+ self &&
1200
+ !zombie &&
1201
+ node._x !== null &&
1202
+ (node._x._pendingFirstChild !== null || node._x._pendingDisposal !== null)
1203
+ )
1204
+ disposeChildren(node, false, true);
1205
+ if (self) {
1206
+ node._flags = flags | REACTIVE_DISPOSED;
1207
+ if (node._parent === null && node._root) unregisterRoot(node);
1208
+ }
1209
+ if (self && true) clearSignals(node);
1210
+ if (self && node._fn) {
1211
+ if (node._x !== null) node._x._inFlight = null;
1212
+ // A death that can unblock the future (L2, `futureBlocked`): a held
1213
+ // flight's — a transaction's or a lane's (its `_x._transaction`) — or a
1214
+ // stale reader's (REACTIVE_FRAME_READ — a frame that derived from one).
1215
+ // Nothing else re-examines a dead node, so make sure a seam comes.
1216
+ if (
1217
+ (node._statusFlags & STATUS_PENDING && node._x?._transaction != null) ||
1218
+ flags & REACTIVE_FRAME_READ
1219
+ )
1220
+ schedule();
1221
+ }
1222
+ let child = zombie ? node._x._pendingFirstChild : node._firstChild;
1223
+ if (zombie) node._x._pendingFirstChild = null;
1224
+ else node._firstChild = null;
1225
+ while (child) {
1226
+ const n = child;
1227
+ // Owner teardown is death regardless of the child's own lifecycle
1228
+ // (#3024): strip AUTO_DISPOSE so a post-disposal read freezes at the
1229
+ // last committed value instead of reawakening in a torn-down tree.
1230
+ // Runs before the recursion so already-dormant children (whose
1231
+ // disposeChildren call early-returns on REACTIVE_DISPOSED) die too.
1232
+ // Only unobserved()'s own node keeps its dormancy — it is never in
1233
+ // this loop; its children are rebuilt fresh on reawaken.
1234
+ n._config &= ~CONFIG_AUTO_DISPOSE;
1235
+ // Heap removal must not be gated on `_deps`: a dependency-free
1236
+ // computation queued by refresh() has a null dep list but still sits in
1237
+ // the dirty heap, and left there the post-disposal flush recomputes it —
1238
+ // recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
1239
+ // comes back to life (post-unmount runs, leaked cleanups, #2983).
1240
+ // deleteFromHeap self-guards on the in-heap flags (and tolerates plain
1241
+ // Owners, whose _flags is undefined), so no gate here.
1242
+ deleteFromHeap(n, dirtyQueue);
1243
+ clearDeps(n);
1244
+ // The chain is detached above so a node a cleanup links mid-drain lands
1245
+ // on the fresh head and survives. Pointing each drained child's prev at
1246
+ // itself routes its later splice onto the detached chain, never the head,
1247
+ // and keeps the dev owner-chain-head invariant honest for those children.
1248
+ child._prevSibling = child;
1249
+ disposeChildren(child, true);
1250
+ // Read after, not before: a sibling this disposal made dormant spliced
1251
+ // itself out of the detached chain, and a cleanup may then have linked
1252
+ // it at the fresh head, which rewrote the `_nextSibling` a pre-read
1253
+ // would still be holding.
1254
+ child = child._nextSibling;
1255
+ }
1256
+ if (!zombie) node._childCount = 0;
1257
+ // O(1) splice out of parent's chain on individual dispose. Skipped during
1258
+ // batch dispose (parent already disposed) and for a zombie (it sits on the
1259
+ // parent's parked frame, drained whole by the commit). We leave
1260
+ // node._nextSibling intact so outer walks that already advanced past us
1261
+ // still reach later siblings.
1262
+ if (
1263
+ self &&
1264
+ !zombie &&
1265
+ !(flags & REACTIVE_ZOMBIE) &&
1266
+ node._parent !== null &&
1267
+ !(node._parent._flags & REACTIVE_DISPOSED)
1268
+ ) {
1269
+ const prev = node._prevSibling;
1270
+ const next = node._nextSibling;
1271
+ // A node with no predecessor must be the chain's head; otherwise the
1272
+ // write below would clobber the head with a stale `_nextSibling`,
1273
+ // orphaning every live child ahead of it (#3543).
1274
+ assertInvariant(
1275
+ prev !== null || node._parent._firstChild === node,
1276
+ "owner-chain-head",
1277
+ "head node is not parent._firstChild — a node was spliced while flagged live but not in the chain (see #3543)"
1278
+ );
1279
+ if (prev !== null) prev._nextSibling = next;
1280
+ else node._parent._firstChild = next;
1281
+ if (next !== null) next._prevSibling = prev;
1282
+ node._prevSibling = null;
1283
+ }
1284
+ runDisposal(node, zombie);
1285
+ // Final effect-returned cleanup fires at true disposal, after `_disposal`
1286
+ // to mirror rerun ordering (compute-phase teardown first, cleanup last).
1287
+ if (self && node._cleanup) {
1288
+ const effectCleanup = node._cleanup;
1289
+ node._cleanup = undefined;
1290
+ enterDisposal();
1291
+ runCleanup(effectCleanup);
1292
+ exitDisposal();
1293
+ }
1201
1294
  }
1202
- /** External-source bridge (wired by enableExternalSource(); undefined while
1203
- * no config is active — including after _resetExternalSourceConfig()). */
1204
- let wireExternalSourceHook = undefined;
1205
- let externalUntrackHook = undefined;
1206
- function setExternalHooks(wire, untrack) {
1207
- wireExternalSourceHook = wire;
1208
- externalUntrackHook = untrack;
1295
+ function linkChild(parent, node) {
1296
+ const head = parent._firstChild;
1297
+ node._prevSibling = null;
1298
+ node._nextSibling = head;
1299
+ if (head !== null) head._prevSibling = node;
1300
+ parent._firstChild = node;
1209
1301
  }
1210
- /** Live transactions — opened, unmerged, not landed. Scanned at every seam
1211
- * while non-empty; nothing on the plain path. */
1212
- const transactions = [];
1213
- /** Observe tier: this flush parked its transaction (`holdStart` fired). */
1214
- let holding = false;
1215
- /** A transaction (listed here) or a lane (lanes.ts lists its own). */
1216
- function newTransaction(lane, parent = null) {
1217
- const t = {
1218
- _nodes: [],
1219
- _queues: [[], []],
1220
- _reruns: [],
1221
- _into: null,
1222
- _parent: parent,
1223
- _lane: lane,
1224
- _verdict: null,
1225
- _open: 0,
1226
- _acted: false,
1227
- _links: null,
1228
- _shown: false,
1229
- _held: false,
1230
- _marks: null
1231
- };
1232
- if (!lane) transactions.push(t);
1233
- return t;
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();
1302
+ function runDisposal(node, zombie) {
1303
+ // Detach the list BEFORE running it (#3601), as `_cleanup` is (#2813). A
1304
+ // cleanup that disposes an ancestor re-enters this node through the death
1305
+ // walk while the loop is still running; with the list still attached, that
1306
+ // walk ran every entry a second time. Detached, the re-entrant drain finds
1307
+ // nothing. The same shape latched a throwing cleanup: the list survived the
1308
+ // throw and every later drain re-ran and re-threw it.
1309
+ const disposal = zombie ? node._x._pendingDisposal : node._disposal;
1310
+ if (!disposal) return;
1311
+ if (zombie) node._x._pendingDisposal = null;
1312
+ else node._disposal = null;
1313
+ // Each cleanup is guarded (`runCleanup`), so the depth is always lowered.
1314
+ enterDisposal();
1315
+ if (Array.isArray(disposal)) {
1316
+ // Unwind order (#3572, restores 1.x #1562): later registrations run
1317
+ // before earlier ones. Children have already been disposed by the caller,
1318
+ // so with LIFO a body that registers cleanup before creating its children
1319
+ // tears down after them — the same order a per-component owner gives.
1320
+ for (let i = disposal.length - 1; i >= 0; i--) {
1321
+ runCleanup(disposal[i]);
1322
+ }
1323
+ } else {
1324
+ runCleanup(disposal);
1325
+ }
1326
+ exitDisposal();
1279
1327
  }
1280
- /** The transaction holding a node (CONFIG_HELD set), resolved through
1281
- * merges; the path is compressed. */
1282
- function txOf(n) {
1283
- return (n._x._transaction = resolveTx(n._x._transaction));
1328
+ function childId(owner, consume) {
1329
+ let counter = owner;
1330
+ while (counter._config & CONFIG_TRANSPARENT && counter._parent) counter = counter._parent;
1331
+ if (counter.id != null)
1332
+ return formatId(counter.id, consume ? counter._childCount++ : counter._childCount);
1333
+ throw new Error("Cannot get child id from owner without an id");
1284
1334
  }
1285
- /** `t` resolved through merges while it is still parked or open; null once
1286
- * it has landed. */
1287
- function liveTx(t) {
1288
- t = resolveTx(t);
1289
- return transactions.indexOf(t) !== -1 ? t : null;
1335
+ /**
1336
+ * Allocates and returns the next stable child id for `owner`. Used by
1337
+ * hydration plumbing and `createUniqueId`. Not part of the user-facing API.
1338
+ *
1339
+ * @internal
1340
+ */
1341
+ function getNextChildId(owner) {
1342
+ return childId(owner, true);
1290
1343
  }
1291
- function resolveTx(t) {
1292
- while (t._into !== null) t = t._into;
1293
- return t;
1344
+ /**
1345
+ * The id a freshly-created node inherits: an explicit `options.id` wins;
1346
+ * transparent nodes share their parent's id; otherwise the parent's next
1347
+ * child id is consumed (or `undefined` outside an id-carrying tree).
1348
+ */
1349
+ function inheritId(options, transparent, parent) {
1350
+ return (
1351
+ options?.id ??
1352
+ (transparent ? parent?.id : parent?.id != null ? getNextChildId(parent) : undefined)
1353
+ );
1294
1354
  }
1295
- /** The flush joins transaction `t` — or needs one of its own (`null`: a pass
1296
- * went pending with nothing held). Joining a second one merges it into the
1297
- * flush's: the work that touched both entangles them (A15). */
1298
- function joinFuture(t) {
1299
- const f = flushTransaction;
1300
- if (f === null) {
1301
- flushTransaction = t === null ? newTransaction(false) : resolveTx(t);
1302
- } else if (t !== null) merge(resolveTx(t), f);
1303
- if (!globalQueue._running) schedule();
1355
+ /**
1356
+ * Returns the *next* child id for `owner` without consuming it. Used by
1357
+ * hydration plumbing to peek at the id a future child will receive.
1358
+ *
1359
+ * @internal
1360
+ */
1361
+ function peekNextChildId(owner) {
1362
+ return childId(owner, false);
1304
1363
  }
1305
- /** `t` merges into `f`: the work that touched both entangles them (A15).
1306
- * A lane never merges: it is a sub-frame, not a peer — its work is routed
1307
- * per pass (`passLane`). */
1308
- function merge(t, f) {
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);
1364
+ function formatId(prefix, id) {
1365
+ const num = id.toString(36),
1366
+ len = num.length - 1;
1367
+ return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
1324
1368
  }
1325
- /** A15: a hold is a property of the async node — observed pending by a
1326
- * render reader. A transaction is blocked while a node it holds, in flight,
1327
- * has one: a pending render effect (the frame itself), or a pending held
1328
- * node a render effect reads. A stale reader served the committed value
1329
- * never went pending, but derives from the flight all the same (#3494) and
1330
- * is still linked to it; a memo between them is pending, and held, too — so
1331
- * one hop of `_subs` is the chain. Reads of the reader's LAST pass only (the
1332
- * link's generation is the pass's): a tail kept for A30 — the committed
1333
- * frame's dependency, awaiting the run that retires it — is not a read of
1334
- * the flight, and a reader that stopped reading releases it (O3, #3494).
1335
- * An errored pass (NotReady included) did not stop: it never got there, and
1336
- * its full list stands (A30) — the reads it did not reach still observe. A
1337
- * flight nobody renders holds nothing (A29). Only the nodes `t` still owns
1338
- * count: one a lane took over since is the lane's to wait on. A guess whose
1339
- * own truth is in flight blocks the lane's parent, not the lane — it stands
1340
- * in for the flight (A17: "visible until its own fetch settles"). */
1341
- function blocked(t) {
1342
- // The transaction being judged (a lane consulted on its behalf judges for
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;
1369
+ /**
1370
+ * Returns the currently-tracking observer (the computation that subscribes to
1371
+ * reactive reads at this point), or `null` if reads here would be untracked.
1372
+ * Used by reactive primitives that need to know whether they're inside a
1373
+ * tracking scope. App code rarely needs this — see `getOwner()` for the
1374
+ * lifecycle owner instead.
1375
+ *
1376
+ * @example
1377
+ * ```ts
1378
+ * // Library predicate: only register a hot-path subscription when the
1379
+ * // caller is inside a tracking scope (memo / effect compute / JSX).
1380
+ * function trackIfTracked(source: () => unknown) {
1381
+ * if (getObserver()) source();
1382
+ * }
1383
+ * ```
1384
+ */
1385
+ function getObserver() {
1386
+ return tracking ? context : null;
1351
1387
  }
1352
- let judge = null;
1353
- /** Observe tier: the flights `t` waits on — its pending held sources a
1354
- * frame reader observes (`blockedBy`'s predicate, collected; the readers
1355
- * themselves are not sources). */
1356
- function blockersOf(t) {
1357
- const out = [];
1358
- for (let i = 0; i < t._nodes.length; i++) {
1359
- const n = t._nodes[i];
1360
- if (
1361
- !n._type &&
1362
- n._statusFlags & STATUS_PENDING &&
1363
- !(n._flags & REACTIVE_DISPOSED) &&
1364
- (n._x._transaction === null || txOf(n) === t) &&
1365
- blockedBy([n], t)
1366
- )
1367
- out.push(n);
1368
- }
1369
- return out;
1388
+ /**
1389
+ * Returns the current reactive **owner** — the lifecycle node that the next
1390
+ * `cleanup()` / `onCleanup()` / `createSignal()` etc. will be attached to.
1391
+ *
1392
+ * Returns `null` if called outside any owner. Capture the owner with
1393
+ * `getOwner()` and re-enter it later with `runWithOwner(owner, fn)` to attach
1394
+ * disposables created from a callback (event handler, async resolution, etc.)
1395
+ * back to a component's lifecycle.
1396
+ *
1397
+ * @example
1398
+ * ```ts
1399
+ * function defer<T>(fn: () => T) {
1400
+ * const owner = getOwner();
1401
+ * queueMicrotask(() => runWithOwner(owner, fn));
1402
+ * }
1403
+ * ```
1404
+ */
1405
+ function getOwner() {
1406
+ return context;
1370
1407
  }
1371
- /** `t`'s own flights only — not its lanes': a flight the frame asked for
1372
- * (a refetch, a plain load) is authoritative; a lane's derivation flight is
1373
- * not (lanes.ts, the body-end corollary). A pending reader counts only if
1374
- * one of the sources it waits on is `t`'s (a render effect marked pending
1375
- * by a lane flight, never re-run, is the lane's wait, not the frame's). */
1376
- function ownFlights(t) {
1377
- return blockedBy(t._nodes, t, true);
1408
+ /**
1409
+ * Low-level: registers `fn` as a disposal callback on the current owner.
1410
+ * Most code should use `onCleanup()` from `solid-js`, which adds dev-mode
1411
+ * checks. `cleanup()` is the unchecked primitive used by internals; `owner`
1412
+ * (internal) targets a specific owner's disposal list instead of the ambient
1413
+ * one.
1414
+ */
1415
+ function cleanup(fn, owner = context) {
1416
+ if (!owner) return fn;
1417
+ if (!owner._disposal) owner._disposal = fn;
1418
+ else if (Array.isArray(owner._disposal)) owner._disposal.push(fn);
1419
+ else owner._disposal = [owner._disposal, fn];
1420
+ return fn;
1378
1421
  }
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
1422
  /**
1546
- * Consume the unhandled-async hit. Returns whether this is the first report
1547
- * of the current enforcement window — the caller warns only then.
1423
+ * Returns `true` if the owner has been disposed. Pair with a captured owner
1424
+ * to bail out of late callbacks whose surrounding component already
1425
+ * unmounted.
1426
+ *
1427
+ * @example
1428
+ * ```ts
1429
+ * function onSettleSafe(fn: () => void) {
1430
+ * const owner = getOwner();
1431
+ * queueMicrotask(() => {
1432
+ * if (owner && isDisposed(owner)) return; // component unmounted; skip
1433
+ * runWithOwner(owner, fn);
1434
+ * });
1435
+ * }
1436
+ * ```
1548
1437
  */
1549
- function resetUnhandledAsync() {
1550
- _hitUnhandledAsync = false;
1551
- if (_reportedUnhandledAsync) return false;
1552
- _reportedUnhandledAsync = true;
1553
- return true;
1438
+ function isDisposed(node) {
1439
+ return !!(node._flags & REACTIVE_DISPOSED);
1440
+ }
1441
+ function disposeRootSelf(self = true) {
1442
+ disposeChildren(this, self);
1443
+ // Outside a flush the dispose call rethrows; inside one, the flush does.
1444
+ // Kept out of `disposeChildren`, whose hot path it slows.
1445
+ if (cleanupError && !globalQueue._running) rethrowCleanupError();
1554
1446
  }
1555
1447
  /**
1556
- * Toggles the dev-mode "must be inside a `<Loading>` boundary" enforcement
1557
- * window. Only `render()` calls this — wrapping the initial mount so that a
1558
- * top-level uncaught async read surfaces the diagnostic. Not part of the
1559
- * user-facing API.
1448
+ * Creates a fresh owner attached as a child of the current owner (or as a
1449
+ * detached root if there is none). Used by framework internals to group
1450
+ * cleanups; app code should use `createRoot()` (host a reactive scope outside
1451
+ * a component) or `runWithOwner()` (re-enter a captured owner).
1560
1452
  *
1561
1453
  * @internal
1562
1454
  */
1563
- function enforceLoadingBoundary(enabled) {
1564
- _enforceLoadingBoundary = enabled;
1565
- if (enabled) _reportedUnhandledAsync = false;
1566
- }
1567
- function setTrackedQueueCallback(value) {
1568
- inTrackedQueueCallback = value;
1569
- }
1570
- // Dev-only marker for the effect half of createEffect/createRenderEffect, so
1571
- // flush() can report the no-op instead of failing silently (React parity).
1572
- /** Action bodies on the stack (action.ts): `flush()` is refused inside one —
1573
- * the body's writes are its transaction's and commit when it settles; a
1574
- * drain mid-step cannot reveal them, and dev says so. */
1575
- let actionDepth = 0;
1576
- function enterAction() {
1577
- actionDepth++;
1578
- }
1579
- function exitAction() {
1580
- actionDepth--;
1455
+ function createOwner(options) {
1456
+ const parent = context;
1457
+ const transparent = options?.transparent ?? false;
1458
+ // Prod and observe boilerplates (see core.ts computed()). The observe
1459
+ // literal carries the `_name` and `_loc` slots the rendering layer fills
1460
+ // with the component label and JSX site (`owner._name = "<App>"`,
1461
+ // `owner._loc = "src/App.tsx:3:5"`) — slots, so labelling a root is a
1462
+ // plain store rather than a shape fork between labelled and plain roots.
1463
+ const owner = {
1464
+ id: inheritId(options, transparent, parent),
1465
+ _config: transparent ? CONFIG_TRANSPARENT : 0,
1466
+ // Present from birth: disposeChildren marks death here on roots too, and
1467
+ // a first store of a missing property is a shape transition plus a
1468
+ // properties-store allocation per root — on a 100×4 table teardown that
1469
+ // was 12 KB and most of disposal's time (2026-10-17).
1470
+ _flags: REACTIVE_NONE,
1471
+ _root: true,
1472
+ _parentComputed: parent?._root ? parent._parentComputed : parent,
1473
+ _firstChild: null,
1474
+ _nextSibling: null,
1475
+ _prevSibling: null,
1476
+ _disposal: null,
1477
+ _context: parent?._context || defaultContext,
1478
+ _childCount: 0,
1479
+ _x: null,
1480
+ _parent: parent,
1481
+ dispose: disposeRootSelf,
1482
+ _name: undefined,
1483
+ _loc: undefined
1484
+ };
1485
+ if (parent && parent._config & CONFIG_CHILDREN_FORBIDDEN) {
1486
+ emitDiagnostic({
1487
+ code: "PRIMITIVE_IN_FORBIDDEN_SCOPE",
1488
+ kind: "lifecycle",
1489
+ severity: "error",
1490
+ message: PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE,
1491
+ ownerId: parent.id,
1492
+ ownerName: parent._name
1493
+ });
1494
+ throw new Error(PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE);
1495
+ }
1496
+ assertNotInEffectCallback();
1497
+ if (parent) linkChild(parent, owner);
1498
+ else registerRoot(owner);
1499
+ DEV.hooks.onOwner?.(owner);
1500
+ return owner;
1581
1501
  }
1582
- /** Provenance (A18, #3331): the question being asked — an action's
1583
- * sequence while a slice of its body and the flush that carries it run, 0
1584
- * otherwise. A guess is stamped with it (lanes.ts; a mainline guess takes a
1585
- * fresh sequence: the newest intent), a flight with the question that
1586
- * started it (async.ts; mainline is always the current question). An
1587
- * answer from an older question than the guess it lands on is stale: held
1588
- * silently, it does not move the graph. */
1589
- let question = 0;
1590
- let questions = 0;
1591
- function setQuestion(q) {
1592
- question = q;
1502
+ /**
1503
+ * Creates a reactive root — an owner scope with its own `dispose()`. A root
1504
+ * created inside an existing owner is owned by it and is disposed when the
1505
+ * parent is disposed; call `dispose()` to tear it down earlier. To create a
1506
+ * root that outlives its creator, detach explicitly:
1507
+ * `runWithOwner(null, () => createRoot(...))`. Pass `id` to seed hydration
1508
+ * ids for the tree it owns.
1509
+ *
1510
+ * `dispose()` tears down every signal, memo, effect, and `onCleanup`
1511
+ * registered inside the root.
1512
+ *
1513
+ * Use this to host long-lived reactive scopes outside of a component (custom
1514
+ * controllers, app bootstrapping, tests). Inside a component, prefer
1515
+ * letting Solid's component lifecycle own things.
1516
+ *
1517
+ * @example
1518
+ * ```ts
1519
+ * // At module level there is no owner, so this root lives until disposed.
1520
+ * const dispose = createRoot(dispose => {
1521
+ * const [n, setN] = createSignal(0);
1522
+ * createEffect(() => n(), value => console.log(value));
1523
+ * setInterval(() => setN(x => x + 1), 1000);
1524
+ * return dispose;
1525
+ * });
1526
+ *
1527
+ * // Later, to tear everything down:
1528
+ * dispose();
1529
+ *
1530
+ * // Inside an owner (component, effect, another root), detach explicitly
1531
+ * // if the root must outlive its creator:
1532
+ * const detached = runWithOwner(null, () => createRoot(d => d));
1533
+ * ```
1534
+ *
1535
+ * @description https://v2.solidjs.com/reference/solid-js/advanced/owner-introspection/create-root
1536
+ */
1537
+ function createRoot(init, options) {
1538
+ const owner = createOwner(options);
1539
+ // A bound function, not an arrow: an arrow would share this scope, which
1540
+ // holds `init`, so keeping `dispose` would keep `init` and all it closes
1541
+ // over alive (#4095).
1542
+ return runWithOwner(owner, () => init(disposeRootSelf.bind(owner, true)));
1593
1543
  }
1594
- function nextQuestion() {
1595
- return ++questions;
1544
+
1545
+ // Optional-module hooks: module bindings, not `GlobalQueue` statics, so a
1546
+ // bundle that never imports the installing module never assigns them — its
1547
+ // minifier folds every guarded call site (and the branch behind it) away.
1548
+ // Each module installs its own set at evaluation (pay-for-use).
1549
+ //
1550
+ // Lanes (lanes.ts; installed when `createOptimistic` — or verdict.ts — is
1551
+ // imported; every call site is behind a bit or a `passLane` nothing else sets).
1552
+ let laneReadHook = null;
1553
+ let laneStageHook = null;
1554
+ let laneOutcomeHook = null;
1555
+ let laneWriteHook = null;
1556
+ let applyGuessesHook = null;
1557
+ let laneSeamsHook = null;
1558
+ let laneCorrectionsHook = null;
1559
+ let endLanesHook = null;
1560
+ let lanesBlockedHook = null;
1561
+ function installLaneHooks(
1562
+ read,
1563
+ stage,
1564
+ outcome,
1565
+ write,
1566
+ applyGuesses,
1567
+ seams,
1568
+ corrections,
1569
+ end,
1570
+ blocked
1571
+ ) {
1572
+ laneReadHook = read;
1573
+ laneStageHook = stage;
1574
+ laneOutcomeHook = outcome;
1575
+ laneWriteHook = write;
1576
+ applyGuessesHook = applyGuesses;
1577
+ laneSeamsHook = seams;
1578
+ laneCorrectionsHook = corrections;
1579
+ endLanesHook = end;
1580
+ lanesBlockedHook = blocked;
1596
1581
  }
1597
- const MAINLINE_QUESTION = 0x7fffffff;
1598
- let inEffectCallback = false;
1599
- function setEffectCallback(value) {
1600
- inEffectCallback = value;
1582
+ // Boundaries (boundaries.ts): the display consumers between an observer
1583
+ // and the root. `catch` — status from a frame reader, nearest boundary
1584
+ // first (true: caught, the root never hears of it; a clear — flags 0 —
1585
+ // settles the reader there); `fresh` — a pass that read a hold (true: a
1586
+ // first pass a loading boundary that has not shown content caught,
1587
+ // `joinPass`; `held`: the flush's hold owns it, nothing catches it);
1588
+ // `hidden` — a frame reader behind a fallback (the boundary showing it)
1589
+ // is not on screen and holds nothing; `boundarySeam` — the seam's sweep (readers
1590
+ // gone or settled without a pass reveal; an `on` re-arm resolves);
1591
+ // `heldRun` — a queued run under a fallback-showing boundary waits for
1592
+ // the reveal (true: held; the boundary re-queues it by type). The
1593
+ // synchronous first render on creation builds the subtree, attached or
1594
+ // not; its updates and the user effects wait.
1595
+ let catchHook = null;
1596
+ let freshHook = null;
1597
+ let hiddenHook = null;
1598
+ let boundarySeamHook = null;
1599
+ let heldRunHook = null;
1600
+ function installBoundaryHooks(caught, fresh, hidden, seam, heldRun) {
1601
+ catchHook = caught;
1602
+ freshHook = fresh;
1603
+ hiddenHook = hidden;
1604
+ boundarySeamHook = seam;
1605
+ heldRunHook = heldRun;
1601
1606
  }
1602
- function schedule() {
1603
- if (halted) {
1604
- notifyHalted();
1605
- return;
1606
- }
1607
- if (scheduled) return;
1608
- scheduled = true;
1609
- if (!syncDepth && !globalQueue._running) {
1610
- queueMicrotask(flush);
1611
- }
1607
+ /** Store (store/store.ts): `park` — the flush parked into `t`: the store
1608
+ * materializes and holds the container node of every staging that had none
1609
+ * (a staging needs the node as its home only when something can read the
1610
+ * frame through it; a hold can); `commit` — fold the pending backings whose
1611
+ * container nodes this flush committed (the owned-raw model's one mutation
1612
+ * point). */
1613
+ let storeParkHook = null;
1614
+ let storeCommitHook = null;
1615
+ function installStoreHooks(park, commit) {
1616
+ storeParkHook = park;
1617
+ storeCommitHook = commit;
1612
1618
  }
1613
- /**
1614
- * Permanently halts the reactive system. Called when a user error escapes
1615
- * every boundary — app state is undefined at that point, so scheduling stops
1616
- * entirely rather than limping along with a half-applied update.
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);
1619
+ /** `affects()` marks (affects.ts): the releases at a landing and at the seam
1620
+ * (ambient marks). */
1621
+ let releaseMarksHook = null;
1622
+ let releaseAmbientMarksHook = null;
1623
+ function installMarkHooks(release, releaseAmbient) {
1624
+ releaseMarksHook = release;
1625
+ releaseAmbientMarksHook = releaseAmbient;
1652
1626
  }
1653
- // Logs on the first write after a halt so a frozen interaction is traceable.
1654
- function notifyHalted() {
1655
- if (haltNotified) return;
1656
- haltNotified = true;
1657
- console.error(
1658
- "[REACTIVITY_HALTED] Update ignored: the reactive system was halted by an earlier uncaught error."
1659
- );
1627
+ /** Verdicts (verdict.ts). */
1628
+ let observeFlightHook = null;
1629
+ function installVerdictHooks(observeFlight) {
1630
+ observeFlightHook = observeFlight;
1660
1631
  }
1661
- /** @internal Test/dev-reload hook. Revives scheduling after a halt. */
1662
- function resetErrorHalt() {
1663
- halted = false;
1664
- haltNotified = false;
1632
+ /** External-source bridge (wired by enableExternalSource(); undefined while
1633
+ * no config is active — including after _resetExternalSourceConfig()). */
1634
+ let wireExternalSourceHook = undefined;
1635
+ let externalUntrackHook = undefined;
1636
+ function setExternalHooks(wire, untrack) {
1637
+ wireExternalSourceHook = wire;
1638
+ externalUntrackHook = untrack;
1665
1639
  }
1666
- class GlobalQueue {
1667
- _queues = [[], []];
1668
- _running = false;
1669
- flush() {
1670
- if (this._running) return;
1671
- this._running = true;
1672
- // A28: companions of nodes written since the last flush mirror the
1673
- // flushed world — re-synced inside the running window (the write is
1674
- // "flushed" from here on).
1675
- resyncUnflushedCompanions();
1676
- passLane = null;
1677
- passTx = null;
1678
- try {
1679
- // Sweep before the heap: unobserved() pulls swept nodes out of it, so
1680
- // a dormant memo dirtied in the same tick is reclaimed instead of
1681
- // recomputed; late subscribers (an effect reading a swept memo this
1682
- // flush) revive it, which is the pay-for-use contract.
1683
- sweepDormant();
1684
- runHeap(dirtyQueue, GlobalQueue._update);
1685
- this.settle();
1686
- clock++;
1687
- // A write the commit sweep staged with no subscriber to dirty is work
1688
- // too — the next round commits it.
1689
- scheduled = dirtyQueue._max >= dirtyQueue._min || pendingNodes.length !== 0;
1690
- this.run(EFFECT_RENDER);
1691
- this.run(EFFECT_USER);
1692
- if (true && holding) {
1693
- holding = false;
1694
- attrHooks.holdEnd();
1695
- }
1696
- if (true && !scheduled) {
1697
- // Fully drained: no staged value may survive this point unqueued or
1698
- // unheld.
1699
- devCheckQuiescent(
1700
- n => pendingNodes.includes(n) || (n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) !== 0
1701
- );
1702
- }
1703
- if (true) DEV.hooks.onUpdate?.();
1704
- } finally {
1705
- this._running = false;
1706
- passTx = null;
1707
- }
1708
- }
1709
- /** L2 — the seam: end of the pure phase. Commit this flush's staged nodes,
1710
- * or park them with the transaction the flush joined; then land every
1711
- * transaction no frame is waiting on. The landing flush is an ordinary
1712
- * flush: the settle walk (async.ts) re-ran the parked readers in its pure
1713
- * phase, and here no flight the transaction holds has a frame deriving
1714
- * from it (`blocked`). */
1715
- settle() {
1716
- // Lanes: an ended action body judges its guesses now that their passes
1717
- // have run (lanes.ts); a correction's re-derivations run in one more
1718
- // pure round — the parent's flights before the parent is judged below.
1719
- if (laneCorrectionsHook?.()) runHeap(dirtyQueue, GlobalQueue._update);
1720
- const joined = flushTransaction;
1721
- flushTransaction = null;
1722
- // The frame's verdict is in: the guesses written since the last seam
1723
- // open their lane under the frame's transaction (a lane's own flush
1724
- // parents a nested lane to it), or are dropped — the frame commits, and
1725
- // there is nothing to be optimistic over. Their passes run next round;
1726
- // a parent landing at this very seam ends the lane before they do.
1727
- applyGuessesHook?.(joined);
1728
- // A lane's own flush (its flight landing, its reveal) parks nothing: the
1729
- // lane's work reached it per pass; the frame's plain writes commit.
1730
- // (Resolved: a guess over another transaction's guess merged them.)
1731
- const t = joined !== null && !joined._lane ? resolveTx(joined) : null;
1732
- if (t !== null) {
1733
- // `t._nodes` grows under the walk (holdFrame pushes); the bound is
1734
- // this flush's list, which does not.
1735
- const count = pendingNodes.length;
1736
- for (let i = 0; i < count; i++) {
1737
- const n = pendingNodes[i];
1738
- // Already held: the pass that re-staged it joined its transaction,
1739
- // which is `t` (or merged into it). Lane work is the lane's (a
1740
- // pending mark propagated onto it queued it here too).
1741
- if (
1742
- !(n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) &&
1743
- (n._x === null || n._x._transaction === null || !n._x._transaction._lane)
1744
- ) {
1745
- // A loading source — pending with nothing staged and no committed
1746
- // value (A16, A19 exc. 1: uninitialized is loading, not pending)
1747
- // that is not an observer — is nobody's frame: not held. Its
1748
- // first landing is commit #0, not a resumption of this frame, and
1749
- // a write that reaches it later overlaps nothing (#2937: the
1750
- // stale stamp was the bridge that entangled unrelated updates
1751
- // with a never-resolving flight). Its observers are held and
1752
- // wait on its flight (`blocked`).
1753
- if (
1754
- n._pendingValue === NOT_PENDING &&
1755
- n._statusFlags & STATUS_UNINITIALIZED &&
1756
- n._type !== EFFECT_RENDER
1757
- )
1758
- continue;
1759
- // A34 (2): a tick whose writes net to the committed value made no
1760
- // proposal (`setShow(false); setShow(true)` on a committed true) —
1761
- // not staged, not held, pends nothing; a later write to it is
1762
- // plain mainline. (A node already held stays held when written
1763
- // back: A34 (1).)
1764
- if (
1765
- n._pendingValue !== NOT_PENDING &&
1766
- !(n._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) &&
1767
- n._x?._pendingFirstChild == null &&
1768
- n._x?._pendingDisposal == null &&
1769
- // The comparator last: a pass that created children (an object
1770
- // owning a fresh async memo, #4111) must not have it read them.
1771
- // A NotReady read there is a change (`isSame`).
1772
- isSame(n, n._value, n._pendingValue)
1773
- ) {
1774
- n._pendingValue = NOT_PENDING;
1775
- n._config &= ~CONFIG_STAGED;
1776
- continue;
1777
- }
1778
- holdNode(n, t);
1779
- }
1780
- // A held pass's frame is held with it (ruling A, O2): the children it
1781
- // created are the transaction's — they compute, and a pass over them
1782
- // or a write that reaches them joins; they effect at the landing.
1783
- if (n._config & CONFIG_STAGED) holdFrame(n, t);
1784
- }
1785
- pendingNodes.length = 0;
1786
- storeParkHook?.(t);
1787
- // (This flush's runs are stashed with `t` below — after the landings,
1788
- // so a `t` that lands at this very seam runs them first, ahead of
1789
- // the runs it held from earlier flushes.)
1790
- // The batch joins: the writes that dirtied these are held with it — a
1791
- // zombie never displays that world. A verdict reader among them does
1792
- // (#3444: `latest()` inside a branch a held Show is removing peers
1793
- // through like the same read outside): it re-runs now, as the
1794
- // holder's verdict lane's work — before the lane seams, which reveal
1795
- // it (nothing commits this seam that could dispose it).
1796
- // (Verdict readers exist only with the verdict engine installed.)
1797
- const zombies = deferredZombies.splice(0);
1798
- if (observeFlightHook) {
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
- }
1640
+ /** Live transactions — opened, unmerged, not landed. Scanned at every seam
1641
+ * while non-empty; nothing on the plain path. */
1642
+ const transactions = [];
1643
+ /** Observe tier: this flush parked its transaction (`holdStart` fired). */
1644
+ let holding = false;
1645
+ /** A transaction (listed here) or a lane (lanes.ts lists its own). */
1646
+ function newTransaction(lane, parent = null) {
1647
+ const t = {
1648
+ _nodes: [],
1649
+ _queues: [[], []],
1650
+ _reruns: [],
1651
+ _into: null,
1652
+ _parent: parent,
1653
+ _lane: lane,
1654
+ _verdict: null,
1655
+ _open: 0,
1656
+ _acted: false,
1657
+ _links: null,
1658
+ _shown: false,
1659
+ _held: false,
1660
+ _marks: null
1661
+ };
1662
+ if (!lane) transactions.push(t);
1663
+ return t;
1664
+ }
1665
+ // Lanes (plan sec. 28, 2026-10-02; lanes.ts). A lane is "a new base of a transition":
1666
+ // the sub-frame an optimistic write opens in the transition of the frame it
1667
+ // is made in. It sees the screen plus its own guesses (plan sec. 19), breaks out of
1668
+ // the parent's hold — its effects run now — and holds itself if its own
1669
+ // derivations hit async. It ends when the parent lands: a guess reverts to
1670
+ // the truth it covered, or lands as the truth that superseded it. A frame
1671
+ // that does not park has nothing to be optimistic over: the write is void.
1672
+ //
1673
+ // The seat of a pass is its node's: a node carrying a lane's value runs as
1674
+ // the lane's whoever dirtied it; a derivation's tracked read of a lane's
1675
+ // value moves its pass into the lane; a leaf's never does (it reads the
1676
+ // screen). Lane work's values go to the lane slot, its runs to the lane's
1677
+ // queues; the rest of the frame is the flush's.
1678
+ /** The lane the running pass is work of; set at `recompute`'s head from the
1679
+ * node, moved by a derivation's lane read, saved and restored around the
1680
+ * pass. */
1681
+ let passLane = null;
1682
+ function setPassLane(l) {
1683
+ passLane = l;
1684
+ }
1685
+ /** The transaction this flush parks into: set by the first join, merged
1686
+ * with every further one. Consumed at the seam. A join outside a flush (a
1687
+ * mount's pending observer, a write to a held node, an action opening)
1688
+ * schedules the seam that consumes it: membership is the tick's — the same
1689
+ * synchronous frame — and the next tick's writes are not this one's. */
1690
+ let flushTransaction = null;
1691
+ /** The transaction the passes running OUTSIDE a flush this tick derive from
1692
+ * (a mainline mount, a mainline re-pass): the passes' own (A29, creation-
1693
+ * time form — "the entry is the pass's alone"): their results are staged
1694
+ * into it, and nothing else in the tick is — a write made after the mount
1695
+ * is a mainline write, a render effect mounted beside it a stale reader.
1696
+ * Inside a flush, or in a tick that already has its transaction (an
1697
+ * action's body), the frame joins instead (`flushTransaction`) — except a
1698
+ * first pass a loading boundary that has not shown content catches (`own`,
1699
+ * A29's boundary exemption, #3540): it is the boundary's, not the tick's,
1700
+ * and a flush that has joined nothing keeps it pass-scoped as outside one.
1701
+ * Cleared by the flush the join schedules, and at the end of a flush that
1702
+ * set it. */
1703
+ let passTx = null;
1704
+ function joinPassTx(t, own) {
1705
+ if ((globalQueue._running && !own) || flushTransaction !== null) return joinFuture(t);
1706
+ if (passTx === null) passTx = resolveTx(t);
1707
+ else merge(resolveTx(t), passTx);
1708
+ schedule();
1709
+ }
1710
+ /** The transaction holding a node (CONFIG_HELD set), resolved through
1711
+ * merges; the path is compressed. */
1712
+ function txOf(n) {
1713
+ return (n._x._transaction = resolveTx(n._x._transaction));
1714
+ }
1715
+ /** `t` resolved through merges while it is still parked or open; null once
1716
+ * it has landed. */
1717
+ function liveTx(t) {
1718
+ t = resolveTx(t);
1719
+ return transactions.indexOf(t) !== -1 ? t : null;
1720
+ }
1721
+ function resolveTx(t) {
1722
+ while (t._into !== null) t = t._into;
1723
+ return t;
1724
+ }
1725
+ /** The flush joins transaction `t` — or needs one of its own (`null`: a pass
1726
+ * went pending with nothing held). Joining a second one merges it into the
1727
+ * flush's: the work that touched both entangles them (A15). */
1728
+ function joinFuture(t) {
1729
+ const f = flushTransaction;
1730
+ if (f === null) {
1731
+ flushTransaction = t === null ? newTransaction(false) : resolveTx(t);
1732
+ } else if (t !== null) merge(resolveTx(t), f);
1733
+ if (!globalQueue._running) schedule();
1734
+ }
1735
+ /** `t` merges into `f`: the work that touched both entangles them (A15).
1736
+ * A lane never merges: it is a sub-frame, not a peer — its work is routed
1737
+ * per pass (`passLane`). */
1738
+ function merge(t, f) {
1739
+ // Both ends resolved: a stale pointer to a transaction already merged
1740
+ // into the other would close a cycle `resolveTx` never leaves.
1741
+ t = resolveTx(t);
1742
+ f = resolveTx(f);
1743
+ if (t === f || t._lane || f._lane) return;
1744
+ if (attrHooks !== null) attrHooks.transitionMerged(f, t);
1745
+ t._into = f;
1746
+ f._open += t._open;
1747
+ f._acted ||= t._acted;
1748
+ append(f._nodes, t._nodes);
1749
+ append(f._queues[0], t._queues[0]);
1750
+ append(f._queues[1], t._queues[1]);
1751
+ append(f._reruns, t._reruns);
1752
+ if (t._marks !== null) append((f._marks ??= []), t._marks);
1753
+ transactions.splice(transactions.indexOf(t), 1);
1754
+ }
1755
+ /** A15: a hold is a property of the async node — observed pending by a
1756
+ * render reader. A transaction is blocked while a node it holds, in flight,
1757
+ * has one: a pending render effect (the frame itself), or a pending held
1758
+ * node a render effect reads. A stale reader served the committed value
1759
+ * never went pending, but derives from the flight all the same (#3494) and
1760
+ * is still linked to it; a memo between them is pending, and held, too — so
1761
+ * one hop of `_subs` is the chain. Reads of the reader's LAST pass only (the
1762
+ * link's generation is the pass's): a tail kept for A30 — the committed
1763
+ * frame's dependency, awaiting the run that retires it — is not a read of
1764
+ * the flight, and a reader that stopped reading releases it (O3, #3494).
1765
+ * An errored pass (NotReady included) did not stop: it never got there, and
1766
+ * its full list stands (A30) — the reads it did not reach still observe. A
1767
+ * flight nobody renders holds nothing (A29). Only the nodes `t` still owns
1768
+ * count: one a lane took over since is the lane's to wait on. A guess whose
1769
+ * own truth is in flight blocks the lane's parent, not the lane — it stands
1770
+ * in for the flight (A17: "visible until its own fetch settles"). */
1771
+ function blocked(t) {
1772
+ // The transaction being judged (a lane consulted on its behalf judges for
1773
+ // it): a zombie whose removal it stages is moot for it alone (`onScreen`).
1774
+ const prev = judge;
1775
+ judge ??= t;
1776
+ // An action still running in it holds it open (action.ts). Its lanes
1777
+ // (lanes.ts): a lane under `t` blocks it while blocked itself.
1778
+ const r = t._open !== 0 || blockedBy(t._nodes, t) || !!lanesBlockedHook?.(t);
1779
+ judge = prev;
1780
+ return r;
1781
+ }
1782
+ let judge = null;
1783
+ /** Observe tier: the flights `t` waits on — its pending held sources a
1784
+ * frame reader observes (`blockedBy`'s predicate, collected; the readers
1785
+ * themselves are not sources). */
1786
+ function blockersOf(t) {
1787
+ const out = [];
1788
+ for (let i = 0; i < t._nodes.length; i++) {
1789
+ const n = t._nodes[i];
1790
+ if (
1791
+ !n._type &&
1792
+ n._statusFlags & STATUS_PENDING &&
1793
+ !(n._flags & REACTIVE_DISPOSED) &&
1794
+ (n._x._transaction === null || txOf(n) === t) &&
1795
+ blockedBy([n], t)
1796
+ )
1797
+ out.push(n);
1907
1798
  }
1908
- enqueue(type, fn, lane = passLane) {
1909
- // Lane work's runs are the lane's: they run at its reveal, not with
1910
- // the frame (which may park). A lane pass that read the frame's own
1911
- // staging is the frame's (`recompute` passes null): its run waits with
1912
- // the frame.
1913
- if (type) (lane !== null ? lane : this)._queues[type - 1].push(fn);
1914
- schedule();
1799
+ return out;
1800
+ }
1801
+ /** `t`'s own flights only — not its lanes': a flight the frame asked for
1802
+ * (a refetch, a plain load) is authoritative; a lane's derivation flight is
1803
+ * not (lanes.ts, the body-end corollary). A pending reader counts only if
1804
+ * one of the sources it waits on is `t`'s (a render effect marked pending
1805
+ * by a lane flight, never re-run, is the lane's wait, not the frame's). */
1806
+ function ownFlights(t) {
1807
+ return blockedBy(t._nodes, t, true);
1808
+ }
1809
+ /** A source the pending node waits on that is `t`'s (held by it, or by no
1810
+ * one) — not another transaction's or lane's work. Itself, when it is the
1811
+ * source. */
1812
+ function ownSource(n, t) {
1813
+ const sources = n._x?._pendingSources;
1814
+ if (sources === undefined) return true;
1815
+ for (const s of sources) {
1816
+ const u = s._x?._transaction;
1817
+ if (u == null || resolveTx(u) === t) return true;
1915
1818
  }
1916
- /** Status reaching the root: pending is absorbed (the root mount defers —
1917
- * dev records it for the ASYNC_OUTSIDE_LOADING_BOUNDARY FYI); an error is
1918
- * unhandled (`false` → the caller halts).
1919
- *
1920
- * L2: this is where a transaction is registered. Pending that reaches an
1921
- * observer (a render effect: the frame) is a frame that cannot show — the
1922
- * flush parks, and the transaction waits on the flight while a frame
1923
- * derives from it (`blocked`). Pending nobody observes holds nothing: a
1924
- * memo in flight that no frame reads is not a frame waiting. (Base: the
1925
- * queue's `_asyncReporters`, INV-3's one registration site.)
1926
- *
1927
- * A boundary between the observer and the root (boundaries.ts) is asked
1928
- * first: a fallback catches what is not ready under it, and the root
1929
- * never hears of it. */
1930
- notify(node, mask, flags, error) {
1931
- if (catchHook?.(node, flags & mask, error)) return true;
1932
- if (mask & STATUS_PENDING) {
1933
- if (flags & STATUS_PENDING) {
1934
- if (_enforceLoadingBoundary) _hitUnhandledAsync = true;
1935
- // A pass that went pending holds its frame: the async's own
1936
- // synchronous frame is not ready (A15 reveal corollary: a reveal
1937
- // that discovers a flight whose inputs are visible holds and joins
1938
- // it; one whose inputs are held is a stale reader and never gets
1939
- // here — `read` serves it the committed value). The frame's, not the
1940
- // observer's: a render effect belongs to whoever dirtied its pass
1941
- // (A15 shared-hole, #3407) — one held by T1 and re-run by T2's write
1942
- // pends for T2's flight, and the two stay parallel; a frame that is
1943
- // T1's already joined T1 through the write or the memo that made it
1944
- // so. Outside a flush — root setup, a mount — the frame is the
1945
- // mount's: its pending observers are one transaction, and a flight
1946
- // started in it resumes it when it lands (maintainer, #3461: "part
1947
- // of the same one"); what the mount published synchronously stays
1948
- // published. Lane work going pending holds the lane, not the frame:
1949
- // `blocked(lane)` finds it in the lane's staging ("if further async
1950
- // downstream is hit it holds like its own transition").
1951
- if (passLane === null) joinFuture(null);
1952
- }
1953
- return true;
1819
+ return false;
1820
+ }
1821
+ function blockedBy(nodes, owner, own = false) {
1822
+ for (let i = 0; i < nodes.length; i++) {
1823
+ const n = nodes[i];
1824
+ if (
1825
+ !(n._statusFlags & STATUS_PENDING) ||
1826
+ n._flags & REACTIVE_DISPOSED ||
1827
+ n._config & CONFIG_GUESS ||
1828
+ (n._x._transaction !== null && txOf(n) !== owner) ||
1829
+ (own && !ownSource(n, owner))
1830
+ )
1831
+ continue;
1832
+ // The frame's own pending observer (off screen: this one holds nothing;
1833
+ // the next listed node may).
1834
+ if (n._type === EFFECT_RENDER) {
1835
+ if (onScreen(n, judge ?? owner)) return true;
1836
+ continue;
1837
+ }
1838
+ for (let s = n._subs; s !== null; s = s._nextSub) {
1839
+ const r = s._sub;
1840
+ // A frame reader: a render effect, or a stale reader of the flight
1841
+ // (REACTIVE_FRAME_READ — a verdict reader, `observeFlight`; lane work,
1842
+ // `frameRead`): served committed instead of going pending, it derives
1843
+ // from the flight all the same and is re-derived at the landing — the
1844
+ // frame's observation survives its reader becoming a lane's (a guess
1845
+ // over a held window, V5/A17). A probe alone does not hold. A
1846
+ // boundary showing content forwards the pending (its output pending,
1847
+ // CONFIG_REDERIVE): transparent, its readers hold through it (A33; the
1848
+ // lane-membership ruling, 2026-10-05). An output that never committed
1849
+ // shows nothing — a mount the frame has not revealed — and holds
1850
+ // nothing.
1851
+ if (
1852
+ ((r._type === EFFECT_RENDER && r._statusFlags & STATUS_PENDING) ||
1853
+ r._flags & REACTIVE_FRAME_READ ||
1854
+ (r._config & CONFIG_REDERIVE && r._statusFlags === STATUS_PENDING)) &&
1855
+ (s._gen === r._depGen || r._x?._error != null) &&
1856
+ !(r._flags & REACTIVE_DISPOSED) &&
1857
+ onScreen(r, judge ?? owner)
1858
+ )
1859
+ return true;
1954
1860
  }
1955
- return false;
1956
1861
  }
1862
+ return false;
1957
1863
  }
1958
- /** A15's stale readers: a frame whose LAST pass read a held node as
1959
- * committed re-derives on the landed world — next round, after the commits.
1960
- * One re-derived since (any later pass cleared the bit: in the transaction,
1961
- * or without the held read) owes nothing. A run this transaction stashed
1962
- * for it is void (#3322: an effect has one value slot, and the stale pass
1963
- * overwrote the value that run was for) — the re-derivation's run replaces
1964
- * it. */
1965
- function reruns(u) {
1966
- for (let i = 0; i < u._reruns.length; i++) {
1967
- const r = u._reruns[i];
1968
- // An errored reader's display is its error (D-39): the landing gives it
1969
- // nothing to re-derive, and a re-run would be a read that retries the
1970
- // errored source (#3945).
1971
- if (r._flags & REACTIVE_DISPOSED || r._statusFlags & STATUS_ERROR) continue;
1972
- if (r._flags & REACTIVE_FRAME_READ || r._config & CONFIG_VERDICT) {
1973
- r._flags &= ~REACTIVE_FRAME_READ;
1974
- r._modified = false;
1975
- // Its frame is replaced by the re-derivation: a run one of its
1976
- // children queued this flush (re-run by the same landing) is for the
1977
- // frame being replaced — void too (#3404).
1978
- for (let c = r._firstChild; c !== null; c = c._nextSibling) if (c._type) c._modified = false;
1979
- enqueueSub(r);
1864
+ /** A frame reader whose say counts for `t`'s hold — `t` the transaction
1865
+ * being judged: not a zombie whose removal `t` stages (A15 #3463: a re-ask
1866
+ * in the unmount frame lands its pending on the zombie it unmounts — the
1867
+ * compiled <Show> of #3372; a zombie blocks every other judgment while it
1868
+ * is visible, a lane's reveal included: only the commit that disposes it
1869
+ * makes its say moot), and not behind a fallback (A33: a boundary showing its fallback is the display of
1870
+ * everything under it, so a reader there is not on screen and holds
1871
+ * nothing — boundaries.ts). */
1872
+ function onScreen(r, t) {
1873
+ return !(r._flags & REACTIVE_ZOMBIE && removalStagedBy(r, t)) && !hiddenHook?.(r);
1874
+ }
1875
+ /** A15 (#3463): a reader whose removal is staged — a zombie, parked by a
1876
+ * pass awaiting its commit — is still on screen and live for every hold,
1877
+ * except the transaction staging its removal: for that one its say is moot
1878
+ * (done, and the commit disposes it; not done, and it stays parked). The
1879
+ * staging transaction is the one holding the pass that parked the frame:
1880
+ * the nearest non-zombie ancestor. */
1881
+ function removalStagedBy(r, t) {
1882
+ let o = r;
1883
+ while (o?._flags & REACTIVE_ZOMBIE) o = o._parent;
1884
+ return o?._config & CONFIG_HELD && o._x?._transaction != null && txOf(o) === resolveTx(t);
1885
+ }
1886
+ /** Unchanged passes this flush whose stale dependency tail awaits the
1887
+ * flush's verdict (A30, #3469): trimmed when the flush commits, kept when
1888
+ * it parks — the committed frame still derives from the previous pass's
1889
+ * dependencies, and a write to one of them must reach the node until a
1890
+ * committing pass replaces it. */
1891
+ const heldTrims = [];
1892
+ function heldTrim(el) {
1893
+ heldTrims.push(el);
1894
+ }
1895
+ /** Zombies popped from the dirty heap this flush (`deferZombie`). The seam
1896
+ * decides their fate: a park cancels them (the writes that dirtied them are
1897
+ * held with it); a commit runs the survivors — the ones whose owner is still
1898
+ * awaiting its commit — in height order, after the commits that would have
1899
+ * disposed them (#3546). No second heap: the list is the deferral. */
1900
+ const deferredZombies = [];
1901
+ function deferZombie(el) {
1902
+ deleteFromHeap(el, dirtyQueue);
1903
+ el._flags &= -4;
1904
+ deferredZombies.push(el);
1905
+ }
1906
+ /** Put a staged node in a transaction: flagged, stamped, on its landing
1907
+ * list. */
1908
+ function holdNode(n, t) {
1909
+ n._config |= CONFIG_HELD;
1910
+ list(n, t);
1911
+ }
1912
+ /** Put a node on a transaction's list, once: a node's `_transaction` is the
1913
+ * transaction whose `_nodes` it is on (every site that sets one does the
1914
+ * other), so a re-pass, a re-landing or a re-staging adds nothing. */
1915
+ function list(n, t) {
1916
+ const x = ext(n);
1917
+ if (x._transaction === t) return;
1918
+ x._transaction = t;
1919
+ t._nodes.push(n);
1920
+ }
1921
+ /** Hold the (uncommitted) frame under a held pass: every computed in the
1922
+ * live child chain, recursively. Plain owners are walked through, not held
1923
+ * — nothing reads or re-passes them. Once per park, over the parked frames
1924
+ * only; recomputes pay nothing for it. A lane's node in the frame (listed on
1925
+ * a lane: a verdict reader broken out of this very hold — display-ahead — or
1926
+ * a derivation of a guess, CONFIG_OVERRIDE; a lane pass's effect) is the
1927
+ * lane's, as the seam's own loop has it: "lane work never makes its node
1928
+ * transaction work" (A15, #3698). Re-listed here it would carry the lane's
1929
+ * value on the transaction's list, where the landing nulls its transaction
1930
+ * and `dissolveLane` no longer finds it — a lane node with no lane, which the
1931
+ * next read of it trips over (`laneRead` → `txOf`). Its frame is the lane's
1932
+ * too (ruling A: a lane pass's children are the lane's), revealed and
1933
+ * retired at the lane's seam. */
1934
+ function holdFrame(owner, t) {
1935
+ for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
1936
+ if (c._x?._transaction?._lane) continue;
1937
+ if (c._fn !== undefined) {
1938
+ if (!(c._config & CONFIG_HELD)) holdNode(c, t);
1939
+ c._config |= CONFIG_HELD_FRAME;
1980
1940
  }
1941
+ holdFrame(c, t);
1981
1942
  }
1982
- u._reruns.length = 0;
1983
1943
  }
1984
- /** The landing: the held frame becomes the frame. */
1985
- function land(u) {
1986
- // Attribution hook: judged complete — its held writes commit next
1987
- // (`_nodes` still lists them).
1988
- if (attrHooks !== null) attrHooks.transitionSettled(u);
1989
- reruns(u);
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);
1944
+ /** The nodes under `owner` a transaction other than `t` holds (boundaries.ts:
1945
+ * what an earlier hold staged under a re-armed boundary). */
1946
+ function heldUnder(owner, t, fn) {
1947
+ for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
1948
+ if ((c._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD && txOf(c) !== t) fn(c);
1949
+ heldUnder(c, t, fn);
1999
1950
  }
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
1951
  }
2006
- /** The held runs join this flush's effect phase after its own runs (the
2007
- * landing's re-passes, in height order): the frame's effects run as one
2008
- * pass would have queued them — a shell re-derived at the landing before a
2009
- * boundary swap held since the write (#3540's log order). */
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
- }
1952
+ /** `n` leaves the hold it was staged in: its next pass publishes mainline. */
1953
+ function unhold(n) {
1954
+ n._x._transaction = null;
1955
+ n._config &= ~CONFIG_HELD;
2015
1956
  }
2016
- /** `a.push(...b)` without the argument-count limit. */
2017
- function append(a, b) {
2018
- for (let i = 0; i < b.length; i++) a.push(b[i]);
1957
+ const dirtyQueue = {
1958
+ _heap: new Array(2000).fill(undefined),
1959
+ _marked: false,
1960
+ _min: 0,
1961
+ _max: 0
1962
+ };
1963
+ let clock = 0;
1964
+ let scheduled = false;
1965
+ let halted = false;
1966
+ let haltNotified = false;
1967
+ // Dev: where the halt happened (the owner path and source lines of its
1968
+ // diagnostic), repeated on the "Update ignored" notice so a frozen interaction
1969
+ // points back at the computation that threw (#4178).
1970
+ let haltedAt = "";
1971
+ // Dev: the computation the halt in progress is located at (haltReactivityAt).
1972
+ let haltSubject;
1973
+ let syncDepth = 0;
1974
+ let inTrackedQueueCallback = false;
1975
+ let _enforceLoadingBoundary = false;
1976
+ let _hitUnhandledAsync = false;
1977
+ // Once per enforcement window: the ASYNC_OUTSIDE_LOADING_BOUNDARY finding is a
1978
+ // fact about the MOUNT ("the root mount will be deferred"), not about each
1979
+ // pending render effect — N async siblings at mount used to produce N copies.
1980
+ let _reportedUnhandledAsync = false;
1981
+ /**
1982
+ * Consume the unhandled-async hit. Returns whether this is the first report
1983
+ * of the current enforcement window — the caller warns only then.
1984
+ */
1985
+ function resetUnhandledAsync() {
1986
+ _hitUnhandledAsync = false;
1987
+ if (_reportedUnhandledAsync) return false;
1988
+ _reportedUnhandledAsync = true;
1989
+ return true;
2019
1990
  }
2020
- /** Nodes staged this flush (`_pendingValue` set), committed at its end. */
2021
- const pendingNodes = [];
2022
- /** Passes that read a staging of this flush as the screen — lane work
2023
- * (core.ts `stagedRead`), a verdict reader before the frame's verdict
2024
- * (verdict.ts): if the frame parks they read a held write — re-derived next
2025
- * round (lanes.ts; a lane's runs wait that round). */
2026
- const stagedReaders = [];
2027
- /** A15's stale reader: `c` was served the committed value of a node `t`
2028
- * holds (a render effect, a verdict reader, lane work) and is re-derived
2029
- * after `t`'s landing (`_reruns`). Once per pass. */
2030
- function staleReader(c, t) {
2031
- if (!(c._flags & REACTIVE_FRAME_READ)) {
2032
- c._flags |= REACTIVE_FRAME_READ;
2033
- t._reruns.push(c);
2034
- }
1991
+ /**
1992
+ * Toggles the dev-mode "must be inside a `<Loading>` boundary" enforcement
1993
+ * window. Only `render()` calls this — wrapping the initial mount so that a
1994
+ * top-level uncaught async read surfaces the diagnostic. Not part of the
1995
+ * user-facing API.
1996
+ *
1997
+ * @internal
1998
+ */
1999
+ function enforceLoadingBoundary(enabled) {
2000
+ _enforceLoadingBoundary = enabled;
2001
+ if (enabled) _reportedUnhandledAsync = false;
2035
2002
  }
2036
- function queuePendingNode(node) {
2037
- lastStagedNodeName = node._name ?? null;
2038
- pendingNodes.push(node);
2039
- if (!globalQueue._running) markUnflushedStaged(); // A28
2003
+ function setTrackedQueueCallback(value) {
2004
+ inTrackedQueueCallback = value;
2040
2005
  }
2041
- // Dev-only attribution for the flush loop guard (#3140): when the guard
2042
- // trips, naming what the loop kept chewing on lets the app author attribute
2043
- // the runaway without patching dist.
2044
- let lastStagedNodeName = null;
2045
- /** §12d: bumped by every recompute and every new subscriber edge. A node's
2046
- * staged-rewrite skip is sound only while NOTHING recomputed or linked since
2047
- * its last notify — a mid-batch pull can clean a marked subscriber, and a
2048
- * skipped re-write would leave it stale. */
2049
- let notifyEpoch = 0;
2050
- function bumpNotifyEpoch() {
2051
- notifyEpoch++;
2006
+ // Dev-only marker for the effect half of createEffect/createRenderEffect, so
2007
+ // flush() can report the no-op instead of failing silently (React parity).
2008
+ /** Action bodies on the stack (action.ts): `flush()` is refused inside one —
2009
+ * the body's writes are its transaction's and commit when it settles; a
2010
+ * drain mid-step cannot reveal them, and dev says so. */
2011
+ let actionDepth = 0;
2012
+ function enterAction() {
2013
+ actionDepth++;
2052
2014
  }
2053
- function insertSubs(node) {
2054
- // §12d: stamp before walking — setSignal's staged-rewrite fast path skips
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);
2015
+ function exitAction() {
2016
+ actionDepth--;
2017
+ }
2018
+ /** Provenance (A18, #3331): the question being asked — an action's
2019
+ * sequence while a slice of its body and the flush that carries it run, 0
2020
+ * otherwise. A guess is stamped with it (lanes.ts; a mainline guess takes a
2021
+ * fresh sequence: the newest intent), a flight with the question that
2022
+ * started it (async.ts; mainline is always the current question). An
2023
+ * answer from an older question than the guess it lands on is stale: held
2024
+ * silently, it does not move the graph. */
2025
+ let question = 0;
2026
+ let questions = 0;
2027
+ function setQuestion(q) {
2028
+ question = q;
2029
+ }
2030
+ function nextQuestion() {
2031
+ return ++questions;
2032
+ }
2033
+ const MAINLINE_QUESTION = 0x7fffffff;
2034
+ let inEffectCallback = false;
2035
+ function setEffectCallback(value) {
2036
+ inEffectCallback = value;
2037
+ }
2038
+ function schedule() {
2039
+ if (halted) {
2040
+ notifyHalted();
2041
+ return;
2042
+ }
2043
+ if (scheduled) return;
2044
+ scheduled = true;
2045
+ if (!syncDepth && !globalQueue._running) {
2046
+ queueMicrotask(flush);
2083
2047
  }
2084
- if (fanOut >= GRAPH_SIZE_WARN_AT) noteFanOut(node, fanOut);
2085
2048
  }
2086
- /** Lane work re-staged (lanes.ts; `recompute`): the lane's members among
2087
- * its subscribers re-derive as the lane's work (REACTIVE_LANE_DIRTY), their
2088
- * runs held with it — not as stale readers republishing the committed view
2089
- * (#3460). After `insertSubs`. A render effect on no lane is marked too:
2090
- * inert for its pass (`recompute` seats a lane only from its own `_x`), it
2091
- * is a live write if the effect becomes a zombie later this flush — an
2092
- * on-screen reader whose removal an adopted write stages follows the lane
2093
- * (the lane-membership ruling, 2026-10-05; `_update`). */
2094
- function laneDirty(node, l) {
2095
- for (let s = node._subs; s !== null; s = s._nextSub) {
2096
- const t = s._sub._x?._transaction;
2097
- if (t?._lane ? sameLane(t, l) : s._sub._type) s._sub._flags |= REACTIVE_LANE_DIRTY;
2049
+ /**
2050
+ * Permanently halts the reactive system. Called when a user error escapes
2051
+ * every boundary — app state is undefined at that point, so scheduling stops
2052
+ * entirely rather than limping along with a half-applied update.
2053
+ */
2054
+ /**
2055
+ * The key a root owner carries its client error hook under (`render`'s
2056
+ * `onError`) — registered, so a runtime writes it with no import of the hook
2057
+ * module (core/error-hooks.ts) and no property mangling in the way. Defined
2058
+ * HERE, not there: a runtime that only writes the key must not retain the
2059
+ * hook machinery (pay-for-use).
2060
+ */
2061
+ const ROOT_ERROR_HOOK = Symbol.for("solid-js/root-error-hook");
2062
+ function haltReactivity(cause) {
2063
+ if (halted) return;
2064
+ halted = true;
2065
+ // A cleanup error collected before the halt is dropped (owner.ts).
2066
+ rethrowCleanupError(true);
2067
+ let message = "[REACTIVITY_HALTED]";
2068
+ {
2069
+ message +=
2070
+ " An uncaught error halted the reactive system. No further updates will be processed. Handle errors with <Errored> or treat this as a crash.";
2071
+ // Located at the computation the error escaped from when the call site
2072
+ // named it (haltReactivityAt), so the console says where it happened
2073
+ // (`in <App> › <Composer>`), not only that it did (#4178). Otherwise the
2074
+ // ambient owner, as every other diagnostic.
2075
+ const entry = emitDiagnostic(
2076
+ {
2077
+ code: "REACTIVITY_HALTED",
2078
+ kind: "error",
2079
+ severity: "error",
2080
+ message
2081
+ },
2082
+ haltSubject
2083
+ );
2084
+ haltedAt = locationLines(entry);
2085
+ message += haltedAt;
2098
2086
  }
2087
+ // Surface the cause here too: callers rethrow it, but a creation-time throw
2088
+ // unwinds through ancestor recomputes that convert it to status instead of
2089
+ // surfacing it (#2884), so the rethrow alone cannot guarantee visibility.
2090
+ // Where the platform has one, hand the cause to its uncaught-error channel
2091
+ // (`reportError` → `error` event → window.onerror / error monitoring): a
2092
+ // halt that only reaches console.error leaves a page that LOOKS alive with
2093
+ // nothing an app or its telemetry can act on (#3338 — an uncaught throw
2094
+ // during the hydration render). The rethrow may reach the top as well in
2095
+ // the non-swallowed cases; a duplicate report beats a silent one.
2096
+ const report = cause !== undefined && globalThis.reportError;
2097
+ report || cause === undefined ? console.error(message) : console.error(message, cause);
2098
+ report && report(cause);
2099
2099
  }
2100
- /** One lane, or two of one link group (#3335): one reveal unit. */
2101
- function sameLane(a, b) {
2102
- return a === b || (a._links !== null && a._links === b._links);
2100
+ /**
2101
+ * Dev: `haltReactivity`, located at `subject` — the computation the uncaught
2102
+ * error escaped from — for the REACTIVITY_HALTED message's owner path. Call
2103
+ * sites pick it in dev builds only (`haltReactivity(e)` in production), so
2104
+ * production keeps the one-argument halt and its bytes unchanged.
2105
+ */
2106
+ function haltReactivityAt(cause, subject) {
2107
+ haltSubject = subject;
2108
+ haltReactivity(cause);
2109
+ haltSubject = undefined;
2110
+ }
2111
+ // Logs on the first write after a halt so a frozen interaction is traceable.
2112
+ function notifyHalted() {
2113
+ if (haltNotified) return;
2114
+ haltNotified = true;
2115
+ console.error(
2116
+ "[REACTIVITY_HALTED] Update ignored: the reactive system was halted by an earlier uncaught error." +
2117
+ haltedAt
2118
+ );
2119
+ }
2120
+ /** @internal Test/dev-reload hook. Revives scheduling after a halt. */
2121
+ function resetErrorHalt() {
2122
+ halted = false;
2123
+ haltNotified = false;
2124
+ haltedAt = "";
2103
2125
  }
2104
- function commitPendingNode(n) {
2105
- const c = n;
2106
- // L2: the commit is where a pass, held or not, stops being uncommitted —
2107
- // unless it has no answer yet. A pass still in flight (pending, nothing
2108
- // staged) stays uncommitted: a re-pass disposes its children on the spot,
2109
- // and the frame it parked stays the frame until a pass of this node
2110
- // commits a value.
2111
- if (!c._fn) {
2112
- n._config &= -385;
2113
- if (n._pendingValue !== NOT_PENDING) {
2114
- n._value = n._pendingValue;
2115
- n._pendingValue = NOT_PENDING;
2126
+ class GlobalQueue {
2127
+ _queues = [[], []];
2128
+ _running = false;
2129
+ flush() {
2130
+ if (this._running) return;
2131
+ this._running = true;
2132
+ // A28: companions of nodes written since the last flush mirror the
2133
+ // flushed world — re-synced inside the running window (the write is
2134
+ // "flushed" from here on).
2135
+ resyncUnflushedCompanions();
2136
+ passLane = null;
2137
+ passTx = null;
2138
+ try {
2139
+ // Sweep before the heap: unobserved() pulls swept nodes out of it, so
2140
+ // a dormant memo dirtied in the same tick is reclaimed instead of
2141
+ // recomputed; late subscribers (an effect reading a swept memo this
2142
+ // flush) revive it, which is the pay-for-use contract.
2143
+ sweepDormant();
2144
+ runHeap(dirtyQueue, GlobalQueue._update);
2145
+ this.settle();
2146
+ clock++;
2147
+ // A write the commit sweep staged with no subscriber to dirty is work
2148
+ // too — the next round commits it.
2149
+ scheduled = dirtyQueue._max >= dirtyQueue._min || pendingNodes.length !== 0;
2150
+ this.run(EFFECT_RENDER);
2151
+ this.run(EFFECT_USER);
2152
+ if (true && holding) {
2153
+ holding = false;
2154
+ attrHooks.holdEnd();
2155
+ }
2156
+ if (true && !scheduled) {
2157
+ // Fully drained: no staged value may survive this point unqueued or
2158
+ // unheld.
2159
+ devCheckQuiescent(
2160
+ n => pendingNodes.includes(n) || (n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) !== 0
2161
+ );
2162
+ }
2163
+ if (true) DEV.hooks.onUpdate?.();
2164
+ } finally {
2165
+ this._running = false;
2166
+ passTx = null;
2116
2167
  }
2117
- return;
2118
2168
  }
2119
- // Computeds only from here (`_statusFlags` is not a signal field). A
2120
- // commit beneath a flight publishes its inputs (A15 reveal corollary,
2121
- // #3305): a reveal of it must observe, not show the pre-flight value.
2122
- const inFlight = n._pendingValue === NOT_PENDING && (c._statusFlags & STATUS_PENDING) !== 0;
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)));
2169
+ /** L2 — the seam: end of the pure phase. Commit this flush's staged nodes,
2170
+ * or park them with the transaction the flush joined; then land every
2171
+ * transaction no frame is waiting on. The landing flush is an ordinary
2172
+ * flush: the settle walk (async.ts) re-ran the parked readers in its pure
2173
+ * phase, and here no flight the transaction holds has a frame deriving
2174
+ * from it (`blocked`). */
2175
+ settle() {
2176
+ // Lanes: an ended action body judges its guesses now that their passes
2177
+ // have run (lanes.ts); a correction's re-derivations run in one more
2178
+ // pure round — the parent's flights before the parent is judged below.
2179
+ if (laneCorrectionsHook?.()) runHeap(dirtyQueue, GlobalQueue._update);
2180
+ const joined = flushTransaction;
2181
+ flushTransaction = null;
2182
+ // The frame's verdict is in: the guesses written since the last seam
2183
+ // open their lane under the frame's transaction (a lane's own flush
2184
+ // parents a nested lane to it), or are dropped — the frame commits, and
2185
+ // there is nothing to be optimistic over. Their passes run next round;
2186
+ // a parent landing at this very seam ends the lane before they do.
2187
+ applyGuessesHook?.(joined);
2188
+ // A lane's own flush (its flight landing, its reveal) parks nothing: the
2189
+ // lane's work reached it per pass; the frame's plain writes commit.
2190
+ // (Resolved: a guess over another transaction's guess merged them.)
2191
+ const t = joined !== null && !joined._lane ? resolveTx(joined) : null;
2192
+ if (t !== null) {
2193
+ // `t._nodes` grows under the walk (holdFrame pushes); the bound is
2194
+ // this flush's list, which does not.
2195
+ const count = pendingNodes.length;
2196
+ for (let i = 0; i < count; i++) {
2197
+ const n = pendingNodes[i];
2198
+ // Already held: the pass that re-staged it joined its transaction,
2199
+ // which is `t` (or merged into it). Lane work is the lane's (a
2200
+ // pending mark propagated onto it queued it here too).
2201
+ if (
2202
+ !(n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) &&
2203
+ (n._x === null || n._x._transaction === null || !n._x._transaction._lane)
2204
+ ) {
2205
+ // A loading source — pending with nothing staged and no committed
2206
+ // value (A16, A19 exc. 1: uninitialized is loading, not pending)
2207
+ // that is not an observer — is nobody's frame: not held. Its
2208
+ // first landing is commit #0, not a resumption of this frame, and
2209
+ // a write that reaches it later overlaps nothing (#2937: the
2210
+ // stale stamp was the bridge that entangled unrelated updates
2211
+ // with a never-resolving flight). Its observers are held and
2212
+ // wait on its flight (`blocked`).
2213
+ if (
2214
+ n._pendingValue === NOT_PENDING &&
2215
+ n._statusFlags & STATUS_UNINITIALIZED &&
2216
+ n._type !== EFFECT_RENDER
2217
+ )
2218
+ continue;
2219
+ // A34 (2): a tick whose writes net to the committed value made no
2220
+ // proposal (`setShow(false); setShow(true)` on a committed true) —
2221
+ // not staged, not held, pends nothing; a later write to it is
2222
+ // plain mainline. (A node already held stays held when written
2223
+ // back: A34 (1).)
2224
+ if (
2225
+ n._pendingValue !== NOT_PENDING &&
2226
+ !(n._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) &&
2227
+ n._x?._pendingFirstChild == null &&
2228
+ n._x?._pendingDisposal == null &&
2229
+ // The comparator last: a pass that created children (an object
2230
+ // owning a fresh async memo, #4111) must not have it read them.
2231
+ // A NotReady read there is a change (`isSame`).
2232
+ isSame(n, n._value, n._pendingValue)
2233
+ ) {
2234
+ n._pendingValue = NOT_PENDING;
2235
+ n._config &= ~CONFIG_STAGED;
2236
+ continue;
2237
+ }
2238
+ holdNode(n, t);
2239
+ }
2240
+ // A held pass's frame is held with it (ruling A, O2): the children it
2241
+ // created are the transaction's — they compute, and a pass over them
2242
+ // or a write that reaches them joins; they effect at the landing.
2243
+ if (n._config & CONFIG_STAGED) holdFrame(n, t);
2244
+ }
2245
+ pendingNodes.length = 0;
2246
+ storeParkHook?.(t);
2247
+ // (This flush's runs are stashed with `t` below — after the landings,
2248
+ // so a `t` that lands at this very seam runs them first, ahead of
2249
+ // the runs it held from earlier flushes.)
2250
+ // The batch joins: the writes that dirtied these are held with it — a
2251
+ // zombie never displays that world. A verdict reader among them does
2252
+ // (#3444: `latest()` inside a branch a held Show is removing peers
2253
+ // through like the same read outside): it re-runs now, as the
2254
+ // holder's verdict lane's work — before the lane seams, which reveal
2255
+ // it (nothing commits this seam that could dispose it).
2256
+ // (Verdict readers exist only with the verdict engine installed.)
2257
+ const zombies = deferredZombies.splice(0);
2258
+ if (observeFlightHook) {
2259
+ zombies.sort((a, b) => a._height - b._height);
2260
+ for (let i = 0; i < zombies.length; i++)
2261
+ if (
2262
+ (zombies[i]._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE)) === REACTIVE_ZOMBIE &&
2263
+ zombies[i]._config & CONFIG_VERDICT
2264
+ )
2265
+ recompute(zombies[i]);
2266
+ }
2267
+ // The unchanged passes' tails stay linked (A30, #3469): their inputs
2268
+ // are held, and the committed frame still derives from them — no trim.
2269
+ } else {
2270
+ commitPendingNodes();
2271
+ // The flush committed: an unchanged pass's stale tail goes now. Not
2272
+ // after a later pass this flush that threw (NotReady included — it
2273
+ // keeps its full list, `_depsTail` marking where it stopped) or that
2274
+ // left an effect owing a run (`runEffect` trims when the run applies):
2275
+ // `recompute`'s own gates.
2276
+ for (let i = 0; i < heldTrims.length; i++) {
2277
+ const t = heldTrims[i];
2278
+ if (t._x?._error == null && !t._modified) trimStaleDeps(t);
2279
+ }
2151
2280
  }
2152
- }
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
- }
2175
- function commitPendingNodes() {
2176
- for (let i = 0; i < pendingNodes.length; i++) {
2177
- const n = pendingNodes[i];
2178
- // A node a lane pass re-staged this round is the lane's to reveal; one
2179
- // a transaction holds (born held by a mainline pass, `passTx`) is that
2180
- // transaction's to commit, at its landing.
2181
- if (!(n._config & (CONFIG_OVERRIDE | CONFIG_HELD))) commitPendingNode(n);
2182
- }
2183
- pendingNodes.length = 0;
2184
- }
2185
- const globalQueue = new GlobalQueue();
2186
- function flush(fn) {
2187
- if (actionDepth > 0) {
2188
- {
2189
- throw new Error(
2190
- "[FLUSH_IN_ACTION] flush() inside an action body is not allowed. An action's writes are held in its " +
2191
- "transaction and commit when the action settles: flush() cannot reveal them, and draining here would " +
2192
- "detach the writes that follow from the transaction. Remove the flush(); to observe the result, read " +
2193
- "after the action resolves."
2194
- );
2281
+ // The list is the flush's either way. Reset only when there is something
2282
+ // to reset (`stagedReaders` below likewise): `length = 0` is a runtime
2283
+ // call even on an empty array, and the plain flush — no unchanged pass,
2284
+ // no lane work — has nothing in either.
2285
+ if (heldTrims.length) heldTrims.length = 0;
2286
+ // Lanes: a blocked one parks its frame (its own flight is up); an
2287
+ // unblocked one reveals this round's work. Then land every transaction
2288
+ // no frame is waiting on — its lanes end with it. Backwards: a landing
2289
+ // removes its entry.
2290
+ // This flush's own runs, the lanes' reveals, and the landings' held runs
2291
+ // are collected apart and ordered below.
2292
+ const own = this._queues;
2293
+ const lanes = (this._queues = [[], []]);
2294
+ // The passes that read this frame's stagings as the screen read held
2295
+ // writes if it parked: the lane seam re-derives them on the committed
2296
+ // world (a lane's runs wait this round).
2297
+ if (laneSeamsHook) {
2298
+ laneSeamsHook(t !== null ? stagedReaders : null);
2299
+ if (stagedReaders.length) stagedReaders.length = 0;
2195
2300
  }
2196
- }
2197
- if (fn) {
2198
- syncDepth++;
2199
- try {
2200
- return fn();
2201
- } finally {
2202
- // Decrement even if the drain throws (a throwing effect): a leaked
2203
- // syncDepth would stop `schedule()` from ever queuing a microtask again.
2204
- try {
2205
- flush();
2206
- } finally {
2207
- syncDepth--;
2208
- }
2301
+ this._queues = [[], []];
2302
+ for (let k = transactions.length - 1; k >= 0; k--) {
2303
+ const u = transactions[k];
2304
+ if (blocked(u)) continue;
2305
+ transactions.splice(k, 1);
2306
+ endLanesHook?.(u);
2307
+ land(u);
2308
+ // A landing's commits can dispose a zombie a transaction judged above
2309
+ // was blocked on (#3463: live "until the commit that disposes it") —
2310
+ // judge them again.
2311
+ k = transactions.length;
2209
2312
  }
2210
- }
2211
- if (globalQueue._running) {
2212
- if (inTrackedQueueCallback) {
2213
- throw new Error(
2214
- "Cannot call flush() from inside onSettled or createTrackedEffect. flush() is not reentrant there. " +
2215
- "Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush())."
2216
- );
2313
+ // The store folds the pending backings whose container nodes committed
2314
+ // — this flush's, or a landing's (store/store.ts installs it).
2315
+ storeCommitHook?.();
2316
+ // Marks declared outside a transaction: the flush that carried them
2317
+ // parked (something async below) — the window is the frame's, the marks
2318
+ // are its transaction's; else they release at the seam, verdict-only,
2319
+ // nothing to show (affects.ts).
2320
+ const live = t && liveTx(t);
2321
+ releaseAmbientMarksHook?.(live);
2322
+ // The effect phase: lane work first (displayed ahead of the frame), then
2323
+ // this flush's runs, then the runs the landings held from earlier
2324
+ // flushes — as one pass would have queued them (#3540: a shell
2325
+ // re-derived at the landing before a boundary swap held since the
2326
+ // write; #3528: a display-ahead swap before the count it is shown
2327
+ // beside). A parked frame's runs wait with its transaction instead: the
2328
+ // effect phase applies a committed frame.
2329
+ const parked = live !== null;
2330
+ // Attribution hook: this flush found its transaction incomplete — its
2331
+ // writes stay staged, its runs are stashed below. Before the effect
2332
+ // phase (the lanes' display-ahead runs are the visible acknowledgers);
2333
+ // `holdEnd` fires after it (`flush`).
2334
+ if (parked && attrHooks !== null) {
2335
+ attrHooks.holdStart(live);
2336
+ holding = true;
2217
2337
  }
2218
- if (inEffectCallback) {
2219
- const message =
2220
- "[FLUSH_IN_EFFECT_CALLBACK] flush() called from inside an effect callback is a no-op: the flush that runs effects is already in progress. " +
2221
- "Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush()).";
2222
- reportDiagnostic(
2223
- emitDiagnostic({
2224
- code: "FLUSH_IN_EFFECT_CALLBACK",
2225
- kind: "lifecycle",
2226
- severity: "warn",
2227
- message
2228
- })
2229
- );
2338
+ for (let i = 0; i < 2; i++) {
2339
+ if (parked) append(live._queues[i], own[i]);
2340
+ // The plain flush — no reveal, no landing, not parked — has nothing to
2341
+ // order around its own runs: they are the queue as they stand (the
2342
+ // same array, not a copy; `run` takes it whole). Every other seam
2343
+ // builds the ordered queue above.
2344
+ this._queues[i] =
2345
+ parked || lanes[i].length || this._queues[i].length
2346
+ ? lanes[i].concat(parked ? [] : own[i], this._queues[i])
2347
+ : own[i];
2230
2348
  }
2231
- return;
2232
- }
2233
- if (halted) return;
2234
- let count = 0;
2235
- // Attribution: whether this call drained anything, so `flushEnd` fires once
2236
- // per real drain and never for a no-op call. The declaration is dead in
2237
- // prod (its only write is behind true) and rollup drops it.
2238
- let drained = false;
2239
- // The drain's opening instant, under the loop's own condition so it fires
2240
- // exactly when `flushEnd` below will. Outside every try (see the rule in
2241
- // attribution-hooks.ts).
2242
- if (attrHooks !== null && scheduled) attrHooks.flushStart();
2243
- while (scheduled) {
2244
- if (++count === 1e5) {
2245
- // Attribution beats a bare guard (#3140): say what kept the loop alive.
2246
- throw new Error(
2247
- `Potential Infinite Loop Detected. Kept alive by scheduled work${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
2248
- );
2349
+ if (deferredZombies.length !== 0) {
2350
+ // Survivors are the displayed frame of a held pass: they rerun for the
2351
+ // live write, parents before children.
2352
+ const zombies = deferredZombies.splice(0).sort((a, b) => a._height - b._height);
2353
+ for (let i = 0; i < zombies.length; i++) {
2354
+ if (!(zombies[i]._flags & REACTIVE_DISPOSED)) recompute(zombies[i]);
2355
+ }
2249
2356
  }
2250
- globalQueue.flush();
2251
- drained = true;
2252
- }
2253
- // Outside every try in this function (see the rule in attribution-hooks.ts):
2254
- // the drain loop above is the one place all scheduled work funnels through,
2255
- // so this is the "committed and effects ran" instant for everything the
2256
- // loop processed.
2257
- if (drained && attrHooks !== null) attrHooks.flushEnd();
2258
- }
2259
- function runQueue(queue, type) {
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;
2357
+ // Boundaries: readers that settled, landed or died this flush are
2358
+ // dropped; a fallback with none left reveals next round.
2359
+ boundarySeamHook?.();
2332
2360
  }
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);
2361
+ run(type) {
2362
+ const effects = this._queues[type - 1];
2363
+ if (effects.length) {
2364
+ this._queues[type - 1] = [];
2365
+ runQueue(effects, type);
2342
2366
  }
2343
2367
  }
2344
- }
2345
- function markNode(el, newState = REACTIVE_DIRTY) {
2346
- const flags = el._flags;
2347
- if ((flags & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= newState) return;
2348
- el._flags = (flags & -4) | newState;
2349
- for (let link = el._subs; link !== null; link = link._nextSub) {
2350
- markNode(link._sub, REACTIVE_CHECK);
2368
+ enqueue(type, fn, lane = passLane) {
2369
+ // Lane work's runs are the lane's: they run at its reveal, not with
2370
+ // the frame (which may park). A lane pass that read the frame's own
2371
+ // staging is the frame's (`recompute` passes null): its run waits with
2372
+ // the frame.
2373
+ if (type) (lane !== null ? lane : this)._queues[type - 1].push(fn);
2374
+ schedule();
2375
+ }
2376
+ /** Status reaching the root: pending is absorbed (the root mount defers —
2377
+ * dev records it for the ASYNC_OUTSIDE_LOADING_BOUNDARY FYI); an error is
2378
+ * unhandled (`false` → the caller halts).
2379
+ *
2380
+ * L2: this is where a transaction is registered. Pending that reaches an
2381
+ * observer (a render effect: the frame) is a frame that cannot show — the
2382
+ * flush parks, and the transaction waits on the flight while a frame
2383
+ * derives from it (`blocked`). Pending nobody observes holds nothing: a
2384
+ * memo in flight that no frame reads is not a frame waiting. (Base: the
2385
+ * queue's `_asyncReporters`, INV-3's one registration site.)
2386
+ *
2387
+ * A boundary between the observer and the root (boundaries.ts) is asked
2388
+ * first: a fallback catches what is not ready under it, and the root
2389
+ * never hears of it. */
2390
+ notify(node, mask, flags, error) {
2391
+ if (catchHook?.(node, flags & mask, error)) return true;
2392
+ if (mask & STATUS_PENDING) {
2393
+ if (flags & STATUS_PENDING) {
2394
+ if (_enforceLoadingBoundary) _hitUnhandledAsync = true;
2395
+ // A pass that went pending holds its frame: the async's own
2396
+ // synchronous frame is not ready (A15 reveal corollary: a reveal
2397
+ // that discovers a flight whose inputs are visible holds and joins
2398
+ // it; one whose inputs are held is a stale reader and never gets
2399
+ // here — `read` serves it the committed value). The frame's, not the
2400
+ // observer's: a render effect belongs to whoever dirtied its pass
2401
+ // (A15 shared-hole, #3407) — one held by T1 and re-run by T2's write
2402
+ // pends for T2's flight, and the two stay parallel; a frame that is
2403
+ // T1's already joined T1 through the write or the memo that made it
2404
+ // so. Outside a flush — root setup, a mount — the frame is the
2405
+ // mount's: its pending observers are one transaction, and a flight
2406
+ // started in it resumes it when it lands (maintainer, #3461: "part
2407
+ // of the same one"); what the mount published synchronously stays
2408
+ // published. Lane work going pending holds the lane, not the frame:
2409
+ // `blocked(lane)` finds it in the lane's staging ("if further async
2410
+ // downstream is hit it holds like its own transition").
2411
+ if (passLane === null) joinFuture(null);
2412
+ }
2413
+ return true;
2414
+ }
2415
+ return false;
2351
2416
  }
2352
2417
  }
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];
2418
+ /** A15's stale readers: a frame whose LAST pass read a held node as
2419
+ * committed re-derives on the landed world — next round, after the commits.
2420
+ * One re-derived since (any later pass cleared the bit: in the transaction,
2421
+ * or without the held read) owes nothing. A run this transaction stashed
2422
+ * for it is void (#3322: an effect has one value slot, and the stale pass
2423
+ * overwrote the value that run was for) — the re-derivation's run replaces
2424
+ * it. */
2425
+ function reruns(u) {
2426
+ for (let i = 0; i < u._reruns.length; i++) {
2427
+ const r = u._reruns[i];
2428
+ // An errored reader's display is its error (D-39): the landing gives it
2429
+ // nothing to re-derive, and a re-run would be a read that retries the
2430
+ // errored source (#3945).
2431
+ if (r._flags & REACTIVE_DISPOSED || r._statusFlags & STATUS_ERROR) continue;
2432
+ if (r._flags & REACTIVE_FRAME_READ || r._config & CONFIG_VERDICT) {
2433
+ r._flags &= ~REACTIVE_FRAME_READ;
2434
+ r._modified = false;
2435
+ // Its frame is replaced by the re-derivation: a run one of its
2436
+ // children queued this flush (re-run by the same landing) is for the
2437
+ // frame being replaced — void too (#3404).
2438
+ for (let c = r._firstChild; c !== null; c = c._nextSibling) if (c._type) c._modified = false;
2439
+ enqueueSub(r);
2361
2440
  }
2362
2441
  }
2363
- heap._max = 0;
2442
+ u._reruns.length = 0;
2364
2443
  }
2365
- function adjustHeight(el, heap) {
2366
- deleteFromHeap(el, heap);
2367
- let newHeight = el._height;
2368
- for (let d = el._deps; d; d = d._nextDep) {
2369
- const dep = d._dep;
2370
- if (dep._fn && dep._height >= newHeight) newHeight = dep._height + 1;
2371
- }
2372
- if (el._height !== newHeight) {
2373
- el._height = newHeight;
2374
- for (let s = el._subs; s !== null; s = s._nextSub) insertIntoHeapHeight(s._sub, heap);
2444
+ /** The landing: the held frame becomes the frame. */
2445
+ function land(u) {
2446
+ // Attribution hook: judged complete — its held writes commit next
2447
+ // (`_nodes` still lists them).
2448
+ if (attrHooks !== null) attrHooks.transitionSettled(u);
2449
+ reruns(u);
2450
+ if (u._marks !== null) releaseMarksHook(u._marks);
2451
+ // Old children die in the commits (cleanups first), then the stashed
2452
+ // effects run ahead of this flush's own. A node a lane took over since
2453
+ // (a guess written over the staged truth) is the lane's to land.
2454
+ for (let i = 0; i < u._nodes.length; i++) {
2455
+ const n = u._nodes[i];
2456
+ if (n._x._transaction === null || txOf(n) !== u) continue;
2457
+ n._x._transaction = null;
2458
+ commitPendingNode(n);
2375
2459
  }
2460
+ // A cleanup those commits ran that wrote a node `u` still held joined
2461
+ // `u`, which has left `transactions`: the write is this landing's, and
2462
+ // the next flush must not park in it (#4089).
2463
+ if (flushTransaction === u) flushTransaction = null;
2464
+ releaseQueues(u);
2376
2465
  }
2377
-
2378
- // L2 (step 2): the parked frame. A pass parks the previous frame's children
2379
- // and `_disposal` list on `_x._pendingFirstChild` / `_x._pendingDisposal`
2380
- // (`parkChildren`, core.ts); they keep reacting until the node's commit
2381
- // disposes them (`zombie` walk below) or the owner dies. `markDisposal` flags
2382
- // the parked subtree REACTIVE_ZOMBIE: the splice guard and the reawaken path
2383
- // key off it, and the heap defers a zombie's recompute past the seam (no
2384
- // second heap — the deferral list in scheduler.ts).
2385
- function markDisposal(el) {
2386
- let child = el._firstChild;
2387
- while (child) {
2388
- child._flags |= REACTIVE_ZOMBIE;
2389
- markDisposal(child);
2390
- child = child._nextSibling;
2466
+ /** The held runs join this flush's effect phase after its own runs (the
2467
+ * landing's re-passes, in height order): the frame's effects run as one
2468
+ * pass would have queued them — a shell re-derived at the landing before a
2469
+ * boundary swap held since the write (#3540's log order). */
2470
+ function releaseQueues(u) {
2471
+ for (let i = 0; i < 2; i++) {
2472
+ append(globalQueue._queues[i], u._queues[i]);
2473
+ u._queues[i] = [];
2391
2474
  }
2392
2475
  }
2393
- function dispose(node) {
2394
- // Direct disposal is death, not dormancy: strip the observation lifecycle
2395
- // so a later read freezes at the last committed value instead of
2396
- // reawakening the node (#3024). The teardown itself (heap removal — a node
2397
- // left queued would be recomputed and resurrected by the next flush (#2983)
2398
- // — dep unlinking, child disposal) is exactly unobserved()'s body; only
2399
- // this flag distinguishes death from dormancy.
2400
- node._config &= ~CONFIG_AUTO_DISPOSE;
2401
- unobserved(node);
2476
+ /** `a.push(...b)` without the argument-count limit. */
2477
+ function append(a, b) {
2478
+ for (let i = 0; i < b.length; i++) a.push(b[i]);
2402
2479
  }
2403
- function disposeChildren(node, self = false, zombie) {
2404
- const flags = node._flags;
2405
- if (flags & REACTIVE_DISPOSED) return;
2406
- // A parked frame dies with its owner (#3024): the commit that would retire
2407
- // it never comes once the node is DISPOSED, so it drains here or its
2408
- // cleanups never run and it stays subscribed, rerunning in a torn-down
2409
- // tree (#3561). Death only (`self`): a re-pass leaves the frame reacting
2410
- // until the commit.
2411
- if (
2412
- self &&
2413
- !zombie &&
2414
- node._x !== null &&
2415
- (node._x._pendingFirstChild !== null || node._x._pendingDisposal !== null)
2416
- )
2417
- disposeChildren(node, false, true);
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;
2468
- }
2469
- if (!zombie) node._childCount = 0;
2470
- // O(1) splice out of parent's chain on individual dispose. Skipped during
2471
- // batch dispose (parent already disposed) and for a zombie (it sits on the
2472
- // parent's parked frame, drained whole by the commit). We leave
2473
- // node._nextSibling intact so outer walks that already advanced past us
2474
- // still reach later siblings.
2475
- if (
2476
- self &&
2477
- !zombie &&
2478
- !(flags & REACTIVE_ZOMBIE) &&
2479
- node._parent !== null &&
2480
- !(node._parent._flags & REACTIVE_DISPOSED)
2481
- ) {
2482
- const prev = node._prevSibling;
2483
- const next = node._nextSibling;
2484
- // A node with no predecessor must be the chain's head; otherwise the
2485
- // write below would clobber the head with a stale `_nextSibling`,
2486
- // orphaning every live child ahead of it (#3543).
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;
2496
- }
2497
- runDisposal(node, zombie);
2498
- // Final effect-returned cleanup fires at true disposal, after `_disposal`
2499
- // to mirror rerun ordering (compute-phase teardown first, cleanup last).
2500
- if (self && node._cleanup) {
2501
- const effectCleanup = node._cleanup;
2502
- node._cleanup = undefined;
2503
- enterDisposal();
2504
- effectCleanup();
2505
- exitDisposal();
2480
+ /** Nodes staged this flush (`_pendingValue` set), committed at its end. */
2481
+ const pendingNodes = [];
2482
+ /** Passes that read a staging of this flush as the screen — lane work
2483
+ * (core.ts `stagedRead`), a verdict reader before the frame's verdict
2484
+ * (verdict.ts): if the frame parks they read a held write — re-derived next
2485
+ * round (lanes.ts; a lane's runs wait that round). */
2486
+ const stagedReaders = [];
2487
+ /** A15's stale reader: `c` was served the committed value of a node `t`
2488
+ * holds (a render effect, a verdict reader, lane work) and is re-derived
2489
+ * after `t`'s landing (`_reruns`). Once per pass. */
2490
+ function staleReader(c, t) {
2491
+ if (!(c._flags & REACTIVE_FRAME_READ)) {
2492
+ c._flags |= REACTIVE_FRAME_READ;
2493
+ t._reruns.push(c);
2506
2494
  }
2507
2495
  }
2508
- function linkChild(parent, node) {
2509
- const head = parent._firstChild;
2510
- node._prevSibling = null;
2511
- node._nextSibling = head;
2512
- if (head !== null) head._prevSibling = node;
2513
- parent._firstChild = node;
2496
+ function queuePendingNode(node) {
2497
+ lastStagedNodeName = node._name ?? null;
2498
+ pendingNodes.push(node);
2499
+ if (!globalQueue._running) markUnflushedStaged(); // A28
2514
2500
  }
2515
- function runDisposal(node, zombie) {
2516
- // Detach the list BEFORE running it (#3601), as `_cleanup` is (#2813). A
2517
- // cleanup that disposes an ancestor re-enters this node through the death
2518
- // walk while the loop is still running; with the list still attached, that
2519
- // walk ran every entry a second time. Detached, the re-entrant drain finds
2520
- // nothing. The same shape latched a throwing cleanup: the list survived the
2521
- // throw and every later drain re-ran and re-threw it.
2522
- const disposal = zombie ? node._x._pendingDisposal : node._disposal;
2523
- if (!disposal) return;
2524
- if (zombie) node._x._pendingDisposal = null;
2525
- else node._disposal = null;
2526
- // No try/finally: it would survive into prod (rollup keeps the frame). A
2527
- // throw leaves the depth raised; dev.ts clears the stale count on the next
2528
- // microtask, since teardown never spans one (core.ts).
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);
2501
+ // Dev-only attribution for the flush loop guard (#3140): when the guard
2502
+ // trips, naming what the loop kept chewing on lets the app author attribute
2503
+ // the runaway without patching dist.
2504
+ let lastStagedNodeName = null;
2505
+ /** §12d: bumped by every recompute and every new subscriber edge. A node's
2506
+ * staged-rewrite skip is sound only while NOTHING recomputed or linked since
2507
+ * its last notify — a mid-batch pull can clean a marked subscriber, and a
2508
+ * skipped re-write would leave it stale. */
2509
+ let notifyEpoch = 0;
2510
+ function bumpNotifyEpoch() {
2511
+ notifyEpoch++;
2512
+ }
2513
+ function insertSubs(node) {
2514
+ // §12d: stamp before walking — setSignal's staged-rewrite fast path skips
2515
+ // the next walk for this node while the epoch holds (marking is idempotent).
2516
+ node._notifiedAt = notifyEpoch;
2517
+ // Presence bits gate the optional-slot probes (see constants.ts): one
2518
+ // masked read of the always-present _config instead of missing-property
2519
+ // lookups in the hottest notify loop.
2520
+ const cfg = node._config;
2521
+ const hasSnapshot = (cfg & CONFIG_HAS_SNAPSHOT) !== 0 && node._x?._snapshotValue !== undefined;
2522
+ // Observe-tier fan-out: this walk visits every subscriber edge anyway, so
2523
+ // the graph-size count is one local increment here and no field anywhere.
2524
+ let fanOut = 0;
2525
+ for (let s = node._subs; s !== null; s = s._nextSub) {
2526
+ const sub = s._sub;
2527
+ fanOut++;
2528
+ // Missed-wake latch (#3037): this write is landing while the subscriber
2529
+ // is mid-recompute (a nested pull committing beneath its reads), and the
2530
+ // heap refuses RECOMPUTING nodes. A gen-current link means the pass
2531
+ // already validated this dep — the value it read is now stale — so latch
2532
+ // for recompute's tail to reschedule. Untouched links need no latch (the
2533
+ // pass either re-reads them fresh or trims them), and neither does the
2534
+ // tail link: it is the read IN FLIGHT — read() links before it pulls, so
2535
+ // this very commit is what that read returns.
2536
+ if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
2537
+ sub._flags |= REACTIVE_MISSED_WAKE;
2538
+ if (hasSnapshot && sub._config & CONFIG_IN_SNAPSHOT_SCOPE) {
2539
+ sub._flags |= REACTIVE_SNAPSHOT_STALE;
2540
+ continue;
2538
2541
  }
2539
- } else {
2540
- disposal.call(disposal);
2542
+ enqueueSub(sub);
2541
2543
  }
2542
- exitDisposal();
2544
+ if (fanOut >= GRAPH_SIZE_WARN_AT) noteFanOut(node, fanOut);
2543
2545
  }
2544
- function childId(owner, consume) {
2545
- let counter = owner;
2546
- while (counter._config & CONFIG_TRANSPARENT && counter._parent) counter = counter._parent;
2547
- if (counter.id != null)
2548
- return formatId(counter.id, consume ? counter._childCount++ : counter._childCount);
2549
- throw new Error("Cannot get child id from owner without an id");
2546
+ /** Lane work re-staged (lanes.ts; `recompute`): the lane's members among
2547
+ * its subscribers re-derive as the lane's work (REACTIVE_LANE_DIRTY), their
2548
+ * runs held with it — not as stale readers republishing the committed view
2549
+ * (#3460). After `insertSubs`. A render effect on no lane is marked too:
2550
+ * inert for its pass (`recompute` seats a lane only from its own `_x`), it
2551
+ * is a live write if the effect becomes a zombie later this flush — an
2552
+ * on-screen reader whose removal an adopted write stages follows the lane
2553
+ * (the lane-membership ruling, 2026-10-05; `_update`). */
2554
+ function laneDirty(node, l) {
2555
+ for (let s = node._subs; s !== null; s = s._nextSub) {
2556
+ const t = s._sub._x?._transaction;
2557
+ if (t?._lane ? sameLane(t, l) : s._sub._type) s._sub._flags |= REACTIVE_LANE_DIRTY;
2558
+ }
2550
2559
  }
2551
- /**
2552
- * Allocates and returns the next stable child id for `owner`. Used by
2553
- * hydration plumbing and `createUniqueId`. Not part of the user-facing API.
2554
- *
2555
- * @internal
2556
- */
2557
- function getNextChildId(owner) {
2558
- return childId(owner, true);
2560
+ /** One lane, or two of one link group (#3335): one reveal unit. */
2561
+ function sameLane(a, b) {
2562
+ return a === b || (a._links !== null && a._links === b._links);
2559
2563
  }
2560
- /**
2561
- * The id a freshly-created node inherits: an explicit `options.id` wins;
2562
- * transparent nodes share their parent's id; otherwise the parent's next
2563
- * child id is consumed (or `undefined` outside an id-carrying tree).
2564
- */
2565
- function inheritId(options, transparent, parent) {
2566
- return (
2567
- options?.id ??
2568
- (transparent ? parent?.id : parent?.id != null ? getNextChildId(parent) : undefined)
2569
- );
2564
+ function commitPendingNode(n) {
2565
+ const c = n;
2566
+ // L2: the commit is where a pass, held or not, stops being uncommitted —
2567
+ // unless it has no answer yet. A pass still in flight (pending, nothing
2568
+ // staged) stays uncommitted: a re-pass disposes its children on the spot,
2569
+ // and the frame it parked stays the frame until a pass of this node
2570
+ // commits a value.
2571
+ if (!c._fn) {
2572
+ n._config &= -385;
2573
+ if (n._pendingValue !== NOT_PENDING) {
2574
+ n._value = n._pendingValue;
2575
+ n._pendingValue = NOT_PENDING;
2576
+ }
2577
+ return;
2578
+ }
2579
+ // Computeds only from here (`_statusFlags` is not a signal field). A
2580
+ // commit beneath a flight publishes its inputs (A15 reveal corollary,
2581
+ // #3305): a reveal of it must observe, not show the pre-flight value.
2582
+ const inFlight = n._pendingValue === NOT_PENDING && (c._statusFlags & STATUS_PENDING) !== 0;
2583
+ const err = n._config & CONFIG_STAGED_ERROR;
2584
+ // An answer commits: the error it replaces leaves the display (D-41).
2585
+ if (!inFlight && c._x) c._x._shownError = undefined;
2586
+ n._config = inFlight ? (n._config & -1048833) | CONFIG_INPUTS_PUBLISHED : n._config & -3147137;
2587
+ // D-41: the staged answer is an error — published now, when a value would
2588
+ // show, through the ordinary error path (`stageError`). Its readers first
2589
+ // see it in the cycle after this seam: a tracked re-read there is not a
2590
+ // retry (`read`), or a boundary re-derived by it would retry it forever.
2591
+ if (err) {
2592
+ const e = n._pendingValue;
2593
+ n._pendingValue = NOT_PENDING;
2594
+ n._time = clock + 1;
2595
+ notifyStatus(c, STATUS_ERROR, e);
2596
+ } else if (n._pendingValue !== NOT_PENDING) {
2597
+ n._value = n._pendingValue;
2598
+ n._pendingValue = NOT_PENDING;
2599
+ // A node born staged (recompute) initializes at this commit.
2600
+ c._statusFlags &= ~STATUS_UNINITIALIZED;
2601
+ // A quiet re-ask's classification survives its landing and dies with
2602
+ // the landed value's commit (A19 exc. 2, #3178): verdict-quiet through
2603
+ // the reveal. (The sweep of a node still in flight is not a landing.)
2604
+ // The A28 stash goes with it.
2605
+ if (c._x != null) ((c._x._reask = false), (c._x._flushed = NOT_PENDING));
2606
+ // An effect with a staged value was born held (L2): its first run is
2607
+ // this commit's, not its creation's (A29) — queue it now.
2608
+ if (n._type && n._type !== EFFECT_TRACKED) {
2609
+ n._modified = true;
2610
+ globalQueue.enqueue(n._type, (n._boundRunEffect ??= GlobalQueue._runEffect.bind(null, c)));
2611
+ }
2612
+ }
2613
+ // The committed value is the first observable answer for a loading-window
2614
+ // node — the window closes here, not at compute time (#2990). Unconditional
2615
+ // store to an always-present computed slot.
2616
+ c._loading = false;
2617
+ c._flags &= ~REACTIVE_MANUAL_WRITE;
2618
+ // The dependencies of the pass that produced the value are the frame's now:
2619
+ // the previous frame's tail goes (A30, #3410; `recompute` left it for a
2620
+ // staged pass). Only after a clean pass: `_error` is cleared by a clean
2621
+ // pass or by the node's own landing (whose pass was clean), so a set
2622
+ // `_error` means the last pass threw, kept its full list, and `_depsTail`
2623
+ // marks where it stopped.
2624
+ if (c._x?._error == null) trimStaleDeps(c);
2625
+ if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
2626
+ // L2: the children this commit publishes are the frame's now — the frame
2627
+ // they replace, parked by the pass (`recompute`), goes.
2628
+ if (
2629
+ !inFlight &&
2630
+ c._x != null &&
2631
+ (c._x._pendingFirstChild !== null || c._x._pendingDisposal !== null)
2632
+ )
2633
+ GlobalQueue._dispose(c, false, true);
2570
2634
  }
2571
- /**
2572
- * Returns the *next* child id for `owner` without consuming it. Used by
2573
- * hydration plumbing to peek at the id a future child will receive.
2574
- *
2575
- * @internal
2576
- */
2577
- function peekNextChildId(owner) {
2578
- return childId(owner, false);
2635
+ function commitPendingNodes() {
2636
+ for (let i = 0; i < pendingNodes.length; i++) {
2637
+ const n = pendingNodes[i];
2638
+ // A node a lane pass re-staged this round is the lane's to reveal; one
2639
+ // a transaction holds (born held by a mainline pass, `passTx`) is that
2640
+ // transaction's to commit, at its landing.
2641
+ if (!(n._config & (CONFIG_OVERRIDE | CONFIG_HELD))) commitPendingNode(n);
2642
+ }
2643
+ pendingNodes.length = 0;
2579
2644
  }
2580
- function formatId(prefix, id) {
2581
- const num = id.toString(36),
2582
- len = num.length - 1;
2583
- return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
2645
+ const globalQueue = new GlobalQueue();
2646
+ function flush(fn) {
2647
+ if (actionDepth > 0) {
2648
+ {
2649
+ throw new Error(
2650
+ "[FLUSH_IN_ACTION] flush() inside an action body is not allowed. An action's writes are held in its " +
2651
+ "transaction and commit when the action settles: flush() cannot reveal them, and draining here would " +
2652
+ "detach the writes that follow from the transaction. Remove the flush(); to observe the result, read " +
2653
+ "after the action resolves."
2654
+ );
2655
+ }
2656
+ }
2657
+ if (fn) {
2658
+ syncDepth++;
2659
+ try {
2660
+ return fn();
2661
+ } finally {
2662
+ // Decrement even if the drain throws (a throwing effect): a leaked
2663
+ // syncDepth would stop `schedule()` from ever queuing a microtask again.
2664
+ try {
2665
+ flush();
2666
+ } finally {
2667
+ syncDepth--;
2668
+ }
2669
+ }
2670
+ }
2671
+ if (globalQueue._running) {
2672
+ if (inTrackedQueueCallback) {
2673
+ throw new Error(
2674
+ "Cannot call flush() from inside onSettled or createTrackedEffect. flush() is not reentrant there. " +
2675
+ "Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush())."
2676
+ );
2677
+ }
2678
+ if (inEffectCallback) {
2679
+ const message =
2680
+ "[FLUSH_IN_EFFECT_CALLBACK] flush() called from inside an effect callback is a no-op: the flush that runs effects is already in progress. " +
2681
+ "Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush()).";
2682
+ reportDiagnostic(
2683
+ emitDiagnostic({
2684
+ code: "FLUSH_IN_EFFECT_CALLBACK",
2685
+ kind: "lifecycle",
2686
+ severity: "warn",
2687
+ message
2688
+ })
2689
+ );
2690
+ }
2691
+ return;
2692
+ }
2693
+ if (halted) return;
2694
+ let count = 0;
2695
+ // Attribution: whether this call drained anything, so `flushEnd` fires once
2696
+ // per real drain and never for a no-op call. The declaration is dead in
2697
+ // prod (its only write is behind true) and rollup drops it.
2698
+ let drained = false;
2699
+ // The drain's opening instant, under the loop's own condition so it fires
2700
+ // exactly when `flushEnd` below will. Outside every try (see the rule in
2701
+ // attribution-hooks.ts).
2702
+ if (attrHooks !== null && scheduled) attrHooks.flushStart();
2703
+ while (scheduled) {
2704
+ if (++count === 1e5) {
2705
+ rethrowCleanupError(true);
2706
+ // Attribution beats a bare guard (#3140): say what kept the loop alive.
2707
+ throw new Error(
2708
+ `Potential Infinite Loop Detected. Kept alive by scheduled work${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
2709
+ );
2710
+ }
2711
+ globalQueue.flush();
2712
+ drained = true;
2713
+ }
2714
+ // Outside every try in this function (see the rule in attribution-hooks.ts):
2715
+ // the drain loop above is the one place all scheduled work funnels through,
2716
+ // so this is the "committed and effects ran" instant for everything the
2717
+ // loop processed.
2718
+ if (drained && attrHooks !== null) attrHooks.flushEnd();
2719
+ // A cleanup that threw during the drain, now that the drain is complete
2720
+ // (owner.ts).
2721
+ rethrowCleanupError();
2584
2722
  }
2585
- /**
2586
- * Returns the currently-tracking observer (the computation that subscribes to
2587
- * reactive reads at this point), or `null` if reads here would be untracked.
2588
- * Used by reactive primitives that need to know whether they're inside a
2589
- * tracking scope. App code rarely needs this — see `getOwner()` for the
2590
- * lifecycle owner instead.
2591
- *
2592
- * @example
2593
- * ```ts
2594
- * // Library predicate: only register a hot-path subscription when the
2595
- * // caller is inside a tracking scope (memo / effect compute / JSX).
2596
- * function trackIfTracked(source: () => unknown) {
2597
- * if (getObserver()) source();
2598
- * }
2599
- * ```
2600
- */
2601
- function getObserver() {
2602
- return tracking ? context : null;
2723
+ function runQueue(queue, type) {
2724
+ for (let i = 0; i < queue.length; i++) queue[i](type);
2603
2725
  }
2726
+
2604
2727
  /**
2605
- * Returns the current reactive **owner** — the lifecycle node that the next
2606
- * `cleanup()` / `onCleanup()` / `createSignal()` etc. will be attached to.
2607
- *
2608
- * Returns `null` if called outside any owner. Capture the owner with
2609
- * `getOwner()` and re-enter it later with `runWithOwner(owner, fn)` to attach
2610
- * disposables created from a callback (event handler, async resolution, etc.)
2611
- * back to a component's lifecycle.
2612
- *
2613
- * @example
2614
- * ```ts
2615
- * function defer<T>(fn: () => T) {
2616
- * const owner = getOwner();
2617
- * queueMicrotask(() => runWithOwner(owner, fn));
2618
- * }
2619
- * ```
2728
+ * Schedule one subscriber to re-run on the next flush: inserted into the
2729
+ * dirty heap with the `_min` cursor pulled down. Tracked effects ride the
2730
+ * heap too — the heap visit is their (empty) compute phase, which hands the
2731
+ * callback to the user queue once the pass has committed (see
2732
+ * GlobalQueue._update, #3291).
2620
2733
  */
2621
- function getOwner() {
2622
- return context;
2734
+ function enqueueSub(node) {
2735
+ if (dirtyQueue._min > node._height) dirtyQueue._min = node._height;
2736
+ insertIntoHeap(node, dirtyQueue);
2623
2737
  }
2624
- /**
2625
- * Low-level: registers `fn` as a disposal callback on the current owner.
2626
- * Most code should use `onCleanup()` from `solid-js`, which adds dev-mode
2627
- * checks. `cleanup()` is the unchecked primitive used by internals; `owner`
2628
- * (internal) targets a specific owner's disposal list instead of the ambient
2629
- * one.
2630
- */
2631
- function cleanup(fn, owner = context) {
2632
- if (!owner) return fn;
2633
- if (!owner._disposal) owner._disposal = fn;
2634
- else if (Array.isArray(owner._disposal)) owner._disposal.push(fn);
2635
- else owner._disposal = [owner._disposal, fn];
2636
- return fn;
2738
+ function actualInsertIntoHeap(n, heap) {
2739
+ const parentHeight =
2740
+ (n._parent?._root ? n._parent._parentComputed?._height : n._parent?._height) ?? -1;
2741
+ if (parentHeight >= n._height) n._height = parentHeight + 1;
2742
+ const height = n._height;
2743
+ const heapAtHeight = heap._heap[height];
2744
+ if (heapAtHeight === undefined) heap._heap[height] = n;
2745
+ else {
2746
+ const tail = heapAtHeight._prevHeap;
2747
+ tail._nextHeap = n;
2748
+ n._prevHeap = tail;
2749
+ heapAtHeight._prevHeap = n;
2750
+ }
2751
+ if (height > heap._max) heap._max = height;
2637
2752
  }
2638
- /**
2639
- * Returns `true` if the owner has been disposed. Pair with a captured owner
2640
- * to bail out of late callbacks whose surrounding component already
2641
- * unmounted.
2642
- *
2643
- * @example
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
- * ```
2653
- */
2654
- function isDisposed(node) {
2655
- return !!(node._flags & REACTIVE_DISPOSED);
2753
+ function insertIntoHeap(n, heap) {
2754
+ let flags = n._flags;
2755
+ // RECOMPUTING refusals are not always losses: a genuinely missed wake (a
2756
+ // write to a link this pass already validated) is latched link-side in
2757
+ // insertSubs as REACTIVE_MISSED_WAKE for recompute's tail (#3037).
2758
+ if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS)) return;
2759
+ if (flags & REACTIVE_CHECK) {
2760
+ n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
2761
+ } else {
2762
+ n._flags = flags | REACTIVE_IN_HEAP;
2763
+ // An unmarked node entering an already-marked heap is marked on the
2764
+ // spot, keeping the markHeap memo valid. `_marked` is only reset by
2765
+ // runHeap, so a write between two mid-tick pulls (read-time markHeap +
2766
+ // updateIfNecessary) would otherwise leave this node unmarked and every
2767
+ // downstream pull stale until the next flush (#2922: the second
2768
+ // `latest()` returned the first write's value). Invalidating the memo
2769
+ // instead re-walked the WHOLE heap on the next pull — with N effects
2770
+ // parked in the heap for a synchronous mount (each row writing a ref
2771
+ // signal its effect subscribes to), mounting N rows was O(N²) (#3350).
2772
+ // markNode's own guard skips an already-DIRTY node.
2773
+ if (heap._marked) markNode(n);
2774
+ }
2775
+ if (!(flags & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(n, heap);
2656
2776
  }
2657
- function disposeRootSelf(self = true) {
2658
- disposeChildren(this, self);
2777
+ function insertIntoHeapHeight(n, heap) {
2778
+ let flags = n._flags;
2779
+ if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_IN_HEAP_HEIGHT)) return;
2780
+ n._flags = flags | REACTIVE_IN_HEAP_HEIGHT;
2781
+ actualInsertIntoHeap(n, heap);
2659
2782
  }
2660
- /**
2661
- * Creates a fresh owner attached as a child of the current owner (or as a
2662
- * detached root if there is none). Used by framework internals to group
2663
- * cleanups; app code should use `createRoot()` (host a reactive scope outside
2664
- * a component) or `runWithOwner()` (re-enter a captured owner).
2665
- *
2666
- * @internal
2667
- */
2668
- function createOwner(options) {
2669
- const parent = context;
2670
- const transparent = options?.transparent ?? false;
2671
- // Prod and observe boilerplates (see core.ts computed()). The observe
2672
- // literal carries the `_name` and `_loc` slots the rendering layer fills
2673
- // with the component label and JSX site (`owner._name = "<App>"`,
2674
- // `owner._loc = "src/App.tsx:3:5"`) — slots, so labelling a root is a
2675
- // plain store rather than a shape fork between labelled and plain roots.
2676
- const owner = {
2677
- id: inheritId(options, transparent, parent),
2678
- _config: transparent ? CONFIG_TRANSPARENT : 0,
2679
- // Present from birth: disposeChildren marks death here on roots too, and
2680
- // a first store of a missing property is a shape transition plus a
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);
2783
+ function deleteFromHeap(n, heap) {
2784
+ const flags = n._flags;
2785
+ if (!(flags & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT))) return;
2786
+ n._flags = flags & -25;
2787
+ const height = n._height;
2788
+ if (n._prevHeap === n) heap._heap[height] = undefined;
2789
+ else {
2790
+ const next = n._nextHeap;
2791
+ const dhh = heap._heap[height];
2792
+ const end = next ?? dhh;
2793
+ if (n === dhh) heap._heap[height] = next;
2794
+ else n._prevHeap._nextHeap = next;
2795
+ end._prevHeap = n._prevHeap;
2708
2796
  }
2709
- assertNotInEffectCallback();
2710
- if (parent) linkChild(parent, owner);
2711
- else registerRoot(owner);
2712
- DEV.hooks.onOwner?.(owner);
2713
- return owner;
2797
+ n._prevHeap = n;
2798
+ n._nextHeap = undefined;
2714
2799
  }
2715
- /**
2716
- * Creates a reactive root — an owner scope with its own `dispose()`. A root
2717
- * created inside an existing owner is owned by it and is disposed when the
2718
- * parent is disposed; call `dispose()` to tear it down earlier. To create a
2719
- * root that outlives its creator, detach explicitly:
2720
- * `runWithOwner(null, () => createRoot(...))`. Pass `id` to seed hydration
2721
- * ids for the tree it owns.
2722
- *
2723
- * `dispose()` tears down every signal, memo, effect, and `onCleanup`
2724
- * registered inside the root.
2725
- *
2726
- * Use this to host long-lived reactive scopes outside of a component (custom
2727
- * controllers, app bootstrapping, tests). Inside a component, prefer
2728
- * letting Solid's component lifecycle own things.
2729
- *
2730
- * @example
2731
- * ```ts
2732
- * // At module level there is no owner, so this root lives until disposed.
2733
- * const dispose = createRoot(dispose => {
2734
- * const [n, setN] = createSignal(0);
2735
- * createEffect(() => n(), value => console.log(value));
2736
- * setInterval(() => setN(x => x + 1), 1000);
2737
- * return dispose;
2738
- * });
2739
- *
2740
- * // Later, to tear everything down:
2741
- * dispose();
2742
- *
2743
- * // Inside an owner (component, effect, another root), detach explicitly
2744
- * // if the root must outlive its creator:
2745
- * const detached = runWithOwner(null, () => createRoot(d => d));
2746
- * ```
2747
- *
2748
- * @description https://v2.solidjs.com/reference/solid-js/advanced/owner-introspection/create-root
2749
- */
2750
- function createRoot(init, options) {
2751
- const owner = createOwner(options);
2752
- // A bound function, not an arrow: an arrow would share this scope, which
2753
- // holds `init`, so keeping `dispose` would keep `init` and all it closes
2754
- // over alive (#4095).
2755
- return runWithOwner(owner, () => init(disposeRootSelf.bind(owner, true)));
2800
+ function markHeap(heap) {
2801
+ if (heap._marked) return;
2802
+ heap._marked = true;
2803
+ for (let i = 0; i <= heap._max; i++) {
2804
+ for (let el = heap._heap[i]; el !== undefined; el = el._nextHeap) {
2805
+ if (el._flags & REACTIVE_IN_HEAP) markNode(el);
2806
+ }
2807
+ }
2808
+ }
2809
+ function markNode(el, newState = REACTIVE_DIRTY) {
2810
+ const flags = el._flags;
2811
+ if ((flags & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= newState) return;
2812
+ el._flags = (flags & -4) | newState;
2813
+ for (let link = el._subs; link !== null; link = link._nextSub) {
2814
+ markNode(link._sub, REACTIVE_CHECK);
2815
+ }
2816
+ }
2817
+ function runHeap(heap, recompute) {
2818
+ heap._marked = false;
2819
+ for (heap._min = 0; heap._min <= heap._max; heap._min++) {
2820
+ let el = heap._heap[heap._min];
2821
+ while (el !== undefined) {
2822
+ if (el._flags & REACTIVE_IN_HEAP) recompute(el);
2823
+ else adjustHeight(el, heap);
2824
+ el = heap._heap[heap._min];
2825
+ }
2826
+ }
2827
+ heap._max = 0;
2828
+ }
2829
+ function adjustHeight(el, heap) {
2830
+ deleteFromHeap(el, heap);
2831
+ let newHeight = el._height;
2832
+ for (let d = el._deps; d; d = d._nextDep) {
2833
+ const dep = d._dep;
2834
+ if (dep._fn && dep._height >= newHeight) newHeight = dep._height + 1;
2835
+ }
2836
+ if (el._height !== newHeight) {
2837
+ el._height = newHeight;
2838
+ for (let s = el._subs; s !== null; s = s._nextSub) insertIntoHeapHeight(s._sub, heap);
2839
+ }
2756
2840
  }
2757
2841
 
2758
2842
  /** The store's shared slot-node release handler (store/store.ts installs it
@@ -2777,8 +2861,10 @@ function unlinkSubs(link) {
2777
2861
  if (nextSub === null) {
2778
2862
  // Store slot nodes dispatch to the ONE shared hook — no per-node
2779
2863
  // unobserved closure, no NodeExtension to hold it.
2864
+ // A user hook that throws is collected like a cleanup (owner.ts): the
2865
+ // unlink and the teardown around it carry on.
2780
2866
  if (dep._config & CONFIG_SLOT_NODE) slotUnobservedHook(dep);
2781
- else dep._x?._unobserved?.();
2867
+ else if (dep._x?._unobserved) runCleanup(dep._x._unobserved);
2782
2868
  // No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
2783
2869
  // A pending node is exempt: its in-flight async work is an observer —
2784
2870
  // tearing down would orphan the work and re-execute it on the next
@@ -3048,10 +3134,12 @@ function stageError(el, error, inPass) {
3048
3134
  // Staged whether or not the flush has joined a hold yet: a pass that runs
3049
3135
  // before its async sibling joins is the seam's to decide, like a value
3050
3136
  // (#4109). A flush that parks nothing publishes it at its commit (S-38).
3137
+ // Not a projection's derive: its readers learn its error through the
3138
+ // family, which the commit's publish never reaches (#4189).
3051
3139
  if (
3052
3140
  !(
3053
3141
  el._config & CONFIG_HELD ||
3054
- (inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running)
3142
+ (inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running && !el.fam)
3055
3143
  ) ||
3056
3144
  passLane
3057
3145
  )
@@ -4043,7 +4131,9 @@ function recompute(el, create = false) {
4043
4131
  // and own nothing to park.
4044
4132
  if (isEffect === EFFECT_TRACKED || el._config & CONFIG_STAGED) {
4045
4133
  // A lazy node only those children read is released when this pass
4046
- // ends, if the pass doesn't read it again (#4142).
4134
+ // ends, if the pass doesn't read it again (#4142). A cleanup or
4135
+ // `unobserved` hook that throws here neither stops the pass nor skips
4136
+ // that drain: its error is rethrown when the flush ends (owner.ts).
4047
4137
  const prev = deferredReleases;
4048
4138
  deferReleases((released = []));
4049
4139
  disposeChildren(el);
@@ -4178,7 +4268,15 @@ function recompute(el, create = false) {
4178
4268
  // (markNode(c) in read()) marks the running node as part of ordinary
4179
4269
  // bookkeeping, and those marks are correctly discarded here.
4180
4270
  missedWake = (el._flags & REACTIVE_MISSED_WAKE) !== 0;
4181
- joined = !!(el._flags & (mount ? REACTIVE_JOINED | REACTIVE_STAGED_READ : REACTIVE_JOINED));
4271
+ // A mount's staged read joins it — except a nested derivation left
4272
+ // pending: no render effect reaches it yet, so it holds nothing and
4273
+ // reaches no boundary (Reader, #4203). A reader that does is caught.
4274
+ joined = !!(
4275
+ el._flags &
4276
+ (mount && (prevLane || isEffect || !(el._statusFlags & STATUS_PENDING))
4277
+ ? REACTIVE_JOINED | REACTIVE_STAGED_READ
4278
+ : REACTIVE_JOINED)
4279
+ );
4182
4280
  // A verdict reader is one for as long as it probes, or reads a memo
4183
4281
  // that does (`read`): a pass that did neither (REACTIVE_PROBED, the same
4184
4282
  // bit) is an ordinary derivation again.
@@ -4637,6 +4735,12 @@ function joinPass(c, el) {
4637
4735
  if (el._statusFlags & STATUS_UNINITIALIZED || c._type !== EFFECT_RENDER)
4638
4736
  joinPassTx(txOf(el), (!c._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c));
4639
4737
  }
4738
+ /** `joinPass` into the hold `t` (a folded guess's: store/optimistic.ts
4739
+ * `foldedRead`, D-34) — a reader is never a render effect there. */
4740
+ function joinPassAt(c, t) {
4741
+ c._flags |= REACTIVE_JOINED;
4742
+ joinPassTx(t, (!c._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c));
4743
+ }
4640
4744
  /** A15's stale reader (shared-hole and reveal corollaries): a render effect
4641
4745
  * reading a node held by a transaction that is not the flush's. It is served
4642
4746
  * the committed value — a pending one's too — publishes mainline, and is
@@ -4938,8 +5042,9 @@ function exitCallback() {
4938
5042
  * its owner (a list pass removing a row, a branch switching away), so its
4939
5043
  * writes are imperative like an effect callback's (#4165). Kept apart from
4940
5044
  * `callbackDepth` because its sites cannot use try/finally (the frame would
4941
- * survive into prod): a throwing cleanup leaves it raised, and dev.ts resets
4942
- * it on the next microtask, which teardown — synchronous — never spans.
5045
+ * survive into prod). A throwing cleanup is caught (owner.ts `runCleanup`);
5046
+ * anything else escaping a teardown leaves it raised, and dev.ts resets it on
5047
+ * the next microtask, which teardown — synchronous — never spans.
4943
5048
  */
4944
5049
  let disposalDepth = 0;
4945
5050
  function enterDisposal() {
@@ -5202,7 +5307,13 @@ function read(el) {
5202
5307
  if (!c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN)
5203
5308
  return el._value;
5204
5309
  // (A plain signal is never uninitialized — the carve-out is `serve`'s.)
5205
- if (observeFlightHook && c._config & CONFIG_VERDICT && stagedScreen(c)) return el._value;
5310
+ // Lane work over a folded lane (D-34), once the frame has a
5311
+ // transaction, reads the screen too.
5312
+ if (
5313
+ ((observeFlightHook && c._config & CONFIG_VERDICT) || (laneStageHook && passLane?.folded)) &&
5314
+ stagedScreen(c)
5315
+ )
5316
+ return el._value;
5206
5317
  stagedRead(c);
5207
5318
  return el._pendingValue;
5208
5319
  }
@@ -5406,7 +5517,10 @@ function serve(el, c) {
5406
5517
  }
5407
5518
  /** A pass read a staging of this flush: it derives from what the flush may
5408
5519
  * yet hold (REACTIVE_STAGED_READ — the seam decides). Lane work too (plan sec. 28, a
5409
- * lane sees the screen plus its own guesses): the staging is the screen if
5520
+ * lane sees the screen plus its own guesses) — a folded lane's only before
5521
+ * the frame has a transaction (after, it reads the screen like a verdict
5522
+ * reader, `stagedScreen` — a list pass never pairs a held length with the
5523
+ * guess's rows, D-34): the staging is the screen if
5410
5524
  * the frame commits — one pass, the common case — and a held write if it
5411
5525
  * parks, which the seam repairs (`stagedReaders`): the pass re-derives on
5412
5526
  * the committed world and its lane's runs wait that round, so the held
@@ -5778,7 +5892,7 @@ export {
5778
5892
  EFFECT_TRACKED as aN,
5779
5893
  heldRunHook as aO,
5780
5894
  unwrapStatusError as aP,
5781
- haltReactivity as aQ,
5895
+ haltReactivityAt as aQ,
5782
5896
  setStrictRead as aR,
5783
5897
  StatusError as aS,
5784
5898
  trimStaleDeps as aT,
@@ -5816,7 +5930,7 @@ export {
5816
5930
  signal as ay,
5817
5931
  setSignal as az,
5818
5932
  CONFIG_AUTHORITATIVE as b,
5819
- isSuppressed as b$,
5933
+ liveRootOwners as b$,
5820
5934
  $REFRESH as b0,
5821
5935
  markRefresh as b1,
5822
5936
  createRoot as b2,
@@ -5828,32 +5942,32 @@ export {
5828
5942
  CONFIG_REDERIVE as b8,
5829
5943
  passTx as b9,
5830
5944
  settlePendingSource as bA,
5831
- notifyEpoch as bB,
5832
- SUPPORTS_PROXY as bC,
5833
- releaseMarksHook as bD,
5834
- installMarkHooks as bE,
5835
- CONFIG_LAGS as bF,
5836
- REACTIVE_REASK as bG,
5837
- heldUnder as bH,
5838
- unhold as bI,
5839
- laneStageHook as bJ,
5840
- REACTIVE_ZOMBIE as bK,
5841
- installBoundaryHooks as bL,
5842
- CAUGHT_REARMED as bM,
5843
- OBSERVE as bN,
5844
- clearSnapshots as bO,
5845
- enforceLoadingBoundary as bP,
5846
- getNextChildId as bQ,
5847
- markSnapshotScope as bR,
5848
- peekNextChildId as bS,
5849
- releaseSnapshotScope as bT,
5850
- resetErrorHalt as bU,
5851
- setConsoleFooter as bV,
5852
- setSnapshotCapture as bW,
5853
- setWriteHook as bX,
5854
- setAttributionHooks as bY,
5855
- liveRootOwners as bZ,
5856
- ownerPath as b_,
5945
+ joinPassAt as bB,
5946
+ notifyEpoch as bC,
5947
+ SUPPORTS_PROXY as bD,
5948
+ releaseMarksHook as bE,
5949
+ installMarkHooks as bF,
5950
+ CONFIG_LAGS as bG,
5951
+ REACTIVE_REASK as bH,
5952
+ haltReactivity as bI,
5953
+ heldUnder as bJ,
5954
+ unhold as bK,
5955
+ laneStageHook as bL,
5956
+ REACTIVE_ZOMBIE as bM,
5957
+ installBoundaryHooks as bN,
5958
+ CAUGHT_REARMED as bO,
5959
+ OBSERVE as bP,
5960
+ clearSnapshots as bQ,
5961
+ enforceLoadingBoundary as bR,
5962
+ getNextChildId as bS,
5963
+ markSnapshotScope as bT,
5964
+ peekNextChildId as bU,
5965
+ releaseSnapshotScope as bV,
5966
+ resetErrorHalt as bW,
5967
+ setConsoleFooter as bX,
5968
+ setSnapshotCapture as bY,
5969
+ setWriteHook as bZ,
5970
+ setAttributionHooks as b_,
5857
5971
  CONFIG_FRESH_READ as ba,
5858
5972
  createOwner as bb,
5859
5973
  writeHook as bc,
@@ -5881,14 +5995,16 @@ export {
5881
5995
  propagateStatus as by,
5882
5996
  forEachDependent as bz,
5883
5997
  CONFIG_OVERRIDE as c,
5884
- blockersOf as c0,
5885
- CONFIG_PLUMBING as c1,
5886
- anyExcluded as c2,
5887
- isExcluded as c3,
5888
- CONFIG_WIDE as c4,
5889
- GRAPH_SIZE_WARN_AT as c5,
5890
- noteFanOut as c6,
5891
- CONFIG_SUPERSEDES as c7,
5998
+ ownerPath as c0,
5999
+ isSuppressed as c1,
6000
+ blockersOf as c2,
6001
+ CONFIG_PLUMBING as c3,
6002
+ anyExcluded as c4,
6003
+ isExcluded as c5,
6004
+ CONFIG_WIDE as c6,
6005
+ GRAPH_SIZE_WARN_AT as c7,
6006
+ noteFanOut as c8,
6007
+ CONFIG_SUPERSEDES as c9,
5892
6008
  newTransaction as d,
5893
6009
  enqueueSub as e,
5894
6010
  CONFIG_VERDICT as f,