@solidjs/signals 2.0.0-rc.15 → 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 (90) hide show
  1. package/dist/dev-shared.js +2268 -1734
  2. package/dist/dev.attribution.js +75 -52
  3. package/dist/dev.js +1348 -658
  4. package/dist/observe/affects-marks.js +86 -0
  5. package/dist/observe/affects.js +22 -73
  6. package/dist/observe/boundaries.js +192 -190
  7. package/dist/observe/core/async.js +177 -131
  8. package/dist/observe/core/attribution-queries.js +1 -1
  9. package/dist/observe/core/attribution.js +176 -159
  10. package/dist/observe/core/constants.js +17 -3
  11. package/dist/observe/core/context.js +5 -3
  12. package/dist/observe/core/core.js +520 -364
  13. package/dist/observe/core/dev.js +66 -38
  14. package/dist/observe/core/effect.js +50 -48
  15. package/dist/observe/core/error.js +5 -3
  16. package/dist/observe/core/external.js +5 -6
  17. package/dist/observe/core/graph.js +60 -44
  18. package/dist/observe/core/heap.js +33 -33
  19. package/dist/observe/core/identities.js +62 -0
  20. package/dist/observe/core/lanes.js +238 -260
  21. package/dist/observe/core/owner.js +117 -79
  22. package/dist/observe/core/scheduler.js +381 -217
  23. package/dist/observe/core/truth.js +45 -0
  24. package/dist/observe/core/verdict.js +91 -50
  25. package/dist/observe/deferred.js +184 -0
  26. package/dist/observe/index.js +7 -1
  27. package/dist/observe/map.js +345 -260
  28. package/dist/observe/reveal.js +76 -76
  29. package/dist/observe/signals.js +47 -31
  30. package/dist/observe/store/affects.js +18 -18
  31. package/dist/observe/store/edges.js +39 -0
  32. package/dist/observe/store/optimistic.js +146 -100
  33. package/dist/observe/store/projection.js +68 -55
  34. package/dist/observe/store/store.js +180 -118
  35. package/dist/prod/affects-marks.js +86 -0
  36. package/dist/prod/affects.js +23 -72
  37. package/dist/prod/boundaries.js +156 -154
  38. package/dist/prod/core/async.js +173 -127
  39. package/dist/prod/core/constants.js +17 -3
  40. package/dist/prod/core/context.js +5 -3
  41. package/dist/prod/core/core.js +495 -343
  42. package/dist/prod/core/effect.js +70 -68
  43. package/dist/prod/core/error.js +5 -3
  44. package/dist/prod/core/external.js +5 -6
  45. package/dist/prod/core/graph.js +60 -44
  46. package/dist/prod/core/heap.js +38 -38
  47. package/dist/prod/core/identities.js +62 -0
  48. package/dist/prod/core/lanes.js +230 -252
  49. package/dist/prod/core/owner.js +114 -77
  50. package/dist/prod/core/scheduler.js +327 -163
  51. package/dist/prod/core/truth.js +45 -0
  52. package/dist/prod/core/verdict.js +90 -49
  53. package/dist/prod/deferred.js +182 -0
  54. package/dist/prod/index.js +7 -1
  55. package/dist/prod/map.js +360 -275
  56. package/dist/prod/reveal.js +76 -76
  57. package/dist/prod/signals.js +42 -26
  58. package/dist/prod/store/affects.js +6 -6
  59. package/dist/prod/store/edges.js +39 -0
  60. package/dist/prod/store/optimistic.js +143 -98
  61. package/dist/prod/store/projection.js +25 -19
  62. package/dist/prod/store/store.js +157 -100
  63. package/dist/types/affects-marks.d.ts +14 -0
  64. package/dist/types/boundaries.d.ts +1 -1
  65. package/dist/types/core/async.d.ts +10 -0
  66. package/dist/types/core/constants.d.ts +13 -1
  67. package/dist/types/core/core.d.ts +54 -5
  68. package/dist/types/core/dev.d.ts +51 -13
  69. package/dist/types/core/error.d.ts +9 -1
  70. package/dist/types/core/graph.d.ts +11 -0
  71. package/dist/types/core/identities.d.ts +17 -0
  72. package/dist/types/core/index.d.ts +2 -1
  73. package/dist/types/core/lanes.d.ts +2 -14
  74. package/dist/types/core/owner.d.ts +9 -3
  75. package/dist/types/core/scheduler.d.ts +70 -32
  76. package/dist/types/core/truth.d.ts +16 -0
  77. package/dist/types/core/types.d.ts +21 -0
  78. package/dist/types/core/verdict.d.ts +32 -0
  79. package/dist/types/deferred.d.ts +28 -0
  80. package/dist/types/index.d.ts +6 -0
  81. package/dist/types/map.d.ts +2 -2
  82. package/dist/types/signals.d.ts +28 -7
  83. package/dist/types/store/edges.d.ts +9 -0
  84. package/dist/types/store/index.d.ts +10 -3
  85. package/dist/types/store/optimistic.d.ts +3 -2
  86. package/dist/types/store/projection.d.ts +4 -3
  87. package/dist/types/store/store.d.ts +1 -1
  88. package/dist/types/store/target.d.ts +16 -8
  89. package/dist/types/store/types.d.ts +14 -0
  90. package/package.json +1 -1
@@ -23,18 +23,18 @@
23
23
  * ```
24
24
  */
25
25
  class NotReadyError extends Error {
26
- source;
27
26
  constructor(source) {
28
27
  super();
29
28
  this.source = source;
30
29
  }
31
30
  }
32
31
  class StatusError extends Error {
33
- source;
34
32
  constructor(source, original) {
35
- super(original instanceof Error ? original.message : String(original), {
36
- cause: original
37
- });
33
+ // Best-effort message: formatting must never replace the original (#4112).
34
+ try {
35
+ var message = String(original instanceof Error ? original.message : original);
36
+ } catch {}
37
+ super(message, { cause: original });
38
38
  this.source = source;
39
39
  }
40
40
  }
@@ -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;
@@ -191,6 +206,9 @@ const CONFIG_SLOT_NODE = 1 << 12;
191
206
  * leaf twin of REACTIVE_MANUAL_WRITE). Set by the user setter, cleared by
192
207
  * the derive's own write to the key. */
193
208
  const CONFIG_MANUAL_WRITE = 1 << 18;
209
+ /** D-41: the staged value in `_pendingValue` is an error — an
210
+ * answer of the held frame, published at its landing like a value. */
211
+ const CONFIG_STAGED_ERROR = 1 << 21;
194
212
  /** Lanes — the node carries a written guess (`optimisticWrite`): its own
195
213
  * source recomputing it, or a plain write landing on it, is the truth
196
214
  * (`supersede`). A lane's derived staging is not. */
@@ -199,15 +217,21 @@ const CONFIG_GUESS = 1 << 13;
199
217
  * pending is a question for its next pass — the verdict changed — so the
200
218
  * propagation re-derives it instead of marking it pending (as a kept-tail
201
219
  * link does, A30). Set at the window, never cleared: a reader that stopped
202
- * asking re-derives once and goes pending through its own read. */
203
- const CONFIG_VERDICT = 1 << 14;
220
+ * asking re-derives once and goes pending through its own read. The same
221
+ * bit as REACTIVE_PROBED: a pass's probe (or a read of a verdict memo,
222
+ * core.ts `read`) carries into the config at its end without a shift. */
223
+ const CONFIG_VERDICT = 1 << 15;
204
224
  /** A dependency's status is a question for this node's pass, not a fact
205
225
  * about its current one: the propagation re-derives it instead of marking
206
226
  * it, for errors as for pending (as CONFIG_VERDICT). A boundary's output
207
227
  * (show the content, or the fallback) and its `on` node (boundaries.ts); a
208
228
  * promise over an expression (`resolve`/`until`, signals.ts: the pass reads
209
229
  * the error and rejects, or re-asks a pending source). */
210
- const CONFIG_REDERIVE = 1 << 15;
230
+ const CONFIG_REDERIVE = 1 << 14;
231
+ /** An answered `createDeferred` node (deferred.ts): held because a
232
+ * downstream hold staged its landing, its re-pass is a lagging question,
233
+ * not an answer, and joins no transaction (D-40). */
234
+ const CONFIG_LAGS = 1 << 19;
211
235
  /** The reader wants the truth, not the lane's guess (`until`, signals.ts):
212
236
  * a read of displayed optimism serves the base the guess covers, makes the
213
237
  * pass no lane's, and the truth landing wakes it even when it confirms the
@@ -259,9 +283,13 @@ const CONFIG_WIDE = 1 << 26;
259
283
  * creation by dev builds only (the observe artifact mangles the option name,
260
284
  * as it does `_wide`). */
261
285
  const CONFIG_SUPERSEDES = 1 << 27;
286
+ const STATUS_NONE = 0;
262
287
  const STATUS_PENDING = 1 << 0;
263
288
  const STATUS_ERROR = 1 << 1;
264
289
  const STATUS_UNINITIALIZED = 1 << 2;
290
+ /** `catchHook`'s answer when the catcher is a boundary re-armed by
291
+ * `on` (truthy, as any catch): what it collects is its own, not the hold's. */
292
+ const CAUGHT_REARMED = 2;
265
293
  const EFFECT_RENDER = 1;
266
294
  const EFFECT_USER = 2;
267
295
  const EFFECT_TRACKED = 3;
@@ -410,13 +438,21 @@ const diagnostics = {
410
438
  };
411
439
  }
412
440
  };
441
+ // Flight identity → earliest declared start (see `AttributionSlot.markFlight`).
442
+ // Weak: a stamp must not keep a settled promise alive.
443
+ const flightOrigins = new WeakMap();
413
444
  const attributionSlot = {
414
445
  get installed() {
415
446
  return attrHooks;
416
447
  },
417
448
  withInteraction,
418
449
  withOrigin,
419
- currentOrigin
450
+ currentOrigin,
451
+ markFlight(flight, startedAt = performance.now()) {
452
+ // Earliest wins (`undefined <= n` is false: the first mark always lands).
453
+ if (!(flightOrigins.get(flight) <= startedAt)) flightOrigins.set(flight, startedAt);
454
+ },
455
+ flightOrigin: flight => flightOrigins.get(flight)
420
456
  };
421
457
  const RECORDS = Symbol.for("@solidjs/signals/observe/records");
422
458
  function recordsChannel() {
@@ -518,10 +554,11 @@ function isSuppressed(entry) {
518
554
  return suppressedEvents.has(entry);
519
555
  }
520
556
  // The repair guide: the `reactivity-diagnostics` skill `solid-js` ships,
521
- // one section per code, at its stable GitHub path. GitHub heading anchors
522
- // are lowercased with underscores kept (`### SILENT_HOLD` → `#silent_hold`).
523
- const GUIDE_URL =
524
- "https://github.com/solidjs/solid/blob/main/packages/solid/skills/reactivity-diagnostics/SKILL.md";
557
+ // one page per code on the docs site. The slug is the code lowercased with
558
+ // `_` → `-` (`STRICT_READ_UNTRACKED` → `strict-read-untracked`). The host
559
+ // flips to docs.solidjs.com at stable (one search-and-replace, with the JSDoc
560
+ // links).
561
+ const GUIDE_URL = "https://v2.solidjs.com/reference/solid-js/diagnostics/";
525
562
  const DEV = {
526
563
  hooks,
527
564
  getChildren,
@@ -531,7 +568,7 @@ const DEV = {
531
568
  getObservers,
532
569
  report: reportDiagnostic,
533
570
  guideUrl(code) {
534
- return `${GUIDE_URL}#${code.toLowerCase()}`;
571
+ return GUIDE_URL + code.toLowerCase().replace(/_/g, "-");
535
572
  }
536
573
  };
537
574
  /**
@@ -570,15 +607,29 @@ function ownerPath(subject) {
570
607
  }
571
608
  return path.length ? path.reverse() : undefined;
572
609
  }
610
+ /**
611
+ * The innermost source location on the subject's chain: the subject's own
612
+ * `_loc` (a signal or computation created with `{ loc }`), else the first
613
+ * `_loc` up the owners — a signal hops to its registering owner, then
614
+ * `_parent` upward (a component root carries its JSX site). The same walk
615
+ * `ownerPath` makes; `undefined` when nothing on the chain was located.
616
+ */
617
+ function locationOf(subject) {
618
+ // Owners have a `_parent` key (null on a root); a signal has none and hops
619
+ // to its registering `_owner` instead.
620
+ for (let node = subject; node; node = node._parent !== undefined ? node._parent : node._owner)
621
+ if (node._loc) return node._loc;
622
+ return undefined;
623
+ }
573
624
  /**
574
625
  * Records a diagnostic on the structured channel (listeners, captures) and
575
626
  * returns the entry. `subject` locates it: the current reactive `context` by
576
627
  * default (right for the synchronous rule checks — they fire inside the
577
628
  * scope that misbehaved); pass the node for scheduler-time findings whose
578
629
  * ambient context is the flush, or `null` for events that have no location
579
- * by nature. An `ownerPath` already on the event wins over the subject walk
580
- * (hosts whose owners are not signals' owners compute their own). Console
581
- * output is a separate, dev-tier step — see `reportDiagnostic`.
630
+ * by nature. An `ownerPath` (or `loc`) already on the event wins over the
631
+ * subject walk (hosts whose owners are not signals' owners compute their
632
+ * own). Console output is a separate, dev-tier step — see `reportDiagnostic`.
582
633
  */
583
634
  function emitDiagnostic(event, subject = context) {
584
635
  const entry = {
@@ -595,6 +646,10 @@ function emitDiagnostic(event, subject = context) {
595
646
  const path = ownerPath(subject);
596
647
  if (path) entry.ownerPath = path;
597
648
  }
649
+ if (entry.loc === undefined) {
650
+ const loc = locationOf(subject);
651
+ if (loc !== undefined) entry.loc = loc;
652
+ }
598
653
  const live = subject ?? undefined;
599
654
  if (live !== undefined) eventSubjects.set(entry, live);
600
655
  for (const listener of diagnosticListeners) listener(entry, live);
@@ -631,12 +686,44 @@ function takeFooter(entry) {
631
686
  * `reportDiagnostic(emitDiagnostic(…))`. Weak, keyed by the entry.
632
687
  */
633
688
  const eventSubjects = new WeakMap();
689
+ /**
690
+ * The `at` line of a console report: the event's `loc` as the compiler wrote
691
+ * it (`src/App.tsx:18:3`), or — in a browser, where a dev server serves the
692
+ * project root at `location.origin` — `http://localhost:5173/src/App.tsx:18:3`,
693
+ * which Chrome and Firefox link to the Sources panel. A path that leaves the
694
+ * root (`../`) or is absolute is not under the origin and prints as-is.
695
+ */
696
+ function locationLine(loc) {
697
+ if (
698
+ typeof document !== "undefined" &&
699
+ typeof location !== "undefined" &&
700
+ !loc.startsWith("..") &&
701
+ !loc.startsWith("/")
702
+ )
703
+ return `${location.origin}/${loc}`;
704
+ return loc;
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
+ }
634
719
  /**
635
720
  * The console face of a diagnostic — ONE entry per finding: the message, the
636
721
  * owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
637
- * once-per-code footer as trailing lines, and — when the subject is a
638
- * binding effect the rendering runtime tagged — the element it writes, as a
639
- * second console argument (hover highlights it, click jumps to Elements).
722
+ * source location (`at src/App.tsx:18:3` — see `locationLine`) when a node on
723
+ * the chain was compiled with one, the once-per-code footer as trailing
724
+ * lines, and — when the subject is a binding effect the rendering runtime
725
+ * tagged — the element it writes, as a second console argument (hover
726
+ * highlights it, click jumps to Elements).
640
727
  * Severity picks the console method. Call sites report the entry
641
728
  * `emitDiagnostic` returned so the structured and console channels never
642
729
  * disagree. Dev-tier: in an observe build this is a no-op, so wiring paths
@@ -645,8 +732,7 @@ const eventSubjects = new WeakMap();
645
732
  */
646
733
  function reportDiagnostic(entry) {
647
734
  if (suppressedEvents.has(entry)) return;
648
- let text = entry.message;
649
- if (entry.ownerPath) text += `\n in ${entry.ownerPath.join(" › ")}`;
735
+ let text = entry.message + locationLines(entry);
650
736
  const footer = takeFooter(entry);
651
737
  if (footer) text += `\n${footer}`;
652
738
  const element = eventSubjects.get(entry)?._devElement;
@@ -929,9 +1015,10 @@ function checkPostAwaitRead(dep, holder, key, nodeName, throwsPending) {
929
1015
  if (callbackDepth !== 0) return;
930
1016
  if (disposalDepth !== 0) {
931
1017
  // Teardown is synchronous, so a depth still raised on the next microtask
932
- // was left behind by a cleanup that threw (owner.ts brackets it without
933
- // try/finally, which would survive into prod). Clear it then, or every
934
- // 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.
935
1022
  if (!disposalHealQueued) {
936
1023
  disposalHealQueued = true;
937
1024
  queueMicrotask(healDisposalDepth);
@@ -1052,1514 +1139,1704 @@ function devCheckQuiescent(isQueuedForCommit) {
1052
1139
  return;
1053
1140
  }
1054
1141
 
1055
- /** Live transactions — opened, unmerged, not landed. Scanned at every seam
1056
- * while non-empty; nothing on the plain path. */
1057
- const transactions = [];
1058
- /** Observe tier: this flush parked its transaction (`holdStart` fired). */
1059
- let holding = false;
1060
- /** A transaction (listed here) or a lane (lanes.ts lists its own). */
1061
- function newTransaction(lane, parent = null) {
1062
- const t = {
1063
- _nodes: [],
1064
- _queues: [[], []],
1065
- _reruns: [],
1066
- _into: null,
1067
- _parent: parent,
1068
- _lane: lane,
1069
- _verdict: null,
1070
- _open: 0,
1071
- _acted: false,
1072
- _links: null,
1073
- _shown: false,
1074
- _held: false,
1075
- _marks: null
1076
- };
1077
- if (!lane) transactions.push(t);
1078
- return t;
1079
- }
1080
- // Lanes (plan sec. 28, 2026-10-02; lanes.ts). A lane is "a new base of a transition":
1081
- // the sub-frame an optimistic write opens in the transition of the frame it
1082
- // is made in. It sees the screen plus its own guesses (plan sec. 19), breaks out of
1083
- // the parent's hold — its effects run now — and holds itself if its own
1084
- // derivations hit async. It ends when the parent lands: a guess reverts to
1085
- // the truth it covered, or lands as the truth that superseded it. A frame
1086
- // that does not park has nothing to be optimistic over: the write is void.
1087
- //
1088
- // The seat of a pass is its node's: a node carrying a lane's value runs as
1089
- // the lane's whoever dirtied it; a derivation's tracked read of a lane's
1090
- // value moves its pass into the lane; a leaf's never does (it reads the
1091
- // screen). Lane work's values go to the lane slot, its runs to the lane's
1092
- // queues; the rest of the frame is the flush's.
1093
- /** The lane the running pass is work of; set at `recompute`'s head from the
1094
- * node, moved by a derivation's lane read, saved and restored around the
1095
- * pass. */
1096
- let passLane = null;
1097
- function setPassLane(l) {
1098
- passLane = l;
1099
- }
1100
- /** The transaction this flush parks into: set by the first join, merged
1101
- * with every further one. Consumed at the seam. A join outside a flush (a
1102
- * mount's pending observer, a write to a held node, an action opening)
1103
- * schedules the seam that consumes it: membership is the tick's — the same
1104
- * synchronous frame — and the next tick's writes are not this one's. */
1105
- let flushTransaction = null;
1106
- /** The transaction the passes running OUTSIDE a flush this tick derive from
1107
- * (a mainline mount, a mainline re-pass): the passes' own (A29, creation-
1108
- * time form — "the entry is the pass's alone"): their results are staged
1109
- * into it, and nothing else in the tick is — a write made after the mount
1110
- * is a mainline write, a render effect mounted beside it a stale reader.
1111
- * Inside a flush, or in a tick that already has its transaction (an
1112
- * action's body), the frame joins instead (`flushTransaction`) — except a
1113
- * first pass a loading boundary that has not shown content catches (`own`,
1114
- * A29's boundary exemption, #3540): it is the boundary's, not the tick's,
1115
- * and a flush that has joined nothing keeps it pass-scoped as outside one.
1116
- * Cleared by the flush the join schedules, and at the end of a flush that
1117
- * set it. */
1118
- let passTx = null;
1119
- function joinPassTx(t, own) {
1120
- if ((globalQueue._running && !own) || flushTransaction !== null) return joinFuture(t);
1121
- if (passTx === null) passTx = resolveTx(t);
1122
- else merge(resolveTx(t), passTx);
1123
- schedule();
1124
- }
1125
- /** The transaction holding a node (CONFIG_HELD set), resolved through
1126
- * merges; the path is compressed. */
1127
- function txOf(n) {
1128
- return (n._x._transaction = resolveTx(n._x._transaction));
1129
- }
1130
- /** `t` resolved through merges while it is still parked or open; null once
1131
- * it has landed. */
1132
- function liveTx(t) {
1133
- t = resolveTx(t);
1134
- return transactions.indexOf(t) !== -1 ? t : null;
1135
- }
1136
- function resolveTx(t) {
1137
- while (t._into !== null) t = t._into;
1138
- return t;
1139
- }
1140
- /** The flush joins transaction `t` — or needs one of its own (`null`: a pass
1141
- * went pending with nothing held). Joining a second one merges it into the
1142
- * flush's: the work that touched both entangles them (A15). */
1143
- function joinFuture(t) {
1144
- const f = flushTransaction;
1145
- if (f === null) {
1146
- flushTransaction = t === null ? newTransaction(false) : resolveTx(t);
1147
- } else if (t !== null) merge(resolveTx(t), f);
1148
- if (!globalQueue._running) schedule();
1149
- }
1150
- /** `t` merges into `f`: the work that touched both entangles them (A15).
1151
- * A lane never merges: it is a sub-frame, not a peer — its work is routed
1152
- * per pass (`passLane`). */
1153
- function merge(t, f) {
1154
- // Both ends resolved: a stale pointer to a transaction already merged
1155
- // into the other would close a cycle `resolveTx` never leaves.
1156
- t = resolveTx(t);
1157
- f = resolveTx(f);
1158
- if (t === f || t._lane || f._lane) return;
1159
- if (attrHooks !== null) attrHooks.transitionMerged(f, t);
1160
- t._into = f;
1161
- f._open += t._open;
1162
- f._acted ||= t._acted;
1163
- append(f._nodes, t._nodes);
1164
- append(f._queues[0], t._queues[0]);
1165
- append(f._queues[1], t._queues[1]);
1166
- append(f._reruns, t._reruns);
1167
- if (t._marks !== null) append((f._marks ??= []), t._marks);
1168
- transactions.splice(transactions.indexOf(t), 1);
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
+ }
1169
1156
  }
1170
- /** A15: a hold is a property of the async node — observed pending by a
1171
- * render reader. A transaction is blocked while a node it holds, in flight,
1172
- * has one: a pending render effect (the frame itself), or a pending held
1173
- * node a render effect reads. A stale reader served the committed value
1174
- * never went pending, but derives from the flight all the same (#3494) and
1175
- * is still linked to it; a memo between them is pending, and held, too — so
1176
- * one hop of `_subs` is the chain. Reads of the reader's LAST pass only (the
1177
- * link's generation is the pass's): a tail kept for A30 — the committed
1178
- * frame's dependency, awaiting the run that retires it — is not a read of
1179
- * the flight, and a reader that stopped reading releases it (O3, #3494).
1180
- * An errored pass (NotReady included) did not stop: it never got there, and
1181
- * its full list stands (A30) — the reads it did not reach still observe. A
1182
- * flight nobody renders holds nothing (A29). Only the nodes `t` still owns
1183
- * count: one a lane took over since is the lane's to wait on. A guess whose
1184
- * own truth is in flight blocks the lane's parent, not the lane — it stands
1185
- * in for the flight (A17: "visible until its own fetch settles"). */
1186
- function blocked(t) {
1187
- // The transaction being judged (a lane consulted on its behalf judges for
1188
- // it): a zombie whose removal it stages is moot for it alone (`onScreen`).
1189
- const prev = judge;
1190
- judge ??= t;
1191
- // An action still running in it holds it open (action.ts). Its lanes
1192
- // (lanes.ts): a lane under `t` blocks it while blocked itself.
1193
- const r =
1194
- t._open !== 0 ||
1195
- blockedBy(t._nodes, t) ||
1196
- (!!GlobalQueue._lanesBlocked && GlobalQueue._lanesBlocked(t));
1197
- judge = prev;
1198
- return r;
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);
1199
1166
  }
1200
- let judge = null;
1201
- /** Observe tier: the flights `t` waits on — its pending held sources a
1202
- * frame reader observes (`blockedBy`'s predicate, collected; the readers
1203
- * themselves are not sources). */
1204
- function blockersOf(t) {
1205
- const out = [];
1206
- for (let i = 0; i < t._nodes.length; i++) {
1207
- const n = t._nodes[i];
1208
- if (
1209
- !n._type &&
1210
- n._statusFlags & STATUS_PENDING &&
1211
- !(n._flags & REACTIVE_DISPOSED) &&
1212
- (n._x._transaction === null || txOf(n) === t) &&
1213
- blockedBy([n], t)
1214
- )
1215
- out.push(n);
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();
1216
1182
  }
1217
- return out;
1218
1183
  }
1219
- /** `t`'s own flights only — not its lanes': a flight the frame asked for
1220
- * (a refetch, a plain load) is authoritative; a lane's derivation flight is
1221
- * not (lanes.ts, the body-end corollary). A pending reader counts only if
1222
- * one of the sources it waits on is `t`'s (a render effect marked pending
1223
- * by a lane flight, never re-run, is the lane's wait, not the frame's). */
1224
- function ownFlights(t) {
1225
- return blockedBy(t._nodes, t, true);
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];
1226
1189
  }
1227
- /** A source the pending node waits on that is `t`'s (held by it, or by no
1228
- * one) — not another transaction's or lane's work. Itself, when it is the
1229
- * source. */
1230
- function ownSource(n, t) {
1231
- const sources = n._x?._pendingSources;
1232
- if (sources === undefined) return true;
1233
- for (const s of sources) {
1234
- const u = s._x?._transaction;
1235
- if (u == null || resolveTx(u) === t) return true;
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);
1236
1208
  }
1237
- return false;
1238
- }
1239
- function blockedBy(nodes, owner, own = false) {
1240
- for (let i = 0; i < nodes.length; i++) {
1241
- const n = nodes[i];
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.
1242
1216
  if (
1243
- !(n._statusFlags & STATUS_PENDING) ||
1244
- n._flags & REACTIVE_DISPOSED ||
1245
- n._config & CONFIG_GUESS ||
1246
- (n._x._transaction !== null && txOf(n) !== owner) ||
1247
- (own && !ownSource(n, owner))
1217
+ (node._statusFlags & STATUS_PENDING && node._x?._transaction != null) ||
1218
+ flags & REACTIVE_FRAME_READ
1248
1219
  )
1249
- continue;
1250
- // The frame's own pending observer (off screen: this one holds nothing;
1251
- // the next listed node may).
1252
- if (n._type === EFFECT_RENDER) {
1253
- if (onScreen(n, judge ?? owner)) return true;
1254
- continue;
1255
- }
1256
- for (let s = n._subs; s !== null; s = s._nextSub) {
1257
- const r = s._sub;
1258
- // A frame reader: a render effect, or a stale reader of the flight
1259
- // (REACTIVE_FRAME_READ — a verdict reader, `observeFlight`; lane work,
1260
- // `frameRead`): served committed instead of going pending, it derives
1261
- // from the flight all the same and is re-derived at the landing — the
1262
- // frame's observation survives its reader becoming a lane's (a guess
1263
- // over a held window, V5/A17). A probe alone does not hold. A
1264
- // boundary showing content forwards the pending (its output pending,
1265
- // CONFIG_REDERIVE): transparent, its readers hold through it (A33; the
1266
- // lane-membership ruling, 2026-10-05). An output that never committed
1267
- // shows nothing — a mount the frame has not revealed — and holds
1268
- // nothing.
1269
- if (
1270
- ((r._type === EFFECT_RENDER && r._statusFlags & STATUS_PENDING) ||
1271
- r._flags & REACTIVE_FRAME_READ ||
1272
- (r._config & CONFIG_REDERIVE && r._statusFlags === STATUS_PENDING)) &&
1273
- (s._gen === r._depGen || r._x?._error != null) &&
1274
- !(r._flags & REACTIVE_DISPOSED) &&
1275
- onScreen(r, judge ?? owner)
1276
- )
1277
- return true;
1278
- }
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();
1279
1293
  }
1280
- return false;
1281
- }
1282
- /** A frame reader whose say counts for `t`'s hold — `t` the transaction
1283
- * being judged: not a zombie whose removal `t` stages (A15 #3463: a re-ask
1284
- * in the unmount frame lands its pending on the zombie it unmounts — the
1285
- * compiled <Show> of #3372; a zombie blocks every other judgment while it
1286
- * is visible, a lane's reveal included: only the commit that disposes it
1287
- * makes its say moot), and not behind a fallback (A33: a boundary showing its fallback is the display of
1288
- * everything under it, so a reader there is not on screen and holds
1289
- * nothing — boundaries.ts). */
1290
- function onScreen(r, t) {
1291
- return (
1292
- !(r._flags & REACTIVE_ZOMBIE && removalStagedBy(r, t)) &&
1293
- !(GlobalQueue._hidden && GlobalQueue._hidden(r))
1294
- );
1295
- }
1296
- /** A15 (#3463): a reader whose removal is staged — a zombie, parked by a
1297
- * pass awaiting its commit — is still on screen and live for every hold,
1298
- * except the transaction staging its removal: for that one its say is moot
1299
- * (done, and the commit disposes it; not done, and it stays parked). The
1300
- * staging transaction is the one holding the pass that parked the frame:
1301
- * the nearest non-zombie ancestor. */
1302
- function removalStagedBy(r, t) {
1303
- let o = r;
1304
- while (o !== null && o._flags & REACTIVE_ZOMBIE) o = o._parent;
1305
- return (
1306
- o !== null &&
1307
- (o._config & CONFIG_HELD) !== 0 &&
1308
- o._x?._transaction != null &&
1309
- txOf(o) === resolveTx(t)
1310
- );
1311
- }
1312
- /** Unchanged passes this flush whose stale dependency tail awaits the
1313
- * flush's verdict (A30, #3469): trimmed when the flush commits, kept when
1314
- * it parks — the committed frame still derives from the previous pass's
1315
- * dependencies, and a write to one of them must reach the node until a
1316
- * committing pass replaces it. */
1317
- const heldTrims = [];
1318
- function heldTrim(el) {
1319
- heldTrims.push(el);
1320
- }
1321
- /** Zombies popped from the dirty heap this flush (`deferZombie`). The seam
1322
- * decides their fate: a park cancels them (the writes that dirtied them are
1323
- * held with it); a commit runs the survivors — the ones whose owner is still
1324
- * awaiting its commit — in height order, after the commits that would have
1325
- * disposed them (#3546). No second heap: the list is the deferral. */
1326
- const deferredZombies = [];
1327
- function deferZombie(el) {
1328
- deleteFromHeap(el, dirtyQueue);
1329
- el._flags &= -4;
1330
- deferredZombies.push(el);
1331
- }
1332
- /** Put a staged node in a transaction: flagged, stamped, on its landing
1333
- * list. */
1334
- function holdNode(n, t) {
1335
- n._config |= CONFIG_HELD;
1336
- list(n, t);
1337
1294
  }
1338
- /** Put a node on a transaction's list, once: a node's `_transaction` is the
1339
- * transaction whose `_nodes` it is on (every site that sets one does the
1340
- * other), so a re-pass, a re-landing or a re-staging adds nothing. */
1341
- function list(n, t) {
1342
- const x = ext(n);
1343
- if (x._transaction === t) return;
1344
- x._transaction = t;
1345
- t._nodes.push(n);
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;
1346
1301
  }
1347
- /** Hold the (uncommitted) frame under a held pass: every computed in the
1348
- * live child chain, recursively. Plain owners are walked through, not held
1349
- * — nothing reads or re-passes them. Once per park, over the parked frames
1350
- * only; recomputes pay nothing for it. A lane's node in the frame (listed on
1351
- * a lane: a verdict reader broken out of this very hold — display-ahead — or
1352
- * a derivation of a guess, CONFIG_OVERRIDE; a lane pass's effect) is the
1353
- * lane's, as the seam's own loop has it: "lane work never makes its node
1354
- * transaction work" (A15, #3698). Re-listed here it would carry the lane's
1355
- * value on the transaction's list, where the landing nulls its transaction
1356
- * and `dissolveLane` no longer finds it — a lane node with no lane, which the
1357
- * next read of it trips over (`laneRead` → `txOf`). Its frame is the lane's
1358
- * too (ruling A: a lane pass's children are the lane's), revealed and
1359
- * retired at the lane's seam. */
1360
- function holdFrame(owner, t) {
1361
- for (let c = owner._firstChild; c !== null; c = c._nextSibling) {
1362
- if (c._x?._transaction?._lane) continue;
1363
- if (c._fn !== undefined) {
1364
- if (!(c._config & CONFIG_HELD)) holdNode(c, t);
1365
- c._config |= CONFIG_HELD_FRAME;
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]);
1366
1322
  }
1367
- holdFrame(c, t);
1323
+ } else {
1324
+ runCleanup(disposal);
1368
1325
  }
1326
+ exitDisposal();
1369
1327
  }
1370
- const dirtyQueue = {
1371
- _heap: new Array(2000).fill(undefined),
1372
- _marked: false,
1373
- _min: 0,
1374
- _max: 0
1375
- };
1376
- let clock = 0;
1377
- let scheduled = false;
1378
- let halted = false;
1379
- let haltNotified = false;
1380
- let syncDepth = 0;
1381
- let inTrackedQueueCallback = false;
1382
- let _enforceLoadingBoundary = false;
1383
- let _hitUnhandledAsync = false;
1384
- // Once per enforcement window: the ASYNC_OUTSIDE_LOADING_BOUNDARY finding is a
1385
- // fact about the MOUNT ("the root mount will be deferred"), not about each
1386
- // pending render effect — N async siblings at mount used to produce N copies.
1387
- let _reportedUnhandledAsync = false;
1388
- /**
1389
- * Consume the unhandled-async hit. Returns whether this is the first report
1390
- * of the current enforcement window — the caller warns only then.
1391
- */
1392
- function resetUnhandledAsync() {
1393
- _hitUnhandledAsync = false;
1394
- if (_reportedUnhandledAsync) return false;
1395
- _reportedUnhandledAsync = true;
1396
- return true;
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");
1397
1334
  }
1398
1335
  /**
1399
- * Toggles the dev-mode "must be inside a `<Loading>` boundary" enforcement
1400
- * window. Only `render()` calls this — wrapping the initial mount so that a
1401
- * top-level uncaught async read surfaces the diagnostic. Not part of the
1402
- * user-facing API.
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.
1403
1338
  *
1404
1339
  * @internal
1405
1340
  */
1406
- function enforceLoadingBoundary(enabled) {
1407
- _enforceLoadingBoundary = enabled;
1408
- if (enabled) _reportedUnhandledAsync = false;
1409
- }
1410
- function setTrackedQueueCallback(value) {
1411
- inTrackedQueueCallback = value;
1341
+ function getNextChildId(owner) {
1342
+ return childId(owner, true);
1412
1343
  }
1413
- // Dev-only marker for the effect half of createEffect/createRenderEffect, so
1414
- // flush() can report the no-op instead of failing silently (React parity).
1415
- /** Action bodies on the stack (action.ts): `flush()` is refused inside one —
1416
- * the body's writes are its transaction's and commit when it settles; a
1417
- * drain mid-step cannot reveal them, and dev says so. */
1418
- let actionDepth = 0;
1419
- function enterAction() {
1420
- actionDepth++;
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
+ );
1421
1354
  }
1422
- function exitAction() {
1423
- actionDepth--;
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);
1424
1363
  }
1425
- /** Provenance (A18, #3331): the question being asked — an action's
1426
- * sequence while a slice of its body and the flush that carries it run, 0
1427
- * otherwise. A guess is stamped with it (lanes.ts; a mainline guess takes a
1428
- * fresh sequence: the newest intent), a flight with the question that
1429
- * started it (async.ts; mainline is always the current question). An
1430
- * answer from an older question than the guess it lands on is stale: held
1431
- * silently, it does not move the graph. */
1432
- let question = 0;
1433
- let questions = 0;
1434
- function setQuestion(q) {
1435
- question = q;
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;
1436
1368
  }
1437
- function nextQuestion() {
1438
- return ++questions;
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;
1439
1387
  }
1440
- const MAINLINE_QUESTION = 0x7fffffff;
1441
- let inEffectCallback = false;
1442
- function setEffectCallback(value) {
1443
- inEffectCallback = value;
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;
1444
1407
  }
1445
- function schedule() {
1446
- if (halted) {
1447
- notifyHalted();
1448
- return;
1449
- }
1450
- if (scheduled) return;
1451
- scheduled = true;
1452
- if (!syncDepth && !globalQueue._running) {
1453
- queueMicrotask(flush);
1454
- }
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;
1455
1421
  }
1456
1422
  /**
1457
- * Permanently halts the reactive system. Called when a user error escapes
1458
- * every boundary — app state is undefined at that point, so scheduling stops
1459
- * entirely rather than limping along with a half-applied update.
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
+ * ```
1460
1437
  */
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();
1446
+ }
1461
1447
  /**
1462
- * The key a root owner carries its client error hook under (`render`'s
1463
- * `onError`) — registered, so a runtime writes it with no import of the hook
1464
- * module (core/error-hooks.ts) and no property mangling in the way. Defined
1465
- * HERE, not there: a runtime that only writes the key must not retain the
1466
- * hook machinery (pay-for-use).
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).
1452
+ *
1453
+ * @internal
1467
1454
  */
1468
- const ROOT_ERROR_HOOK = Symbol.for("solid-js/root-error-hook");
1469
- function haltReactivity(cause) {
1470
- if (halted) return;
1471
- halted = true;
1472
- let message = "[REACTIVITY_HALTED]";
1473
- {
1474
- message +=
1475
- " An uncaught error halted the reactive system. No further updates will be processed. Handle errors with <Errored> or treat this as a crash.";
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) {
1476
1486
  emitDiagnostic({
1477
- code: "REACTIVITY_HALTED",
1478
- kind: "error",
1487
+ code: "PRIMITIVE_IN_FORBIDDEN_SCOPE",
1488
+ kind: "lifecycle",
1479
1489
  severity: "error",
1480
- message
1490
+ message: PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE,
1491
+ ownerId: parent.id,
1492
+ ownerName: parent._name
1481
1493
  });
1494
+ throw new Error(PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE);
1482
1495
  }
1483
- // Surface the cause here too: callers rethrow it, but a creation-time throw
1484
- // unwinds through ancestor recomputes that convert it to status instead of
1485
- // surfacing it (#2884), so the rethrow alone cannot guarantee visibility.
1486
- // Where the platform has one, hand the cause to its uncaught-error channel
1487
- // (`reportError` → `error` event → window.onerror / error monitoring): a
1488
- // halt that only reaches console.error leaves a page that LOOKS alive with
1489
- // nothing an app or its telemetry can act on (#3338 — an uncaught throw
1490
- // during the hydration render). The rethrow may reach the top as well in
1491
- // the non-swallowed cases; a duplicate report beats a silent one.
1492
- const report = cause !== undefined && globalThis.reportError;
1493
- report || cause === undefined ? console.error(message) : console.error(message, cause);
1494
- report && report(cause);
1496
+ assertNotInEffectCallback();
1497
+ if (parent) linkChild(parent, owner);
1498
+ else registerRoot(owner);
1499
+ DEV.hooks.onOwner?.(owner);
1500
+ return owner;
1495
1501
  }
1496
- // Logs on the first write after a halt so a frozen interaction is traceable.
1497
- function notifyHalted() {
1498
- if (haltNotified) return;
1499
- haltNotified = true;
1500
- console.error(
1501
- "[REACTIVITY_HALTED] Update ignored: the reactive system was halted by an earlier uncaught error."
1502
- );
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)));
1503
1543
  }
1504
- /** @internal Test/dev-reload hook. Revives scheduling after a halt. */
1505
- function resetErrorHalt() {
1506
- halted = false;
1507
- haltNotified = false;
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;
1581
+ }
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;
1606
+ }
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;
1618
+ }
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;
1626
+ }
1627
+ /** Verdicts (verdict.ts). */
1628
+ let observeFlightHook = null;
1629
+ function installVerdictHooks(observeFlight) {
1630
+ observeFlightHook = observeFlight;
1631
+ }
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;
1508
1639
  }
1509
- class GlobalQueue {
1510
- _queues = [[], []];
1511
- _running = false;
1512
- flush() {
1513
- if (this._running) return;
1514
- this._running = true;
1515
- // A28: companions of nodes written since the last flush mirror the
1516
- // flushed world — re-synced inside the running window (the write is
1517
- // "flushed" from here on).
1518
- resyncUnflushedCompanions();
1519
- passLane = null;
1520
- passTx = null;
1521
- try {
1522
- // Sweep before the heap: unobserved() pulls swept nodes out of it, so
1523
- // a dormant memo dirtied in the same tick is reclaimed instead of
1524
- // recomputed; late subscribers (an effect reading a swept memo this
1525
- // flush) revive it, which is the pay-for-use contract.
1526
- sweepDormant();
1527
- runHeap(dirtyQueue, GlobalQueue._update);
1528
- this.settle();
1529
- clock++;
1530
- // A write the commit sweep staged with no subscriber to dirty is work
1531
- // too — the next round commits it.
1532
- scheduled = dirtyQueue._max >= dirtyQueue._min || pendingNodes.length !== 0;
1533
- this.run(EFFECT_RENDER);
1534
- this.run(EFFECT_USER);
1535
- if (true && holding) {
1536
- holding = false;
1537
- attrHooks.holdEnd();
1538
- }
1539
- if (true && !scheduled) {
1540
- // Fully drained: no staged value may survive this point unqueued or
1541
- // unheld.
1542
- devCheckQuiescent(
1543
- n => pendingNodes.includes(n) || (n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) !== 0
1544
- );
1545
- }
1546
- if (true) DEV.hooks.onUpdate?.();
1547
- } finally {
1548
- this._running = false;
1549
- passTx = null;
1550
- }
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);
1551
1798
  }
1552
- /** L2 — the seam: end of the pure phase. Commit this flush's staged nodes,
1553
- * or park them with the transaction the flush joined; then land every
1554
- * transaction no frame is waiting on. The landing flush is an ordinary
1555
- * flush: the settle walk (async.ts) re-ran the parked readers in its pure
1556
- * phase, and here no flight the transaction holds has a frame deriving
1557
- * from it (`blocked`). */
1558
- settle() {
1559
- // Lanes: an ended action body judges its guesses now that their passes
1560
- // have run (lanes.ts); a correction's re-derivations run in one more
1561
- // pure round — the parent's flights before the parent is judged below.
1562
- if (GlobalQueue._laneCorrections?.()) runHeap(dirtyQueue, GlobalQueue._update);
1563
- const joined = flushTransaction;
1564
- flushTransaction = null;
1565
- // The frame's verdict is in: the guesses written since the last seam
1566
- // open their lane under the frame's transaction (a lane's own flush
1567
- // parents a nested lane to it), or are dropped — the frame commits, and
1568
- // there is nothing to be optimistic over. Their passes run next round;
1569
- // a parent landing at this very seam ends the lane before they do.
1570
- GlobalQueue._applyGuesses?.(joined);
1571
- // A lane's own flush (its flight landing, its reveal) parks nothing: the
1572
- // lane's work reached it per pass; the frame's plain writes commit.
1573
- // (Resolved: a guess over another transaction's guess merged them.)
1574
- const t = joined !== null && !joined._lane ? resolveTx(joined) : null;
1575
- if (t !== null) {
1576
- // `t._nodes` grows under the walk (holdFrame pushes); the bound is
1577
- // this flush's list, which does not.
1578
- const count = pendingNodes.length;
1579
- for (let i = 0; i < count; i++) {
1580
- const n = pendingNodes[i];
1581
- // Already held: the pass that re-staged it joined its transaction,
1582
- // which is `t` (or merged into it). Lane work is the lane's (a
1583
- // pending mark propagated onto it queued it here too).
1584
- if (
1585
- !(n._config & (CONFIG_HELD | CONFIG_OVERRIDE)) &&
1586
- (n._x === null || n._x._transaction === null || !n._x._transaction._lane)
1587
- ) {
1588
- // A loading source — pending with nothing staged and no committed
1589
- // value (A16, A19 exc. 1: uninitialized is loading, not pending)
1590
- // that is not an observer — is nobody's frame: not held. Its
1591
- // first landing is commit #0, not a resumption of this frame, and
1592
- // a write that reaches it later overlaps nothing (#2937: the
1593
- // stale stamp was the bridge that entangled unrelated updates
1594
- // with a never-resolving flight). Its observers are held and
1595
- // wait on its flight (`blocked`).
1596
- if (
1597
- n._pendingValue === NOT_PENDING &&
1598
- n._statusFlags & STATUS_UNINITIALIZED &&
1599
- n._type !== EFFECT_RENDER
1600
- )
1601
- continue;
1602
- // A34 (2): a tick whose writes net to the committed value made no
1603
- // proposal (`setShow(false); setShow(true)` on a committed true) —
1604
- // not staged, not held, pends nothing; a later write to it is
1605
- // plain mainline. (A node already held stays held when written
1606
- // back: A34 (1).)
1607
- if (
1608
- n._pendingValue !== NOT_PENDING &&
1609
- !(n._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED)) &&
1610
- n._equals &&
1611
- n._equals(n._value, n._pendingValue) &&
1612
- n._x?._pendingFirstChild == null &&
1613
- n._x?._pendingDisposal == null
1614
- ) {
1615
- n._pendingValue = NOT_PENDING;
1616
- n._config &= ~CONFIG_STAGED;
1617
- continue;
1618
- }
1619
- holdNode(n, t);
1620
- }
1621
- // A held pass's frame is held with it (ruling A, O2): the children it
1622
- // created are the transaction's — they compute, and a pass over them
1623
- // or a write that reaches them joins; they effect at the landing.
1624
- if (n._config & CONFIG_STAGED) holdFrame(n, t);
1625
- }
1626
- pendingNodes.length = 0;
1627
- GlobalQueue._storePark?.(t);
1628
- // (This flush's runs are stashed with `t` below — after the landings,
1629
- // so a `t` that lands at this very seam runs them first, ahead of
1630
- // the runs it held from earlier flushes.)
1631
- // The batch joins: the writes that dirtied these are held with it — a
1632
- // zombie never displays that world. A verdict reader among them does
1633
- // (#3444: `latest()` inside a branch a held Show is removing peers
1634
- // through like the same read outside): it re-runs now, as the
1635
- // holder's verdict lane's work — before the lane seams, which reveal
1636
- // it (nothing commits this seam that could dispose it).
1637
- const zombies = deferredZombies.splice(0).sort((a, b) => a._height - b._height);
1638
- for (let i = 0; i < zombies.length; i++)
1639
- if (
1640
- (zombies[i]._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE)) === REACTIVE_ZOMBIE &&
1641
- zombies[i]._config & CONFIG_VERDICT
1642
- )
1643
- recompute(zombies[i]);
1644
- // The unchanged passes' tails stay linked (A30, #3469): their inputs
1645
- // are held, and the committed frame still derives from them — no trim.
1646
- } else {
1647
- commitPendingNodes();
1648
- // The flush committed: an unchanged pass's stale tail goes now. Not
1649
- // after a later pass this flush that threw (NotReady included — it
1650
- // keeps its full list, `_depsTail` marking where it stopped) or that
1651
- // left an effect owing a run (`runEffect` trims when the run applies):
1652
- // `recompute`'s own gates.
1653
- for (let i = 0; i < heldTrims.length; i++) {
1654
- const t = heldTrims[i];
1655
- if (t._x?._error == null && !t._modified) trimStaleDeps(t);
1656
- }
1657
- }
1658
- // The list is the flush's either way. Reset only when there is something
1659
- // to reset (`stagedReaders` below likewise): `length = 0` is a runtime
1660
- // call even on an empty array, and the plain flush — no unchanged pass,
1661
- // no lane work — has nothing in either.
1662
- if (heldTrims.length) heldTrims.length = 0;
1663
- // Lanes: a blocked one parks its frame (its own flight is up); an
1664
- // unblocked one reveals this round's work. Then land every transaction
1665
- // no frame is waiting on — its lanes end with it. Backwards: a landing
1666
- // removes its entry.
1667
- // This flush's own runs, the lanes' reveals, and the landings' held runs
1668
- // are collected apart and ordered below.
1669
- const own = this._queues;
1670
- const lanes = (this._queues = [[], []]);
1671
- // The passes that read this frame's stagings as the screen read held
1672
- // writes if it parked: the lane seam re-derives them on the committed
1673
- // world (a lane's runs wait this round).
1674
- GlobalQueue._laneSeams?.(t !== null ? stagedReaders : null);
1675
- if (stagedReaders.length) stagedReaders.length = 0;
1676
- this._queues = [[], []];
1677
- for (let k = transactions.length - 1; k >= 0; k--) {
1678
- const u = transactions[k];
1679
- if (blocked(u)) continue;
1680
- transactions.splice(k, 1);
1681
- GlobalQueue._endLanes?.(u);
1682
- land(u);
1683
- // A landing's commits can dispose a zombie a transaction judged above
1684
- // was blocked on (#3463: live "until the commit that disposes it") —
1685
- // judge them again.
1686
- k = transactions.length;
1687
- }
1688
- // The store folds the pending backings whose container nodes committed
1689
- // — this flush's, or a landing's (store/store.ts installs it).
1690
- GlobalQueue._storeCommit?.();
1691
- // Marks declared outside a transaction: the flush that carried them
1692
- // parked (something async below) — the window is the frame's, the marks
1693
- // are its transaction's; else they release at the seam, verdict-only,
1694
- // nothing to show (affects.ts).
1695
- const live = t && liveTx(t);
1696
- GlobalQueue._releaseAmbientMarks?.(live);
1697
- // The effect phase: lane work first (displayed ahead of the frame), then
1698
- // this flush's runs, then the runs the landings held from earlier
1699
- // flushes — as one pass would have queued them (#3540: a shell
1700
- // re-derived at the landing before a boundary swap held since the
1701
- // write; #3528: a display-ahead swap before the count it is shown
1702
- // beside). A parked frame's runs wait with its transaction instead: the
1703
- // effect phase applies a committed frame.
1704
- const parked = live !== null;
1705
- // Attribution hook: this flush found its transaction incomplete — its
1706
- // writes stay staged, its runs are stashed below. Before the effect
1707
- // phase (the lanes' display-ahead runs are the visible acknowledgers);
1708
- // `holdEnd` fires after it (`flush`).
1709
- if (parked && attrHooks !== null) {
1710
- attrHooks.holdStart(live);
1711
- holding = true;
1712
- }
1713
- for (let i = 0; i < 2; i++) {
1714
- if (parked) append(live._queues[i], own[i]);
1715
- // The plain flush — no reveal, no landing, not parked — has nothing to
1716
- // order around its own runs: they are the queue as they stand (the
1717
- // same array, not a copy; `run` takes it whole). Every other seam
1718
- // builds the ordered queue above.
1719
- this._queues[i] =
1720
- parked || lanes[i].length || this._queues[i].length
1721
- ? lanes[i].concat(parked ? [] : own[i], this._queues[i])
1722
- : own[i];
1723
- }
1724
- if (deferredZombies.length !== 0) {
1725
- // Survivors are the displayed frame of a held pass: they rerun for the
1726
- // live write, parents before children.
1727
- const zombies = deferredZombies.splice(0).sort((a, b) => a._height - b._height);
1728
- for (let i = 0; i < zombies.length; i++) {
1729
- if (!(zombies[i]._flags & REACTIVE_DISPOSED)) recompute(zombies[i]);
1730
- }
1731
- }
1732
- // Boundaries: readers that settled, landed or died this flush are
1733
- // dropped; a fallback with none left reveals next round.
1734
- GlobalQueue._boundarySeam?.();
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;
1735
1818
  }
1736
- run(type) {
1737
- const effects = this._queues[type - 1];
1738
- if (effects.length) {
1739
- this._queues[type - 1] = [];
1740
- runQueue(effects, type);
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;
1741
1837
  }
1742
- }
1743
- enqueue(type, fn, lane = passLane) {
1744
- // Lane work's runs are the lane's: they run at its reveal, not with
1745
- // the frame (which may park). A lane pass that read the frame's own
1746
- // staging is the frame's (`recompute` passes null): its run waits with
1747
- // the frame.
1748
- if (type) (lane !== null ? lane : this)._queues[type - 1].push(fn);
1749
- schedule();
1750
- }
1751
- /** Status reaching the root: pending is absorbed (the root mount defers —
1752
- * dev records it for the ASYNC_OUTSIDE_LOADING_BOUNDARY FYI); an error is
1753
- * unhandled (`false` → the caller halts).
1754
- *
1755
- * L2: this is where a transaction is registered. Pending that reaches an
1756
- * observer (a render effect: the frame) is a frame that cannot show — the
1757
- * flush parks, and the transaction waits on the flight while a frame
1758
- * derives from it (`blocked`). Pending nobody observes holds nothing: a
1759
- * memo in flight that no frame reads is not a frame waiting. (Base: the
1760
- * queue's `_asyncReporters`, INV-3's one registration site.)
1761
- *
1762
- * A boundary between the observer and the root (boundaries.ts) is asked
1763
- * first: a fallback catches what is not ready under it, and the root
1764
- * never hears of it. */
1765
- notify(node, mask, flags, error) {
1766
- if (GlobalQueue._catch && GlobalQueue._catch(node, flags & mask, error)) return true;
1767
- if (mask & STATUS_PENDING) {
1768
- if (flags & STATUS_PENDING) {
1769
- if (_enforceLoadingBoundary) _hitUnhandledAsync = true;
1770
- // A pass that went pending holds its frame: the async's own
1771
- // synchronous frame is not ready (A15 reveal corollary: a reveal
1772
- // that discovers a flight whose inputs are visible holds and joins
1773
- // it; one whose inputs are held is a stale reader and never gets
1774
- // here — `read` serves it the committed value). The frame's, not the
1775
- // observer's: a render effect belongs to whoever dirtied its pass
1776
- // (A15 shared-hole, #3407) — one held by T1 and re-run by T2's write
1777
- // pends for T2's flight, and the two stay parallel; a frame that is
1778
- // T1's already joined T1 through the write or the memo that made it
1779
- // so. Outside a flush — root setup, a mount — the frame is the
1780
- // mount's: its pending observers are one transaction, and a flight
1781
- // started in it resumes it when it lands (maintainer, #3461: "part
1782
- // of the same one"); what the mount published synchronously stays
1783
- // published. Lane work going pending holds the lane, not the frame:
1784
- // `blocked(lane)` finds it in the lane's staging ("if further async
1785
- // downstream is hit it holds like its own transition").
1786
- if (passLane === null) joinFuture(null);
1787
- }
1788
- return true;
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;
1789
1860
  }
1790
- return false;
1791
1861
  }
1862
+ return false;
1863
+ }
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);
1792
1911
  }
1793
- /** A15's stale readers: a frame whose LAST pass read a held node as
1794
- * committed re-derives on the landed world — next round, after the commits.
1795
- * One re-derived since (any later pass cleared the bit: in the transaction,
1796
- * or without the held read) owes nothing. A run this transaction stashed
1797
- * for it is void (#3322: an effect has one value slot, and the stale pass
1798
- * overwrote the value that run was for) — the re-derivation's run replaces
1799
- * it. */
1800
- function reruns(u) {
1801
- for (let i = 0; i < u._reruns.length; i++) {
1802
- const r = u._reruns[i];
1803
- // An errored reader's display is its error (D-39): the landing gives it
1804
- // nothing to re-derive, and a re-run would be a read that retries the
1805
- // errored source (#3945).
1806
- if (r._flags & REACTIVE_DISPOSED || r._statusFlags & STATUS_ERROR) continue;
1807
- if (r._flags & REACTIVE_FRAME_READ || r._config & CONFIG_VERDICT) {
1808
- r._flags &= ~REACTIVE_FRAME_READ;
1809
- r._modified = false;
1810
- // Its frame is replaced by the re-derivation: a run one of its
1811
- // children queued this flush (re-run by the same landing) is for the
1812
- // frame being replaced — void too (#3404).
1813
- for (let c = r._firstChild; c !== null; c = c._nextSibling) if (c._type) c._modified = false;
1814
- enqueueSub(r);
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;
1815
1940
  }
1941
+ holdFrame(c, t);
1816
1942
  }
1817
- u._reruns.length = 0;
1818
1943
  }
1819
- /** The landing: the held frame becomes the frame. */
1820
- function land(u) {
1821
- // Attribution hook: judged complete — its held writes commit next
1822
- // (`_nodes` still lists them).
1823
- if (attrHooks !== null) attrHooks.transitionSettled(u);
1824
- reruns(u);
1825
- if (u._marks !== null) GlobalQueue._releaseMarks(u._marks);
1826
- // Old children die in the commits (cleanups first), then the stashed
1827
- // effects run ahead of this flush's own. A node a lane took over since
1828
- // (a guess written over the staged truth) is the lane's to land.
1829
- for (let i = 0; i < u._nodes.length; i++) {
1830
- const n = u._nodes[i];
1831
- if (n._x._transaction === null || txOf(n) !== u) continue;
1832
- n._x._transaction = null;
1833
- 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);
1834
1950
  }
1835
- releaseQueues(u);
1836
1951
  }
1837
- /** The held runs join this flush's effect phase after its own runs (the
1838
- * landing's re-passes, in height order): the frame's effects run as one
1839
- * pass would have queued them — a shell re-derived at the landing before a
1840
- * boundary swap held since the write (#3540's log order). */
1841
- function releaseQueues(u) {
1842
- for (let i = 0; i < 2; i++) {
1843
- append(globalQueue._queues[i], u._queues[i]);
1844
- u._queues[i] = [];
1845
- }
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;
1846
1956
  }
1847
- /** `a.push(...b)` without the argument-count limit. */
1848
- function append(a, b) {
1849
- 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;
1850
1990
  }
1851
- /** Nodes staged this flush (`_pendingValue` set), committed at its end. */
1852
- const pendingNodes = [];
1853
- /** Passes that read a staging of this flush as the screen — lane work
1854
- * (core.ts `stagedRead`), a verdict reader before the frame's verdict
1855
- * (verdict.ts): if the frame parks they read a held write — re-derived next
1856
- * round (lanes.ts; a lane's runs wait that round). */
1857
- const stagedReaders = [];
1858
- /** A15's stale reader: `c` was served the committed value of a node `t`
1859
- * holds (a render effect, a verdict reader, lane work) and is re-derived
1860
- * after `t`'s landing (`_reruns`). Once per pass. */
1861
- function staleReader(c, t) {
1862
- if (!(c._flags & REACTIVE_FRAME_READ)) {
1863
- c._flags |= REACTIVE_FRAME_READ;
1864
- t._reruns.push(c);
1865
- }
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;
1866
2002
  }
1867
- function queuePendingNode(node) {
1868
- lastStagedNodeName = node._name ?? null;
1869
- pendingNodes.push(node);
1870
- if (!globalQueue._running) markUnflushedStaged(); // A28
2003
+ function setTrackedQueueCallback(value) {
2004
+ inTrackedQueueCallback = value;
1871
2005
  }
1872
- // Dev-only attribution for the flush loop guard (#3140): when the guard
1873
- // trips, naming what the loop kept chewing on lets the app author attribute
1874
- // the runaway without patching dist.
1875
- let lastStagedNodeName = null;
1876
- /** §12d: bumped by every recompute and every new subscriber edge. A node's
1877
- * staged-rewrite skip is sound only while NOTHING recomputed or linked since
1878
- * its last notify — a mid-batch pull can clean a marked subscriber, and a
1879
- * skipped re-write would leave it stale. */
1880
- let notifyEpoch = 0;
1881
- function bumpNotifyEpoch() {
1882
- 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++;
1883
2014
  }
1884
- function insertSubs(node) {
1885
- // §12d: stamp before walking — setSignal's staged-rewrite fast path skips
1886
- // the next walk for this node while the epoch holds (marking is idempotent).
1887
- node._notifiedAt = notifyEpoch;
1888
- // Presence bits gate the optional-slot probes (see constants.ts): one
1889
- // masked read of the always-present _config instead of missing-property
1890
- // lookups in the hottest notify loop.
1891
- const cfg = node._config;
1892
- const hasSnapshot = (cfg & CONFIG_HAS_SNAPSHOT) !== 0 && node._x?._snapshotValue !== undefined;
1893
- // Observe-tier fan-out: this walk visits every subscriber edge anyway, so
1894
- // the graph-size count is one local increment here and no field anywhere.
1895
- let fanOut = 0;
1896
- for (let s = node._subs; s !== null; s = s._nextSub) {
1897
- const sub = s._sub;
1898
- fanOut++;
1899
- // Missed-wake latch (#3037): this write is landing while the subscriber
1900
- // is mid-recompute (a nested pull committing beneath its reads), and the
1901
- // heap refuses RECOMPUTING nodes. A gen-current link means the pass
1902
- // already validated this dep — the value it read is now stale — so latch
1903
- // for recompute's tail to reschedule. Untouched links need no latch (the
1904
- // pass either re-reads them fresh or trims them), and neither does the
1905
- // tail link: it is the read IN FLIGHT — read() links before it pulls, so
1906
- // this very commit is what that read returns.
1907
- if (sub._flags & REACTIVE_RECOMPUTING_DEPS && s._gen === sub._depGen && s !== sub._depsTail)
1908
- sub._flags |= REACTIVE_MISSED_WAKE;
1909
- if (hasSnapshot && sub._config & CONFIG_IN_SNAPSHOT_SCOPE) {
1910
- sub._flags |= REACTIVE_SNAPSHOT_STALE;
1911
- continue;
1912
- }
1913
- 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);
1914
2047
  }
1915
- if (fanOut >= GRAPH_SIZE_WARN_AT) noteFanOut(node, fanOut);
1916
2048
  }
1917
- /** Lane work re-staged (lanes.ts; `recompute`): the lane's members among
1918
- * its subscribers re-derive as the lane's work (REACTIVE_LANE_DIRTY), their
1919
- * runs held with it — not as stale readers republishing the committed view
1920
- * (#3460). After `insertSubs`. A render effect on no lane is marked too:
1921
- * inert for its pass (`recompute` seats a lane only from its own `_x`), it
1922
- * is a live write if the effect becomes a zombie later this flush — an
1923
- * on-screen reader whose removal an adopted write stages follows the lane
1924
- * (the lane-membership ruling, 2026-10-05; `_update`). */
1925
- function laneDirty(node, l) {
1926
- for (let s = node._subs; s !== null; s = s._nextSub) {
1927
- const t = s._sub._x?._transaction;
1928
- 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;
1929
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);
1930
2099
  }
1931
- /** One lane, or two of one link group (#3335): one reveal unit. */
1932
- function sameLane(a, b) {
1933
- 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;
1934
2110
  }
1935
- function commitPendingNode(n) {
1936
- const c = n;
1937
- // L2: the commit is where a pass, held or not, stops being uncommitted —
1938
- // unless it has no answer yet. A pass still in flight (pending, nothing
1939
- // staged) stays uncommitted: a re-pass disposes its children on the spot,
1940
- // and the frame it parked stays the frame until a pass of this node
1941
- // commits a value.
1942
- if (!c._fn) {
1943
- n._config &= -385;
1944
- if (n._pendingValue !== NOT_PENDING) {
1945
- n._value = n._pendingValue;
1946
- n._pendingValue = NOT_PENDING;
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 = "";
2125
+ }
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;
1947
2167
  }
1948
- return;
1949
2168
  }
1950
- // Computeds only from here (`_statusFlags` is not a signal field). A
1951
- // commit beneath a flight publishes its inputs (A15 reveal corollary,
1952
- // #3305): a reveal of it must observe, not show the pre-flight value.
1953
- const inFlight = n._pendingValue === NOT_PENDING && (c._statusFlags & STATUS_PENDING) !== 0;
1954
- n._config = inFlight ? (n._config & -1048833) | CONFIG_INPUTS_PUBLISHED : n._config & -1049985;
1955
- if (n._pendingValue !== NOT_PENDING) {
1956
- n._value = n._pendingValue;
1957
- n._pendingValue = NOT_PENDING;
1958
- // A node born staged (recompute) initializes at this commit.
1959
- c._statusFlags &= ~STATUS_UNINITIALIZED;
1960
- // A quiet re-ask's classification survives its landing and dies with
1961
- // the landed value's commit (A19 exc. 2, #3178): verdict-quiet through
1962
- // the reveal. (The sweep of a node still in flight is not a landing.)
1963
- // The A28 stash goes with it.
1964
- if (c._x != null) ((c._x._reask = false), (c._x._flushed = NOT_PENDING));
1965
- // An effect with a staged value was born held (L2): its first run is
1966
- // this commit's, not its creation's (A29) — queue it now.
1967
- if (n._type && n._type !== EFFECT_TRACKED) {
1968
- n._modified = true;
1969
- 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
+ }
1970
2280
  }
1971
- }
1972
- // The committed value is the first observable answer for a loading-window
1973
- // node — the window closes here, not at compute time (#2990). Unconditional
1974
- // store to an always-present computed slot.
1975
- c._loading = false;
1976
- c._flags &= ~REACTIVE_MANUAL_WRITE;
1977
- // The dependencies of the pass that produced the value are the frame's now:
1978
- // the previous frame's tail goes (A30, #3410; `recompute` left it for a
1979
- // staged pass). Only after a clean pass: `_error` is cleared by a clean
1980
- // pass or by the node's own landing (whose pass was clean), so a set
1981
- // `_error` means the last pass threw, kept its full list, and `_depsTail`
1982
- // marks where it stopped.
1983
- if (c._x?._error == null) trimStaleDeps(c);
1984
- if (!(c._statusFlags & STATUS_PENDING)) c._statusFlags &= ~STATUS_UNINITIALIZED;
1985
- // L2: the children this commit publishes are the frame's now — the frame
1986
- // they replace, parked by the pass (`recompute`), goes.
1987
- if (
1988
- !inFlight &&
1989
- c._x != null &&
1990
- (c._x._pendingFirstChild !== null || c._x._pendingDisposal !== null)
1991
- )
1992
- GlobalQueue._dispose(c, false, true);
1993
- }
1994
- function commitPendingNodes() {
1995
- for (let i = 0; i < pendingNodes.length; i++) {
1996
- const n = pendingNodes[i];
1997
- // A node a lane pass re-staged this round is the lane's to reveal; one
1998
- // a transaction holds (born held by a mainline pass, `passTx`) is that
1999
- // transaction's to commit, at its landing.
2000
- if (!(n._config & (CONFIG_OVERRIDE | CONFIG_HELD))) commitPendingNode(n);
2001
- }
2002
- pendingNodes.length = 0;
2003
- }
2004
- const globalQueue = new GlobalQueue();
2005
- function flush(fn) {
2006
- if (actionDepth > 0) {
2007
- {
2008
- throw new Error(
2009
- "[FLUSH_IN_ACTION] flush() inside an action body is not allowed. An action's writes are held in its " +
2010
- "transaction and commit when the action settles: flush() cannot reveal them, and draining here would " +
2011
- "detach the writes that follow from the transaction. Remove the flush(); to observe the result, read " +
2012
- "after the action resolves."
2013
- );
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;
2014
2300
  }
2015
- }
2016
- if (fn) {
2017
- syncDepth++;
2018
- try {
2019
- return fn();
2020
- } finally {
2021
- // Decrement even if the drain throws (a throwing effect): a leaked
2022
- // syncDepth would stop `schedule()` from ever queuing a microtask again.
2023
- try {
2024
- flush();
2025
- } finally {
2026
- syncDepth--;
2027
- }
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;
2028
2312
  }
2029
- }
2030
- if (globalQueue._running) {
2031
- if (inTrackedQueueCallback) {
2032
- throw new Error(
2033
- "Cannot call flush() from inside onSettled or createTrackedEffect. flush() is not reentrant there. " +
2034
- "Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush())."
2035
- );
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;
2036
2337
  }
2037
- if (inEffectCallback) {
2038
- const message =
2039
- "[FLUSH_IN_EFFECT_CALLBACK] flush() called from inside an effect callback is a no-op: the flush that runs effects is already in progress. " +
2040
- "Writes made here are processed in the same flush's continuation; to force a drain afterwards, defer it: queueMicrotask(() => flush()).";
2041
- reportDiagnostic(
2042
- emitDiagnostic({
2043
- code: "FLUSH_IN_EFFECT_CALLBACK",
2044
- kind: "lifecycle",
2045
- severity: "warn",
2046
- message
2047
- })
2048
- );
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];
2049
2348
  }
2050
- return;
2051
- }
2052
- if (halted) return;
2053
- let count = 0;
2054
- // Attribution: whether this call drained anything, so `flushEnd` fires once
2055
- // per real drain and never for a no-op call. The declaration is dead in
2056
- // prod (its only write is behind true) and rollup drops it.
2057
- let drained = false;
2058
- // The drain's opening instant, under the loop's own condition so it fires
2059
- // exactly when `flushEnd` below will. Outside every try (see the rule in
2060
- // attribution-hooks.ts).
2061
- if (attrHooks !== null && scheduled) attrHooks.flushStart();
2062
- while (scheduled) {
2063
- if (++count === 1e5) {
2064
- // Attribution beats a bare guard (#3140): say what kept the loop alive.
2065
- throw new Error(
2066
- `Potential Infinite Loop Detected. Kept alive by scheduled work${lastStagedNodeName ? `; last staged node: ${lastStagedNodeName}` : ""}`
2067
- );
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
+ }
2068
2356
  }
2069
- globalQueue.flush();
2070
- drained = true;
2357
+ // Boundaries: readers that settled, landed or died this flush are
2358
+ // dropped; a fallback with none left reveals next round.
2359
+ boundarySeamHook?.();
2071
2360
  }
2072
- // Outside every try in this function (see the rule in attribution-hooks.ts):
2073
- // the drain loop above is the one place all scheduled work funnels through,
2074
- // so this is the "committed and effects ran" instant for everything the
2075
- // loop processed.
2076
- if (drained && attrHooks !== null) attrHooks.flushEnd();
2077
- }
2078
- function runQueue(queue, type) {
2079
- for (let i = 0; i < queue.length; i++) queue[i](type);
2080
- }
2081
-
2082
- /**
2083
- * Schedule one subscriber to re-run on the next flush: inserted into the
2084
- * dirty heap with the `_min` cursor pulled down. Tracked effects ride the
2085
- * heap too — the heap visit is their (empty) compute phase, which hands the
2086
- * callback to the user queue once the pass has committed (see
2087
- * GlobalQueue._update, #3291).
2088
- */
2089
- function enqueueSub(node) {
2090
- if (dirtyQueue._min > node._height) dirtyQueue._min = node._height;
2091
- insertIntoHeap(node, dirtyQueue);
2092
- }
2093
- function actualInsertIntoHeap(n, heap) {
2094
- const parentHeight =
2095
- (n._parent?._root ? n._parent._parentComputed?._height : n._parent?._height) ?? -1;
2096
- if (parentHeight >= n._height) n._height = parentHeight + 1;
2097
- const height = n._height;
2098
- const heapAtHeight = heap._heap[height];
2099
- if (heapAtHeight === undefined) heap._heap[height] = n;
2100
- else {
2101
- const tail = heapAtHeight._prevHeap;
2102
- tail._nextHeap = n;
2103
- n._prevHeap = tail;
2104
- heapAtHeight._prevHeap = n;
2361
+ run(type) {
2362
+ const effects = this._queues[type - 1];
2363
+ if (effects.length) {
2364
+ this._queues[type - 1] = [];
2365
+ runQueue(effects, type);
2366
+ }
2105
2367
  }
2106
- if (height > heap._max) heap._max = height;
2107
- }
2108
- function insertIntoHeap(n, heap) {
2109
- let flags = n._flags;
2110
- // RECOMPUTING refusals are not always losses: a genuinely missed wake (a
2111
- // write to a link this pass already validated) is latched link-side in
2112
- // insertSubs as REACTIVE_MISSED_WAKE for recompute's tail (#3037).
2113
- if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS)) return;
2114
- if (flags & REACTIVE_CHECK) {
2115
- n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
2116
- } else {
2117
- n._flags = flags | REACTIVE_IN_HEAP;
2118
- // An unmarked node entering an already-marked heap is marked on the
2119
- // spot, keeping the markHeap memo valid. `_marked` is only reset by
2120
- // runHeap, so a write between two mid-tick pulls (read-time markHeap +
2121
- // updateIfNecessary) would otherwise leave this node unmarked and every
2122
- // downstream pull stale until the next flush (#2922: the second
2123
- // `latest()` returned the first write's value). Invalidating the memo
2124
- // instead re-walked the WHOLE heap on the next pull — with N effects
2125
- // parked in the heap for a synchronous mount (each row writing a ref
2126
- // signal its effect subscribes to), mounting N rows was O(N²) (#3350).
2127
- // markNode's own guard skips an already-DIRTY node.
2128
- if (heap._marked) markNode(n);
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();
2129
2375
  }
2130
- if (!(flags & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(n, heap);
2131
- }
2132
- function insertIntoHeapHeight(n, heap) {
2133
- let flags = n._flags;
2134
- if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_IN_HEAP_HEIGHT)) return;
2135
- n._flags = flags | REACTIVE_IN_HEAP_HEIGHT;
2136
- actualInsertIntoHeap(n, heap);
2137
- }
2138
- function deleteFromHeap(n, heap) {
2139
- const flags = n._flags;
2140
- if (!(flags & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT))) return;
2141
- n._flags = flags & -25;
2142
- const height = n._height;
2143
- if (n._prevHeap === n) heap._heap[height] = undefined;
2144
- else {
2145
- const next = n._nextHeap;
2146
- const dhh = heap._heap[height];
2147
- const end = next ?? dhh;
2148
- if (n === dhh) heap._heap[height] = next;
2149
- else n._prevHeap._nextHeap = next;
2150
- end._prevHeap = n._prevHeap;
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;
2151
2416
  }
2152
- n._prevHeap = n;
2153
- n._nextHeap = undefined;
2154
2417
  }
2155
- function markHeap(heap) {
2156
- if (heap._marked) return;
2157
- heap._marked = true;
2158
- for (let i = 0; i <= heap._max; i++) {
2159
- for (let el = heap._heap[i]; el !== undefined; el = el._nextHeap) {
2160
- if (el._flags & REACTIVE_IN_HEAP) markNode(el);
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);
2161
2440
  }
2162
2441
  }
2442
+ u._reruns.length = 0;
2163
2443
  }
2164
- function markNode(el, newState = REACTIVE_DIRTY) {
2165
- const flags = el._flags;
2166
- if ((flags & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= newState) return;
2167
- el._flags = (flags & -4) | newState;
2168
- for (let link = el._subs; link !== null; link = link._nextSub) {
2169
- markNode(link._sub, REACTIVE_CHECK);
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);
2170
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);
2171
2465
  }
2172
- function runHeap(heap, recompute) {
2173
- heap._marked = false;
2174
- for (heap._min = 0; heap._min <= heap._max; heap._min++) {
2175
- let el = heap._heap[heap._min];
2176
- while (el !== undefined) {
2177
- if (el._flags & REACTIVE_IN_HEAP) recompute(el);
2178
- else adjustHeight(el, heap);
2179
- el = heap._heap[heap._min];
2180
- }
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] = [];
2181
2474
  }
2182
- heap._max = 0;
2183
2475
  }
2184
- function adjustHeight(el, heap) {
2185
- deleteFromHeap(el, heap);
2186
- let newHeight = el._height;
2187
- for (let d = el._deps; d; d = d._nextDep) {
2188
- const dep = d._dep;
2189
- if (dep._fn && dep._height >= newHeight) newHeight = dep._height + 1;
2190
- }
2191
- if (el._height !== newHeight) {
2192
- el._height = newHeight;
2193
- for (let s = el._subs; s !== null; s = s._nextSub) insertIntoHeapHeight(s._sub, heap);
2194
- }
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]);
2195
2479
  }
2196
-
2197
- // L2 (step 2): the parked frame. A pass parks the previous frame's children
2198
- // and `_disposal` list on `_x._pendingFirstChild` / `_x._pendingDisposal`
2199
- // (`parkChildren`, core.ts); they keep reacting until the node's commit
2200
- // disposes them (`zombie` walk below) or the owner dies. `markDisposal` flags
2201
- // the parked subtree REACTIVE_ZOMBIE: the splice guard and the reawaken path
2202
- // key off it, and the heap defers a zombie's recompute past the seam (no
2203
- // second heap — the deferral list in scheduler.ts).
2204
- function markDisposal(el) {
2205
- let child = el._firstChild;
2206
- while (child) {
2207
- child._flags |= REACTIVE_ZOMBIE;
2208
- markDisposal(child);
2209
- child = child._nextSibling;
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);
2210
2494
  }
2211
2495
  }
2212
- function dispose(node) {
2213
- // Direct disposal is death, not dormancy: strip the observation lifecycle
2214
- // so a later read freezes at the last committed value instead of
2215
- // reawakening the node (#3024). The teardown itself (heap removal — a node
2216
- // left queued would be recomputed and resurrected by the next flush (#2983)
2217
- // — dep unlinking, child disposal) is exactly unobserved()'s body; only
2218
- // this flag distinguishes death from dormancy.
2219
- node._config &= ~CONFIG_AUTO_DISPOSE;
2220
- unobserved(node);
2496
+ function queuePendingNode(node) {
2497
+ lastStagedNodeName = node._name ?? null;
2498
+ pendingNodes.push(node);
2499
+ if (!globalQueue._running) markUnflushedStaged(); // A28
2221
2500
  }
2222
- function disposeChildren(node, self = false, zombie) {
2223
- const flags = node._flags;
2224
- if (flags & REACTIVE_DISPOSED) return;
2225
- // A parked frame dies with its owner (#3024): the commit that would retire
2226
- // it never comes once the node is DISPOSED, so it drains here or its
2227
- // cleanups never run and it stays subscribed, rerunning in a torn-down
2228
- // tree (#3561). Death only (`self`): a re-pass leaves the frame reacting
2229
- // until the commit.
2230
- if (
2231
- self &&
2232
- !zombie &&
2233
- node._x !== null &&
2234
- (node._x._pendingFirstChild !== null || node._x._pendingDisposal !== null)
2235
- )
2236
- disposeChildren(node, false, true);
2237
- if (self) {
2238
- node._flags = flags | REACTIVE_DISPOSED;
2239
- if (node._parent === null && node._root) unregisterRoot(node);
2240
- }
2241
- if (self && true) clearSignals(node);
2242
- if (self && node._fn) {
2243
- if (node._x !== null) node._x._inFlight = null;
2244
- // A death that can unblock the future (L2, `futureBlocked`): a held
2245
- // flight's — a transaction's or a lane's (its `_x._transaction`) — or a
2246
- // stale reader's (REACTIVE_FRAME_READ — a frame that derived from one).
2247
- // Nothing else re-examines a dead node, so make sure a seam comes.
2248
- if (
2249
- (node._statusFlags & STATUS_PENDING && node._x?._transaction != null) ||
2250
- flags & REACTIVE_FRAME_READ
2251
- )
2252
- schedule();
2253
- }
2254
- let child = zombie ? node._x._pendingFirstChild : node._firstChild;
2255
- if (zombie) node._x._pendingFirstChild = null;
2256
- else node._firstChild = null;
2257
- while (child) {
2258
- const n = child;
2259
- // Owner teardown is death regardless of the child's own lifecycle
2260
- // (#3024): strip AUTO_DISPOSE so a post-disposal read freezes at the
2261
- // last committed value instead of reawakening in a torn-down tree.
2262
- // Runs before the recursion so already-dormant children (whose
2263
- // disposeChildren call early-returns on REACTIVE_DISPOSED) die too.
2264
- // Only unobserved()'s own node keeps its dormancy — it is never in
2265
- // this loop; its children are rebuilt fresh on reawaken.
2266
- n._config &= ~CONFIG_AUTO_DISPOSE;
2267
- // Heap removal must not be gated on `_deps`: a dependency-free
2268
- // computation queued by refresh() has a null dep list but still sits in
2269
- // the dirty heap, and left there the post-disposal flush recomputes it —
2270
- // recompute() rewriting `_flags` clears REACTIVE_DISPOSED and the node
2271
- // comes back to life (post-unmount runs, leaked cleanups, #2983).
2272
- // deleteFromHeap self-guards on the in-heap flags (and tolerates plain
2273
- // Owners, whose _flags is undefined), so no gate here.
2274
- deleteFromHeap(n, dirtyQueue);
2275
- clearDeps(n);
2276
- // The chain is detached above so a node a cleanup links mid-drain lands
2277
- // on the fresh head and survives. Pointing each drained child's prev at
2278
- // itself routes its later splice onto the detached chain, never the head,
2279
- // and keeps the dev owner-chain-head invariant honest for those children.
2280
- child._prevSibling = child;
2281
- disposeChildren(child, true);
2282
- // Read after, not before: a sibling this disposal made dormant spliced
2283
- // itself out of the detached chain, and a cleanup may then have linked
2284
- // it at the fresh head, which rewrote the `_nextSibling` a pre-read
2285
- // would still be holding.
2286
- child = child._nextSibling;
2287
- }
2288
- if (!zombie) node._childCount = 0;
2289
- // O(1) splice out of parent's chain on individual dispose. Skipped during
2290
- // batch dispose (parent already disposed) and for a zombie (it sits on the
2291
- // parent's parked frame, drained whole by the commit). We leave
2292
- // node._nextSibling intact so outer walks that already advanced past us
2293
- // still reach later siblings.
2294
- if (
2295
- self &&
2296
- !zombie &&
2297
- !(flags & REACTIVE_ZOMBIE) &&
2298
- node._parent !== null &&
2299
- !(node._parent._flags & REACTIVE_DISPOSED)
2300
- ) {
2301
- const prev = node._prevSibling;
2302
- const next = node._nextSibling;
2303
- // A node with no predecessor must be the chain's head; otherwise the
2304
- // write below would clobber the head with a stale `_nextSibling`,
2305
- // orphaning every live child ahead of it (#3543).
2306
- assertInvariant(
2307
- prev !== null || node._parent._firstChild === node,
2308
- "owner-chain-head",
2309
- "head node is not parent._firstChild — a node was spliced while flagged live but not in the chain (see #3543)"
2310
- );
2311
- if (prev !== null) prev._nextSibling = next;
2312
- else node._parent._firstChild = next;
2313
- if (next !== null) next._prevSibling = prev;
2314
- node._prevSibling = null;
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;
2541
+ }
2542
+ enqueueSub(sub);
2315
2543
  }
2316
- runDisposal(node, zombie);
2317
- // Final effect-returned cleanup fires at true disposal, after `_disposal`
2318
- // to mirror rerun ordering (compute-phase teardown first, cleanup last).
2319
- if (self && node._cleanup) {
2320
- const effectCleanup = node._cleanup;
2321
- node._cleanup = undefined;
2322
- enterDisposal();
2323
- effectCleanup();
2324
- exitDisposal();
2544
+ if (fanOut >= GRAPH_SIZE_WARN_AT) noteFanOut(node, fanOut);
2545
+ }
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;
2325
2558
  }
2326
2559
  }
2327
- function linkChild(parent, node) {
2328
- const head = parent._firstChild;
2329
- node._prevSibling = null;
2330
- node._nextSibling = head;
2331
- if (head !== null) head._prevSibling = node;
2332
- parent._firstChild = node;
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);
2333
2563
  }
2334
- function runDisposal(node, zombie) {
2335
- // Detach the list BEFORE running it (#3601), as `_cleanup` is (#2813). A
2336
- // cleanup that disposes an ancestor re-enters this node through the death
2337
- // walk while the loop is still running; with the list still attached, that
2338
- // walk ran every entry a second time. Detached, the re-entrant drain finds
2339
- // nothing. The same shape latched a throwing cleanup: the list survived the
2340
- // throw and every later drain re-ran and re-threw it.
2341
- const disposal = zombie ? node._x._pendingDisposal : node._disposal;
2342
- if (!disposal) return;
2343
- if (zombie) node._x._pendingDisposal = null;
2344
- else node._disposal = null;
2345
- // No try/finally: it would survive into prod (rollup keeps the frame). A
2346
- // throw leaves the depth raised; dev.ts clears the stale count on the next
2347
- // microtask, since teardown never spans one (core.ts).
2348
- enterDisposal();
2349
- if (Array.isArray(disposal)) {
2350
- // Unwind order (#3572, restores 1.x #1562): later registrations run
2351
- // before earlier ones. Children have already been disposed by the caller,
2352
- // so with LIFO a body that registers cleanup before creating its children
2353
- // tears down after them — the same order a per-component owner gives.
2354
- for (let i = disposal.length - 1; i >= 0; i--) {
2355
- const callable = disposal[i];
2356
- callable.call(callable);
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;
2357
2576
  }
2358
- } else {
2359
- disposal.call(disposal);
2577
+ return;
2360
2578
  }
2361
- exitDisposal();
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);
2362
2634
  }
2363
- function childId(owner, consume) {
2364
- let counter = owner;
2365
- while (counter._config & CONFIG_TRANSPARENT && counter._parent) counter = counter._parent;
2366
- if (counter.id != null)
2367
- return formatId(counter.id, consume ? counter._childCount++ : counter._childCount);
2368
- throw new Error("Cannot get child id from owner without an id");
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;
2369
2644
  }
2370
- /**
2371
- * Allocates and returns the next stable child id for `owner`. Used by
2372
- * hydration plumbing and `createUniqueId`. Not part of the user-facing API.
2373
- *
2374
- * @internal
2375
- */
2376
- function getNextChildId(owner) {
2377
- return childId(owner, true);
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();
2378
2722
  }
2379
- /**
2380
- * The id a freshly-created node inherits: an explicit `options.id` wins;
2381
- * transparent nodes share their parent's id; otherwise the parent's next
2382
- * child id is consumed (or `undefined` outside an id-carrying tree).
2383
- */
2384
- function inheritId(options, transparent, parent) {
2385
- return (
2386
- options?.id ??
2387
- (transparent ? parent?.id : parent?.id != null ? getNextChildId(parent) : undefined)
2388
- );
2723
+ function runQueue(queue, type) {
2724
+ for (let i = 0; i < queue.length; i++) queue[i](type);
2389
2725
  }
2726
+
2390
2727
  /**
2391
- * Returns the *next* child id for `owner` without consuming it. Used by
2392
- * hydration plumbing to peek at the id a future child will receive.
2393
- *
2394
- * @internal
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).
2395
2733
  */
2396
- function peekNextChildId(owner) {
2397
- return childId(owner, false);
2734
+ function enqueueSub(node) {
2735
+ if (dirtyQueue._min > node._height) dirtyQueue._min = node._height;
2736
+ insertIntoHeap(node, dirtyQueue);
2398
2737
  }
2399
- function formatId(prefix, id) {
2400
- const num = id.toString(36),
2401
- len = num.length - 1;
2402
- return prefix + (len ? String.fromCharCode(64 + len) : "") + num;
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;
2403
2752
  }
2404
- /**
2405
- * Returns the currently-tracking observer (the computation that subscribes to
2406
- * reactive reads at this point), or `null` if reads here would be untracked.
2407
- * Used by reactive primitives that need to know whether they're inside a
2408
- * tracking scope. App code rarely needs this — see `getOwner()` for the
2409
- * lifecycle owner instead.
2410
- *
2411
- * @example
2412
- * ```ts
2413
- * // Library predicate: only register a hot-path subscription when the
2414
- * // caller is inside a tracking scope (memo / effect compute / JSX).
2415
- * function trackIfTracked(source: () => unknown) {
2416
- * if (getObserver()) source();
2417
- * }
2418
- * ```
2419
- */
2420
- function getObserver() {
2421
- return tracking ? context : null;
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);
2422
2776
  }
2423
- /**
2424
- * Returns the current reactive **owner** — the lifecycle node that the next
2425
- * `cleanup()` / `onCleanup()` / `createSignal()` etc. will be attached to.
2426
- *
2427
- * Returns `null` if called outside any owner. Capture the owner with
2428
- * `getOwner()` and re-enter it later with `runWithOwner(owner, fn)` to attach
2429
- * disposables created from a callback (event handler, async resolution, etc.)
2430
- * back to a component's lifecycle.
2431
- *
2432
- * @example
2433
- * ```ts
2434
- * function defer<T>(fn: () => T) {
2435
- * const owner = getOwner();
2436
- * queueMicrotask(() => runWithOwner(owner, fn));
2437
- * }
2438
- * ```
2439
- */
2440
- function getOwner() {
2441
- return context;
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);
2442
2782
  }
2443
- /**
2444
- * Low-level: registers `fn` as a disposal callback on the current owner.
2445
- * Most code should use `onCleanup()` from `solid-js`, which adds dev-mode
2446
- * checks. `cleanup()` is the unchecked primitive used by internals.
2447
- */
2448
- function cleanup(fn) {
2449
- if (!context) return fn;
2450
- if (!context._disposal) context._disposal = fn;
2451
- else if (Array.isArray(context._disposal)) context._disposal.push(fn);
2452
- else context._disposal = [context._disposal, fn];
2453
- return fn;
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;
2796
+ }
2797
+ n._prevHeap = n;
2798
+ n._nextHeap = undefined;
2454
2799
  }
2455
- /**
2456
- * Returns `true` if the owner has been disposed. Pair with a captured owner
2457
- * to bail out of late callbacks whose surrounding component already
2458
- * unmounted.
2459
- *
2460
- * @example
2461
- * ```ts
2462
- * function onSettleSafe(fn: () => void) {
2463
- * const owner = getOwner();
2464
- * queueMicrotask(() => {
2465
- * if (owner && isDisposed(owner)) return; // component unmounted; skip
2466
- * runWithOwner(owner, fn);
2467
- * });
2468
- * }
2469
- * ```
2470
- */
2471
- function isDisposed(node) {
2472
- return !!(node._flags & REACTIVE_DISPOSED);
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
+ }
2473
2808
  }
2474
- function disposeRootSelf(self = true) {
2475
- disposeChildren(this, self);
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
+ }
2476
2816
  }
2477
- /**
2478
- * Creates a fresh owner attached as a child of the current owner (or as a
2479
- * detached root if there is none). Used by framework internals to group
2480
- * cleanups; app code should use `createRoot()` (host a reactive scope outside
2481
- * a component) or `runWithOwner()` (re-enter a captured owner).
2482
- *
2483
- * @internal
2484
- */
2485
- function createOwner(options) {
2486
- const parent = context;
2487
- const transparent = options?.transparent ?? false;
2488
- // Prod and observe boilerplates (see core.ts computed()). The observe
2489
- // literal carries the `_name` slot the rendering layer fills with the
2490
- // component label (`owner._name = "<App>"`) — a slot, so labelling a root
2491
- // is a plain store rather than a shape fork between labelled and plain roots.
2492
- const owner = {
2493
- id: inheritId(options, transparent, parent),
2494
- _config: transparent ? CONFIG_TRANSPARENT : 0,
2495
- _root: true,
2496
- _parentComputed: parent?._root ? parent._parentComputed : parent,
2497
- _firstChild: null,
2498
- _nextSibling: null,
2499
- _prevSibling: null,
2500
- _disposal: null,
2501
- _context: parent?._context || defaultContext,
2502
- _childCount: 0,
2503
- _x: null,
2504
- _parent: parent,
2505
- dispose: disposeRootSelf,
2506
- _name: undefined
2507
- };
2508
- if (parent && parent._config & CONFIG_CHILDREN_FORBIDDEN) {
2509
- emitDiagnostic({
2510
- code: "PRIMITIVE_IN_FORBIDDEN_SCOPE",
2511
- kind: "lifecycle",
2512
- severity: "error",
2513
- message: PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE,
2514
- ownerId: parent.id,
2515
- ownerName: parent._name
2516
- });
2517
- throw new Error(PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE);
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
+ }
2518
2826
  }
2519
- assertNotInEffectCallback();
2520
- if (parent) linkChild(parent, owner);
2521
- else registerRoot(owner);
2522
- DEV.hooks.onOwner?.(owner);
2523
- return owner;
2827
+ heap._max = 0;
2524
2828
  }
2525
- /**
2526
- * Creates a reactive root — an owner scope with its own `dispose()`. A root
2527
- * created inside an existing owner is owned by it and is disposed when the
2528
- * parent is disposed; call `dispose()` to tear it down earlier. To create a
2529
- * root that outlives its creator, detach explicitly:
2530
- * `runWithOwner(null, () => createRoot(...))`. Pass `id` to seed hydration
2531
- * ids for the tree it owns.
2532
- *
2533
- * `dispose()` tears down every signal, memo, effect, and `onCleanup`
2534
- * registered inside the root.
2535
- *
2536
- * Use this to host long-lived reactive scopes outside of a component (custom
2537
- * controllers, app bootstrapping, tests). Inside a component, prefer
2538
- * letting Solid's component lifecycle own things.
2539
- *
2540
- * @example
2541
- * ```ts
2542
- * // At module level there is no owner, so this root lives until disposed.
2543
- * const dispose = createRoot(dispose => {
2544
- * const [n, setN] = createSignal(0);
2545
- * createEffect(() => n(), value => console.log(value));
2546
- * setInterval(() => setN(x => x + 1), 1000);
2547
- * return dispose;
2548
- * });
2549
- *
2550
- * // Later, to tear everything down:
2551
- * dispose();
2552
- *
2553
- * // Inside an owner (component, effect, another root), detach explicitly
2554
- * // if the root must outlive its creator:
2555
- * const detached = runWithOwner(null, () => createRoot(d => d));
2556
- * ```
2557
- *
2558
- * @description https://docs.solidjs.com/reference/reactive-utilities/create-root
2559
- */
2560
- function createRoot(init, options) {
2561
- const owner = createOwner(options);
2562
- return runWithOwner(owner, () => init(() => owner.dispose()));
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
+ }
2563
2840
  }
2564
2841
 
2565
2842
  /** The store's shared slot-node release handler (store/store.ts installs it
@@ -2584,20 +2861,24 @@ function unlinkSubs(link) {
2584
2861
  if (nextSub === null) {
2585
2862
  // Store slot nodes dispatch to the ONE shared hook — no per-node
2586
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.
2587
2866
  if (dep._config & CONFIG_SLOT_NODE) slotUnobservedHook(dep);
2588
- else dep._x?._unobserved?.();
2867
+ else if (dep._x?._unobserved) runCleanup(dep._x._unobserved);
2589
2868
  // No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
2590
2869
  // A pending node is exempt: its in-flight async work is an observer —
2591
2870
  // tearing down would orphan the work and re-execute it on the next
2592
2871
  // read. The settle path runs this same last-one-out check when that
2593
2872
  // observer releases (the untracked-read dormancy sweep guards on
2594
2873
  // pending identically).
2874
+ // During a pass's teardown of its unfinished frame, the release waits
2875
+ // for the pass to end (`deferredReleases`, #4142).
2595
2876
  const c = dep;
2596
2877
  c._fn &&
2597
2878
  c._config & CONFIG_AUTO_DISPOSE &&
2598
2879
  !(c._flags & REACTIVE_ZOMBIE) &&
2599
2880
  !(c._statusFlags & STATUS_PENDING) &&
2600
- unobserved(c);
2881
+ (deferredReleases ? deferredReleases.push(c) : unobserved(c));
2601
2882
  }
2602
2883
  }
2603
2884
  return nextDep;
@@ -2626,6 +2907,17 @@ function clearDeps(el) {
2626
2907
  el._deps = null;
2627
2908
  el._depsTail = null;
2628
2909
  }
2910
+ /**
2911
+ * Last-one-out releases held back while a pass tears down the frame of an
2912
+ * earlier pass that never committed (#4142). The pass is about to rebuild
2913
+ * the same children, which read the same lazy nodes again. Released on the
2914
+ * spot, a lazy async memo whose only reader was the old frame's child memo
2915
+ * was torn down, re-ran on the new child's read, went pending, and the cycle
2916
+ * repeated forever. `recompute` collects them around that teardown and
2917
+ * releases what is still unobserved when the pass ends.
2918
+ */
2919
+ let deferredReleases = null;
2920
+ const deferReleases = list => (deferredReleases = list);
2629
2921
  function unobserved(el) {
2630
2922
  deleteFromHeap(el, dirtyQueue);
2631
2923
  clearDeps(el);
@@ -2827,6 +3119,45 @@ function releaseSettledDependents(el) {
2827
3119
  // (releaseSettledDependents shape): identity holders can sit below an
2828
3120
  // intermediate whose own error state has since been scrubbed or replaced
2829
3121
  // (e.g. an error boundary's tree node).
3122
+ /** D-41: an error a pass of a held node found is an answer of
3123
+ * that frame — it ends the wait like a value and shows when a value would.
3124
+ * Staged like one: `_pendingValue` carries it (CONFIG_STAGED_ERROR), readers
3125
+ * in the hold derive from it (`serve` throws it), and the commit publishes
3126
+ * it through the ordinary error path (`commitPendingNode` → `notifyStatus`),
3127
+ * dependents included. Not for a lane pass, nor a node with no shown value
3128
+ * (born held — its error has no frame to wait behind): those publish now,
3129
+ * as before. Returns whether the error was staged. */
3130
+ function stageError(el, error, inPass) {
3131
+ // Held, or — for a pass's own throw — derived from a staging of a flush
3132
+ // (REACTIVE_STAGED_READ is this pass's; a landing after the pass cannot
3133
+ // read it, the flush it read a staging of has committed or parked since).
3134
+ // Staged whether or not the flush has joined a hold yet: a pass that runs
3135
+ // before its async sibling joins is the seam's to decide, like a value
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).
3139
+ if (
3140
+ !(
3141
+ el._config & CONFIG_HELD ||
3142
+ (inPass && el._flags & REACTIVE_STAGED_READ && globalQueue._running && !el.fam)
3143
+ ) ||
3144
+ passLane
3145
+ )
3146
+ return false;
3147
+ // The node answered (an error ends the wait — no longer pending, nor
3148
+ // blocked); a shown error stays its display until the commit.
3149
+ clearPendingSources(el);
3150
+ if (el._x) el._x._blocked = false;
3151
+ if (!((el._statusFlags &= STATUS_ERROR | STATUS_UNINITIALIZED) & STATUS_ERROR))
3152
+ setPendingError(el);
3153
+ el._pendingValue = error instanceof StatusError ? error : new StatusError(el, error);
3154
+ el._config |= CONFIG_STAGED | CONFIG_STAGED_ERROR;
3155
+ // Readers parked on the flight re-run and derive from the staging; the
3156
+ // verdict readers the walk wakes need a flush (#4106).
3157
+ settlePendingSource(el);
3158
+ schedule();
3159
+ return true;
3160
+ }
2830
3161
  function settleErroredDependents(el, error) {
2831
3162
  let scheduled = false;
2832
3163
  const visited = new Set();
@@ -3073,7 +3404,11 @@ function handleAsync(
3073
3404
  el._time = clock;
3074
3405
  return;
3075
3406
  }
3076
- notifyStatus(el, stillPending ? STATUS_PENDING : STATUS_ERROR, error);
3407
+ // D-41: a held node's error is staged (`stageError`). A derived store's
3408
+ // readers learn its error through the family, not the derive's
3409
+ // dependents: it publishes now.
3410
+ if (stillPending || setter || !stageError(el, error))
3411
+ notifyStatus(el, stillPending ? STATUS_PENDING : STATUS_ERROR, error);
3077
3412
  // A NotReady rejection is a landing into another pending source. The
3078
3413
  // rejected flight will never settle its self entry, so transfer ownership
3079
3414
  // after notifyStatus has propagated the replacement source.
@@ -3091,7 +3426,7 @@ function handleAsync(
3091
3426
  // skip this stale async result — the upcoming flush will recompute the node
3092
3427
  // with the new value, creating a fresh Promise that supersedes this one.
3093
3428
  if (el._flags & REACTIVE_DIRTY) return;
3094
- const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
3429
+ const wasUninitialized = el._statusFlags & STATUS_UNINITIALIZED;
3095
3430
  landStatus(el);
3096
3431
  // Attribution hook: lets the engine snapshot state before the landing
3097
3432
  // branches, so it can tell whether the plain path's setSignal committed a
@@ -3104,6 +3439,9 @@ function handleAsync(
3104
3439
  handleError(error);
3105
3440
  return;
3106
3441
  }
3442
+ // A store derive's landing commits through its setter: unheld, the
3443
+ // shown error leaves now; held, at the derive's commit.
3444
+ if (!(el._config & CONFIG_HELD)) el._x._shownError = undefined;
3107
3445
  if (wasUninitialized) landStatus(el, true);
3108
3446
  // Attribution hook: this path landed through the setter (derived
3109
3447
  // stores, projections), which does not go through setSignal, so the
@@ -3119,14 +3457,9 @@ function handleAsync(
3119
3457
  // CARVE 2: the override-covered landing (hold + A18 supersession) and the
3120
3458
  // lane-routed landing (derived override, lane effect queue) went with
3121
3459
  // the optimistic engine; every landing is the plain setSignal. A node
3122
- // already held stages its answer into that hold (`setSignal`).
3123
- try {
3124
- setSignal(el, () => value);
3125
- } catch (e) {
3126
- // Same containment as above: setSignal's comparator throw is the only
3127
- // pre-commit failure here, and there is no user callsite to throw to.
3128
- notifyStatus(el, STATUS_ERROR, e);
3129
- }
3460
+ // already held stages its answer into that hold (`setSignal`). Its
3461
+ // comparator throw is contained there (`isSame`, A3).
3462
+ setSignal(el, () => value);
3130
3463
  // Attribution hook: this path landed through setSignal, whose write
3131
3464
  // hook already saw any committed change — direct=false lets the engine
3132
3465
  // reclassify that write as an async landing iff it actually committed.
@@ -3171,15 +3504,14 @@ function handleAsync(
3171
3504
  // close registration uses the ambient owner; and DEFERRED (the flattening
3172
3505
  // path — a thenable resolved to an iterable in a later microtask), where
3173
3506
  // there is no caller to serve and no ambient owner: sync-settled steps
3174
- // write through asyncWrite, and close registration goes through the slot
3175
- // the thenable branch pre-registered while it still owned the context.
3176
- // Returns whether a sync answer landed (first yield or empty completion) —
3177
- // meaningful only in the live posture.
3178
- const consumeIterator = (source, registerClose) => {
3507
+ // write through asyncWrite, and close registration targets `owner` (el
3508
+ // itself) directly. Returns whether a sync answer landed (first yield or
3509
+ // empty completion) — meaningful only in the live posture.
3510
+ const consumeIterator = (source, owner) => {
3179
3511
  const it = source[Symbol.asyncIterator]();
3180
3512
  let hadValue = false;
3181
3513
  let completed = false;
3182
- let initialRead = !registerClose;
3514
+ let initialRead = !owner;
3183
3515
  const close = () => {
3184
3516
  if (completed) return;
3185
3517
  completed = true;
@@ -3188,7 +3520,7 @@ function handleAsync(
3188
3520
  if (isThenable(returned)) returned.then(undefined, () => {});
3189
3521
  } catch {}
3190
3522
  };
3191
- registerClose ? registerClose(close) : cleanup(close);
3523
+ cleanup(close, owner);
3192
3524
  // Flight-identity cancellation (#3122): the registration above is the
3193
3525
  // owner-death backstop; the teardown slot fires at the _inFlight release
3194
3526
  // sites so supersede stops this stream immediately.
@@ -3196,8 +3528,15 @@ function handleAsync(
3196
3528
  // Release check before each next pull: an unobserved lazy node must tear
3197
3529
  // down (its close above runs via disposal, closing the iterator) instead
3198
3530
  // of pumping the stream forever with zero subscribers (#2935).
3531
+ // Later pulls run from an async continuation with no compute catch above
3532
+ // them: a throw invoking the protocol (next(), the step's then) settles
3533
+ // as this flight's error instead of escaping as a rejection (#4112).
3199
3534
  const iterateOrRelease = () => {
3200
- if (!settleAutodispose()) iterate();
3535
+ try {
3536
+ if (!settleAutodispose()) iterate();
3537
+ } catch (e) {
3538
+ handleError(e);
3539
+ }
3201
3540
  };
3202
3541
  const iterate = () => {
3203
3542
  let syncResult,
@@ -3282,7 +3621,7 @@ function handleAsync(
3282
3621
  // stream itself rather than settling on the iterable object. One level
3283
3622
  // only: A+ `then` already collapses nested thenables, so the resolved
3284
3623
  // value is never itself a thenable.
3285
- const flattenIfIterable = (value, registerClose) => {
3624
+ const flattenIfIterable = (value, owner) => {
3286
3625
  let innerIterator = false;
3287
3626
  if (typeof value === "object" && value !== null) {
3288
3627
  untrack(() => {
@@ -3290,8 +3629,9 @@ function handleAsync(
3290
3629
  });
3291
3630
  }
3292
3631
  if (!innerIterator) return false;
3293
- const landed = consumeIterator(value, registerClose);
3294
- if (!registerClose) liveLanded = landed;
3632
+ syncValue = undefined;
3633
+ const landed = consumeIterator(value, owner);
3634
+ if (!owner) liveLanded = landed;
3295
3635
  return true;
3296
3636
  };
3297
3637
  if (thenable) {
@@ -3299,29 +3639,32 @@ function handleAsync(
3299
3639
  rejected = false,
3300
3640
  syncError,
3301
3641
  isSync = true;
3302
- // Close registration for the flattening path. Consumption starts in a
3303
- // microtask where the ambient owner is gone (or worse, someone else's),
3304
- // so `cleanup()` can't be used — the close targets el's disposal list
3305
- // directly, exactly where a live cleanup() during this recompute would
3306
- // have put it. Only a flight that actually flattens becomes
3307
- // disposal-bearing.
3308
- const registerDeferredClose = fn => {
3309
- if (!el._disposal) el._disposal = fn;
3310
- else if (Array.isArray(el._disposal)) el._disposal.push(fn);
3311
- else el._disposal = [el._disposal, fn];
3312
- };
3313
3642
  watchAsyncTail(el, result).then(
3314
3643
  v => {
3315
3644
  if (isSync) {
3316
3645
  syncValue = v;
3317
3646
  resolved = true;
3318
- } else if (
3319
- el._x?._inFlight === result &&
3320
- !(el._flags & REACTIVE_DISPOSED) &&
3321
- flattenIfIterable(v, registerDeferredClose)
3322
- );
3323
- else {
3324
- asyncWrite(v);
3647
+ } else {
3648
+ // Flattening consumes the stream as the value: each yield lands
3649
+ // through asyncWrite under this flight's identity; the first one
3650
+ // clears pending exactly like a plain promise resolution would.
3651
+ // Disposed nodes never start a pump (their disposal list already
3652
+ // ran, so nothing could close the iterator). Consumption starts in
3653
+ // a microtask where the ambient owner is gone, so the close
3654
+ // registers on el's own disposal list. Iterator setup here has no
3655
+ // compute catch above it: a protocol throw settles as this
3656
+ // flight's error (#4112).
3657
+ try {
3658
+ if (
3659
+ el._x?._inFlight === result &&
3660
+ !(el._flags & REACTIVE_DISPOSED) &&
3661
+ flattenIfIterable(v, el)
3662
+ )
3663
+ return;
3664
+ asyncWrite(v);
3665
+ } catch (e) {
3666
+ handleError(e);
3667
+ }
3325
3668
  settleAutodispose();
3326
3669
  }
3327
3670
  },
@@ -3425,6 +3768,8 @@ function notifyStatus(el, status, error, blockStatus) {
3425
3768
  // (CARVE 2: the lane assignment went with the engine.)
3426
3769
  {
3427
3770
  if (status === STATUS_PENDING && pendingSource) {
3771
+ // D-41: a re-ask of a shown error keeps it the display (`_shownError`).
3772
+ if (el._statusFlags & STATUS_ERROR) el._x._shownError = el._x._error;
3428
3773
  addPendingSource(el, pendingSource);
3429
3774
  el._statusFlags = STATUS_PENDING | (el._statusFlags & STATUS_UNINITIALIZED);
3430
3775
  // Preserve the current source on this propagation so readers park on
@@ -3504,7 +3849,8 @@ function propagateStatus(el, status, error, downstreamBlockStatus) {
3504
3849
  if (
3505
3850
  passLane === null &&
3506
3851
  (sub._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD &&
3507
- globalQueue._running
3852
+ globalQueue._running &&
3853
+ !freshHook?.(sub, pendingSource._time === clock)
3508
3854
  )
3509
3855
  joinFuture(txOf(sub));
3510
3856
  return;
@@ -3519,7 +3865,7 @@ function propagateStatus(el, status, error, downstreamBlockStatus) {
3519
3865
  // the dependent's pending is the lane's own flight (`laneStage`).
3520
3866
  {
3521
3867
  // (No answer: a pending propagation leaves a written guess a guess.)
3522
- if (passLane !== null) GlobalQueue._laneStage(sub, passLane, true, el);
3868
+ if (laneStageHook && passLane !== null) laneStageHook(sub, passLane, true, el);
3523
3869
  else {
3524
3870
  queuePendingNode(sub);
3525
3871
  // A15 (#3443): pending propagates onto a held memo without
@@ -3561,8 +3907,29 @@ GlobalQueue._update = el => {
3561
3907
  // (REACTIVE_LANE_DIRTY — a lane shows ahead of any park): it runs now, as
3562
3908
  // the lane's work, for the reveal this seam (#3463).
3563
3909
  else if (el._flags & REACTIVE_ZOMBIE && !(el._flags & REACTIVE_LANE_DIRTY)) deferZombie(el);
3564
- else recompute(el);
3910
+ // A lane frame's member (parked by a lane pass, #3698) follows mainline
3911
+ // writes only: a lane's dirty on it is cancelled — the lane's apply retires
3912
+ // it, and the pass that replaced it already decided what that screen shows
3913
+ // (#4137: a `<Show>` branch withdrawn by its own lane re-ran on a falsy
3914
+ // condition).
3915
+ // (Lanes exist only with the lane engine installed: folded away without.)
3916
+ else if (laneStageHook && el._flags & REACTIVE_ZOMBIE && laneFrameMember(el)) {
3917
+ deleteFromHeap(el, dirtyQueue);
3918
+ el._flags &= -65540;
3919
+ } else recompute(el);
3565
3920
  };
3921
+ /** The zombie's frame was parked by lane work: the nearest non-zombie owner
3922
+ * (the one whose pass parked it) carries a lane's derived value — or, an
3923
+ * effect (no lane value: `laneStage`), is listed in a lane (an `insert()`
3924
+ * hole's render effect). */
3925
+ function laneFrameMember(el) {
3926
+ let o = el._parent;
3927
+ while (o._flags & REACTIVE_ZOMBIE) o = o._parent;
3928
+ return (
3929
+ (o._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE ||
3930
+ (o._type && o._x?._transaction?._lane)
3931
+ );
3932
+ }
3566
3933
  GlobalQueue._dispose = disposeChildren;
3567
3934
  const PRIMITIVE_IN_FORBIDDEN_SCOPE_MESSAGE =
3568
3935
  "[PRIMITIVE_IN_FORBIDDEN_SCOPE] Cannot create reactive primitives inside createTrackedEffect or owner-backed onSettled";
@@ -3597,6 +3964,10 @@ const REACTIVE_WRITE_IN_OWNED_SCOPE_REFRESH_MESSAGE =
3597
3964
  let tracking = false;
3598
3965
  let context = null;
3599
3966
  let snapshotCaptureActive = false;
3967
+ /** The lane at the root of the running chain of first passes: the lane whose
3968
+ * pass created the chain's first node (a mount), inherited by every first
3969
+ * pass nested under it, whose seats are mainline (`recompute`, #4166). */
3970
+ let laneRoot;
3600
3971
  let snapshotSources = null;
3601
3972
  function ownerInSnapshotScope(owner) {
3602
3973
  while (owner) {
@@ -3656,6 +4027,7 @@ function recompute(el, create = false) {
3656
4027
  // §12d: any recompute can clean a marked subscriber — invalidate skips.
3657
4028
  bumpNotifyEpoch();
3658
4029
  const isEffect = el._type;
4030
+ let released;
3659
4031
  // Lanes (plan sec. 28): the seat of a pass is its node's. A node carrying a lane's
3660
4032
  // derived value runs as the lane's whoever dirtied it — a sync write, a
3661
4033
  // boundary reset, a frame rerun — so its children and its result are the
@@ -3671,18 +4043,27 @@ function recompute(el, create = false) {
3671
4043
  // derivation's pass into the lane (`read`); a leaf's never moves.
3672
4044
  // Restored at the end, after this pass's staging and runs have been
3673
4045
  // routed.
4046
+ // Lanes exist only with the lane engine installed (`laneStageHook`): a
4047
+ // bundle without it seats every pass on mainline, and folds the seat away.
3674
4048
  const prevLane = passLane;
4049
+ // The mount a first pass belongs to: its creator's lane, or the one its
4050
+ // creator's own mount came from — a render effect a verdict lane's mount's
4051
+ // content creates is that mount's too (#4166). Lanes only, like the seat.
4052
+ const prevMount = laneRoot;
4053
+ const mount = laneStageHook && (laneRoot = create && (prevLane || prevMount));
3675
4054
  setPassLane(
3676
- (el._flags & REACTIVE_LANE_DIRTY ||
3677
- (el._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE) &&
3678
- el._x?._transaction?._lane
3679
- ? el._x._transaction
3680
- : // Loose `!=`: false with no creator lane (undefined == null).
3681
- create &&
3682
- prevLane?._parent._verdict != prevLane &&
3683
- (creatorPass(context)?._flags ?? 0) & REACTIVE_RECOMPUTING_DEPS
3684
- ? prevLane
3685
- : null
4055
+ !laneStageHook
4056
+ ? null
4057
+ : (el._flags & REACTIVE_LANE_DIRTY ||
4058
+ (el._config & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE) &&
4059
+ el._x?._transaction?._lane
4060
+ ? el._x._transaction
4061
+ : // Loose `!=`: false with no creator lane (undefined == null).
4062
+ create &&
4063
+ prevLane?._parent._verdict != prevLane &&
4064
+ creatorPass(context)?._flags & REACTIVE_RECOMPUTING_DEPS
4065
+ ? prevLane
4066
+ : null
3686
4067
  );
3687
4068
  // Attribution hook: fired before this run touches the dep list — `_deps`
3688
4069
  // still holds the previous run's links (the subscriptions that could have
@@ -3724,8 +4105,11 @@ function recompute(el, create = false) {
3724
4105
  // Held by a blocked lane: the frame joins nothing — a lane never holds
3725
4106
  // a sync write (#3460). The pass is the lane's if it reads the lane's
3726
4107
  // world (`read`), and has left it otherwise.
3727
- if (tx._lane);
3728
- else if (isEffect !== EFFECT_RENDER && !(el._config & CONFIG_VERDICT)) joinPassTx(tx);
4108
+ // An answered deferred node's re-pass is a lagging question (D-40):
4109
+ // it joins nothing, so the write that reached it commits mainline.
4110
+ if (tx._lane || el._config & CONFIG_LAGS);
4111
+ else if (isEffect !== EFFECT_RENDER && !(el._config & CONFIG_VERDICT))
4112
+ joinPassTx(tx, freshHook?.(el, el._statusFlags & STATUS_UNINITIALIZED));
3729
4113
  else if (
3730
4114
  tx !== flushTransaction &&
3731
4115
  !(el._statusFlags & STATUS_UNINITIALIZED || el._config & CONFIG_HELD_FRAME)
@@ -3745,8 +4129,22 @@ function recompute(el, create = false) {
3745
4129
  // frame parked before it stays parked. Otherwise they ARE the frame:
3746
4130
  // park them until this pass commits. Tracked effects run after the seam
3747
4131
  // and own nothing to park.
3748
- if (isEffect === EFFECT_TRACKED || el._config & CONFIG_STAGED) disposeChildren(el);
3749
- else if (el._firstChild !== null || el._disposal !== null) parkChildren(el);
4132
+ if (isEffect === EFFECT_TRACKED || el._config & CONFIG_STAGED) {
4133
+ // A lazy node only those children read is released when this pass
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).
4137
+ const prev = deferredReleases;
4138
+ deferReleases((released = []));
4139
+ disposeChildren(el);
4140
+ deferReleases(prev);
4141
+ } else if (el._firstChild !== null || el._disposal !== null) parkChildren(el);
4142
+ // An error a pass staged (D-41) is replaced by this pass's answer: a
4143
+ // value, a flight, or an error of its own (#4109, #4145).
4144
+ if (el._config & CONFIG_STAGED_ERROR) {
4145
+ el._pendingValue = NOT_PENDING;
4146
+ el._config ^= CONFIG_STAGED_ERROR;
4147
+ }
3750
4148
  clearSignals(el);
3751
4149
  }
3752
4150
  const wasUninitialized = !!(el._statusFlags & STATUS_UNINITIALIZED);
@@ -3763,6 +4161,8 @@ function recompute(el, create = false) {
3763
4161
  // settlePendingSource walk never runs for a landing that was preempted
3764
4162
  // (#3181).
3765
4163
  const wasPendingSource = el._x?._pendingSources?.has(el);
4164
+ // D-41: this pass's error is staged, not published (`stageError`).
4165
+ let stagedErr = false;
3766
4166
  // A19 exc. 2: a `refresh()` re-asks the question already answered — quiet
3767
4167
  // for the verdict — unless the question in flight is a new one, which the
3768
4168
  // re-ask does not launder (a poll during a quiet confirm stays quiet).
@@ -3777,7 +4177,7 @@ function recompute(el, create = false) {
3777
4177
  // frame, and the splice/reawaken paths key off it. A zombie reruns for
3778
4178
  // live writes until the commit that disposes it, so every per-pass wipe
3779
4179
  // — here, the finally below, updateIfNecessary — carries it (#3543).
3780
- el._flags = REACTIVE_RECOMPUTING_DEPS | (el._flags & REACTIVE_ZOMBIE);
4180
+ el._flags = REACTIVE_RECOMPUTING_DEPS | (el._flags & (REACTIVE_ZOMBIE | REACTIVE_REASK));
3781
4181
  el._time = clock;
3782
4182
  // The pass's previous value: its staging, else a lane node's lane value
3783
4183
  // when it has one (lanes.ts), else the committed one.
@@ -3789,7 +4189,8 @@ function recompute(el, create = false) {
3789
4189
  : el._value;
3790
4190
  let oldHeight = el._height;
3791
4191
  let missedWake = false;
3792
- // L2: did this pass read the future (REACTIVE_JOINED, set by read)?
4192
+ // L2: did this pass read the future (REACTIVE_JOINED, set by read) — or,
4193
+ // a lane's mount, a staging of this flush (#3851, #4166)?
3793
4194
  let joined = false;
3794
4195
  let prevTracking = tracking;
3795
4196
  let prevStrictRead = false;
@@ -3834,11 +4235,15 @@ function recompute(el, create = false) {
3834
4235
  // retry can actually run.
3835
4236
  parkLoadingWindow(el, e);
3836
4237
  } else {
3837
- if (notReady) ext(el)._blocked = true;
3838
- notifyStatus(el, notReady ? STATUS_PENDING : STATUS_ERROR, e);
3839
- // The replacement source is fully propagated now. If no new flight
3840
- // re-owned self, retire the superseded flight and its dependent copies.
3841
- if (notReady && wasPendingSource && !el._x?._inFlight) settlePendingSource(el);
4238
+ // D-41: a pass of a held node, or one that read a staging of this
4239
+ // flush, stages its error (`stageError`).
4240
+ if (notReady || !(stagedErr = stageError(el, e, true))) {
4241
+ if (notReady) ext(el)._blocked = true;
4242
+ notifyStatus(el, notReady ? STATUS_PENDING : STATUS_ERROR, e);
4243
+ // The replacement source is fully propagated now. If no new flight
4244
+ // re-owned self, retire the superseded flight and its dependent copies.
4245
+ if (notReady && wasPendingSource && !el._x?._inFlight) settlePendingSource(el);
4246
+ }
3842
4247
  // A re-park drops what the earlier pass carried (#3456): a source this
3843
4248
  // pass no longer reaches — its branch switched, or a fresh flight
3844
4249
  // replaced the inputs' pending with its own — stays copied onto
@@ -3846,14 +4251,16 @@ function recompute(el, create = false) {
3846
4251
  // stops at this node (nothing left to retire) before it finds them. A
3847
4252
  // dependent then waits forever on a flight it has no path to. The
3848
4253
  // re-park twin of the unchanged-value recovery sweep below; dependents
3849
- // with another path keep the source (retryReaches).
3850
- if (notReady && outgoingPendingSources)
4254
+ // with another path keep the source (retryReaches). A staged error
4255
+ // answered what the pass carried: the same.
4256
+ if ((notReady || stagedErr) && outgoingPendingSources)
3851
4257
  for (const source of outgoingPendingSources)
3852
4258
  if (source !== el && !el._x?._pendingSources?.has(source))
3853
4259
  settlePendingSource(el, source);
3854
4260
  }
3855
4261
  } finally {
3856
4262
  tracking = prevTracking;
4263
+ if (laneStageHook) laneRoot = prevMount;
3857
4264
  strictRead = prevStrictRead;
3858
4265
  // Consume the missed-wake latch (#3037, set by insertSubs): a dep write
3859
4266
  // landed beneath this pass on a link it had already validated. The wipe
@@ -3861,10 +4268,19 @@ function recompute(el, create = false) {
3861
4268
  // (markNode(c) in read()) marks the running node as part of ordinary
3862
4269
  // bookkeeping, and those marks are correctly discarded here.
3863
4270
  missedWake = (el._flags & REACTIVE_MISSED_WAKE) !== 0;
3864
- joined = (el._flags & REACTIVE_JOINED) !== 0;
3865
- // A verdict reader is one for as long as it probes: a pass that entered
3866
- // no window (REACTIVE_PROBED) is an ordinary derivation again.
3867
- if (!(el._flags & REACTIVE_PROBED)) el._config &= ~CONFIG_VERDICT;
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
+ );
4280
+ // A verdict reader is one for as long as it probes, or reads a memo
4281
+ // that does (`read`): a pass that did neither (REACTIVE_PROBED, the same
4282
+ // bit) is an ordinary derivation again.
4283
+ el._config = (el._config & ~CONFIG_VERDICT) | (el._flags & REACTIVE_PROBED);
3868
4284
  // REACTIVE_DISPOSED survives too (#3621): the pass may have disposed its
3869
4285
  // own owner (a memo calling its root's `dispose()`, a cleanup doing so
3870
4286
  // #3601/#3606), and `disposeChildren` set the flag on this node
@@ -3883,6 +4299,7 @@ function recompute(el, create = false) {
3883
4299
  REACTIVE_PROBE_UNANSWERED)) |
3884
4300
  (create ? el._flags & REACTIVE_SNAPSHOT_STALE : 0);
3885
4301
  context = oldcontext;
4302
+ if (released) for (const node of released) releaseIfSettledUnobserved(node);
3886
4303
  // A19 exc. 2: a pass that went pending on a `refresh()` re-asks the
3887
4304
  // question already answered — quiet for the verdict; any other pending
3888
4305
  // pass is a new question. Settled, there is nothing to classify.
@@ -3911,15 +4328,15 @@ function recompute(el, create = false) {
3911
4328
  // seat that read none of the lane's world has left it: its result is the
3912
4329
  // frame's (a derivation whose branch no longer reaches the guess). A
3913
4330
  // guess is written, not derived — it never leaves this way.
3914
- let lane = passLane;
4331
+ let lane = !laneStageHook ? null : passLane;
3915
4332
  // Listed before its staging, a pending pass included (the lane's own
3916
4333
  // flight is the lane's); false: the pass left the lane (lanes.ts).
3917
- const errored = !!el._x?._error;
3918
- if (lane !== null) {
3919
- if (GlobalQueue._laneStage(el, lane, create, errored)) setPassLane(lane);
4334
+ const errored = !!el._x?._error || stagedErr;
4335
+ if (laneStageHook && lane !== null) {
4336
+ if (laneStageHook(el, lane, create, errored)) setPassLane(lane);
3920
4337
  else lane = null;
3921
4338
  }
3922
- if (!el._x?._error) {
4339
+ if (!errored) {
3923
4340
  // Observe-tier fan-in (HUGE_FAN_IN): the validated prefix [_deps.._depsTail]
3924
4341
  // IS this pass's distinct sources — count it here rather than per link. A
3925
4342
  // begin/end bracket around the pass plus a per-link increment measured
@@ -3946,17 +4363,9 @@ function recompute(el, create = false) {
3946
4363
  : el._config & CONFIG_OVERRIDE && el._x._lane !== NOT_PENDING
3947
4364
  ? el._x._lane
3948
4365
  : el._value;
3949
- let valueChanged = false;
3950
- try {
3951
- valueChanged =
3952
- (!isEffect && wasUninitialized) || !el._equals || !el._equals(compareValue, value);
3953
- } catch (e) {
3954
- // A throwing user comparator is an error of this node's computation.
3955
- // Route it through the same status path as a compute-phase throw so
3956
- // error boundaries contain it; otherwise it unwinds the scheduler
3957
- // flush, bypassing every boundary and wedging the queue (#2837).
3958
- notifyStatus(el, STATUS_ERROR, e);
3959
- }
4366
+ // A throwing comparator is contained inside `isSame` (A3, #2837; #4111).
4367
+ const valueChanged =
4368
+ (!isEffect && wasUninitialized) || !!el._x?._shownError || !isSame(el, compareValue, value);
3960
4369
  // A committed derived change becomes a cause for this node's subscribers,
3961
4370
  // chaining their attribution through this node to the root write.
3962
4371
  if (attrHooks !== null) {
@@ -4000,7 +4409,7 @@ function recompute(el, create = false) {
4000
4409
  !create &&
4001
4410
  lane === null &&
4002
4411
  el._config & CONFIG_GUESS &&
4003
- GlobalQueue._laneOutcome(el, value, errored)
4412
+ laneOutcomeHook(el, value, errored)
4004
4413
  );
4005
4414
  else if (errored);
4006
4415
  else if (valueChanged) {
@@ -4034,9 +4443,7 @@ function recompute(el, create = false) {
4034
4443
  create
4035
4444
  ? !(
4036
4445
  (flushTransaction !== null || passTx !== null) &&
4037
- (joined ||
4038
- (prevLane && el._flags & REACTIVE_STAGED_READ) ||
4039
- (creatorPass(oldcontext)?._flags ?? 0) & REACTIVE_JOINED)
4446
+ (joined || creatorPass(oldcontext)?._flags & REACTIVE_JOINED)
4040
4447
  )
4041
4448
  : isEffect && el._pendingValue === NOT_PENDING
4042
4449
  ) {
@@ -4065,8 +4472,12 @@ function recompute(el, create = false) {
4065
4472
  el._statusFlags |= STATUS_UNINITIALIZED;
4066
4473
  // A boundary that has not shown content catches it: its fallback
4067
4474
  // waits for the commit (#3540). (The pass's own status clear above
4068
- // dropped what the read recorded.)
4069
- GlobalQueue._fresh?.(el);
4475
+ // dropped what the read recorded.) Not when the flush has joined
4476
+ // the hold and the pass is its own (not a verdict lane's mount,
4477
+ // #3851): the mount is part of the hold, and the boundary appears
4478
+ // at its commit, its fallback never seen (D-13, U-3) — a `<Show>`
4479
+ // mounting it over a landing included.
4480
+ freshHook?.(el, !mount && flushTransaction);
4070
4481
  }
4071
4482
  }
4072
4483
  // insertSubs only walks _subs (no scheduling of its own), so a
@@ -4101,16 +4512,14 @@ function recompute(el, create = false) {
4101
4512
  // A first pass that went pending where the born-held arm would have held
4102
4513
  // its value (it read a held node, or a verdict lane's mount read a staging,
4103
4514
  // #3851) is held like it: its answer is the hold's, and the hold waits
4104
- // for it (A29, #3800) — unless a boundary that has not shown content
4105
- // catches it: then it is the boundary's, and the hold does not wait (#3540).
4106
- if (
4107
- create &&
4108
- lane === null &&
4109
- (joined || (prevLane && el._flags & REACTIVE_STAGED_READ)) &&
4110
- el._statusFlags & STATUS_PENDING
4111
- ) {
4515
+ // for it (A29, #3800). A boundary that has not shown content catches it
4516
+ // and it is held all the same — its answer reveals no earlier than the
4517
+ // hold's commit (D-12) — but the hold does not wait for it: its only
4518
+ // readers are behind the fallback (`_hidden`; F-4, #3540). A boundary
4519
+ // re-armed by `on` owns what it collects outright (D-14): not held.
4520
+ if (create && lane === null && joined && el._statusFlags & STATUS_PENDING) {
4112
4521
  const t = flushTransaction ?? passTx;
4113
- if (t !== null && !t._lane && !GlobalQueue._fresh?.(el)) holdNode(el, t);
4522
+ if (t && !t._lane && freshHook?.(el) !== CAUGHT_REARMED) holdNode(el, t);
4114
4523
  }
4115
4524
  // Dependencies are the committed frame's until it is replaced (A30, #3410):
4116
4525
  // a pass that staged its value leaves the previous pass's tail linked for
@@ -4222,12 +4631,12 @@ function computed(fn, options) {
4222
4631
  const loading = options !== null && typeof options === "object" && "loadingValue" in options;
4223
4632
  // Two literals, one per tier, selected at build time (the observe flag is
4224
4633
  // a literal after replacement; the untaken branch is dead code). The observe
4225
- // literal is the prod literal plus its `_name` slot — a slot in the
4226
- // boilerplate, because a post-construction `self._name = …` forces a
4634
+ // literal is the prod literal plus its `_name` and `_loc` slots — slots in
4635
+ // the boilerplate, because a post-construction `self._name = …` forces a
4227
4636
  // hidden-class transition and an out-of-object property store on EVERY node
4228
4637
  // (measured: the whole of the observe tier's creation overhead). Keep the
4229
4638
  // two in sync — the dist artifact test pins observe's key set to prod's
4230
- // plus `_name`.
4639
+ // plus the slots.
4231
4640
  const self = {
4232
4641
  id: inheritId(options, transparent, context),
4233
4642
  _config:
@@ -4269,6 +4678,7 @@ function computed(fn, options) {
4269
4678
  // The slot is always present (hidden class); plumbing leaves it
4270
4679
  // unset, which `ownerPath` skips.
4271
4680
  _name: options?._plumbing ? undefined : (options?.name ?? "computed"),
4681
+ _loc: options?.loc,
4272
4682
  // Observe: the verdict windows this pass entered (verdict.ts) — a
4273
4683
  // slot, so the stamp is never a write after construction.
4274
4684
  _devWindows: 0
@@ -4300,7 +4710,8 @@ function ext(el) {
4300
4710
  _flushedAt: -1,
4301
4711
  _q: 0,
4302
4712
  _lane: NOT_PENDING,
4303
- _marks: 0
4713
+ _marks: 0,
4714
+ _shownError: undefined
4304
4715
  });
4305
4716
  }
4306
4717
  /** A tracked read of a held node (the reader derives from its transaction's
@@ -4322,10 +4733,13 @@ function joinPass(c, el) {
4322
4733
  // the reader's position, not where the memo was created). Its catch, if
4323
4734
  // any, is at its staging.
4324
4735
  if (el._statusFlags & STATUS_UNINITIALIZED || c._type !== EFFECT_RENDER)
4325
- joinPassTx(
4326
- txOf(el),
4327
- (!c._type && c._statusFlags & STATUS_UNINITIALIZED) || GlobalQueue._fresh?.(c)
4328
- );
4736
+ joinPassTx(txOf(el), (!c._type && c._statusFlags & STATUS_UNINITIALIZED) || freshHook?.(c));
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));
4329
4743
  }
4330
4744
  /** A15's stale reader (shared-hole and reveal corollaries): a render effect
4331
4745
  * reading a node held by a transaction that is not the flush's. It is served
@@ -4362,10 +4776,11 @@ function frameRead(c, el) {
4362
4776
  // read the screen. Lane work with no committed value yet reads a flight
4363
4777
  // as a mount's memo does: it enters, and the boundary it mounts catches
4364
4778
  // the pending (#3540).
4365
- const verdict = c._config & CONFIG_VERDICT;
4779
+ // (Verdict readers and lanes exist only with their engines installed.)
4780
+ const verdict = observeFlightHook && c._config & CONFIG_VERDICT;
4781
+ const lane = laneStageHook && passLane !== null;
4366
4782
  if (
4367
- (passLane === null ||
4368
- (c._statusFlags & STATUS_UNINITIALIZED && el._statusFlags & STATUS_PENDING)) &&
4783
+ (!lane || (c._statusFlags & STATUS_UNINITIALIZED && el._statusFlags & STATUS_PENDING)) &&
4369
4784
  !verdict &&
4370
4785
  (c._type !== EFFECT_RENDER || el._config & CONFIG_INPUTS_PUBLISHED)
4371
4786
  )
@@ -4374,7 +4789,7 @@ function frameRead(c, el) {
4374
4789
  // Lane work and verdict readers are never the transaction's pass, whichever
4375
4790
  // flush runs it: a node T holds re-enters T when it re-runs (`recompute`'s
4376
4791
  // head), and the pass may then read a guess and become the lane's.
4377
- if ((t === flushTransaction && passLane === null && !verdict) || bornInto(c, t)) return false;
4792
+ if ((t === flushTransaction && !lane && !verdict) || bornInto(c, t)) return false;
4378
4793
  staleReader(c, t);
4379
4794
  return true;
4380
4795
  }
@@ -4461,6 +4876,7 @@ function createEffectNode(fn, effectFn, errorFn, type, options) {
4461
4876
  _type: type,
4462
4877
  _x: null,
4463
4878
  _name: options?.name ?? "effect",
4879
+ _loc: options?.loc,
4464
4880
  _devWindows: 0
4465
4881
  };
4466
4882
  // Effects dispatch status through the SHARED notifier (statusNotifierOf,
@@ -4512,7 +4928,7 @@ function setupComputedNode(self, options) {
4512
4928
  if (context) linkChild(context, self);
4513
4929
  DEV.hooks.onOwner?.(self);
4514
4930
  if (parent) self._height = parent._height + 1;
4515
- GlobalQueue._wireExternalSource?.(self);
4931
+ wireExternalSourceHook?.(self);
4516
4932
  !options?.lazy && recompute(self, true);
4517
4933
  if (snapshotCaptureActive && !options?.lazy) {
4518
4934
  if (!(self._statusFlags & STATUS_PENDING) && !(self._config & CONFIG_NO_SNAPSHOT)) {
@@ -4524,7 +4940,7 @@ function setupComputedNode(self, options) {
4524
4940
  }
4525
4941
  function signal(v, options) {
4526
4942
  // Prod and observe boilerplates — see computed(). The observe literal adds
4527
- // `_name` and `_owner` (the creating owner, stamped by registerGraph for
4943
+ // `_name`, `_loc` and `_owner` (the creating owner, stamped by registerGraph for
4528
4944
  // createSignal nodes so ownerPath can locate signal subjects; null here,
4529
4945
  // and staying null on internal signals — one shape either way).
4530
4946
  const s = {
@@ -4540,6 +4956,7 @@ function signal(v, options) {
4540
4956
  _notifiedAt: -1,
4541
4957
  _x: null,
4542
4958
  _name: options?.name ?? "signal",
4959
+ _loc: options?.loc,
4543
4960
  _owner: null
4544
4961
  };
4545
4962
  s._internal = false;
@@ -4563,7 +4980,8 @@ function isEqual(a, b) {
4563
4980
  * object, no NodeExtension, no post-construction expandos. `ownedWrite` is
4564
4981
  * baked in: the setter carries the owned-scope guard; node-level writes are
4565
4982
  * the store's notification machinery. The observe literal adds the label
4566
- * slot (the store relabels it `store.<key>` when the engine is installed). */
4983
+ * and location slots (the store relabels `store.<key>` and stamps the
4984
+ * store's `loc` when the engine is installed). */
4567
4985
  function slotSignal(v, equals, host, key, acc) {
4568
4986
  const s = {
4569
4987
  _equals: equals,
@@ -4581,6 +4999,7 @@ function slotSignal(v, equals, host, key, acc) {
4581
4999
  px: undefined,
4582
5000
  pxv: undefined,
4583
5001
  _name: "signal",
5002
+ _loc: undefined,
4584
5003
  _owner: null
4585
5004
  };
4586
5005
  s._internal = false;
@@ -4617,10 +5036,15 @@ function exitCallback() {
4617
5036
  }
4618
5037
  /**
4619
5038
  * Dev-only: > 0 while owner teardown runs cleanups (`_disposal` entries and
4620
- * effect-returned cleanups — owner.ts), for the same reason. Kept apart from
5039
+ * effect-returned cleanups — owner.ts), for the same reason. The owned-scope
5040
+ * write guard (setSignal, markRefresh, the store setter) stands down here
5041
+ * too: a cleanup is teardown, not part of whichever computation is disposing
5042
+ * its owner (a list pass removing a row, a branch switching away), so its
5043
+ * writes are imperative like an effect callback's (#4165). Kept apart from
4621
5044
  * `callbackDepth` because its sites cannot use try/finally (the frame would
4622
- * survive into prod): a throwing cleanup leaves it raised, and dev.ts resets
4623
- * 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.
4624
5048
  */
4625
5049
  let disposalDepth = 0;
4626
5050
  function enterDisposal() {
@@ -4644,6 +5068,16 @@ function setStrictRead(v) {
4644
5068
  * Use `untrack` inside a memo or effect when you need to read a signal once
4645
5069
  * without making the surrounding computation depend on its future changes.
4646
5070
  *
5071
+ * `untrack` stops dependency tracking, not async settlement. Reading an
5072
+ * uninitialized async source still suspends the owning computation; it runs
5073
+ * again when that source first resolves. Afterward, the untracked read is a
5074
+ * snapshot and does not subscribe the computation to later changes.
5075
+ *
5076
+ * If the first read must return immediately, give the async source a
5077
+ * `loadingValue` before reading it with `untrack`. `isPending` and `latest`
5078
+ * inspect updates to an existing value; neither supplies a missing initial
5079
+ * value.
5080
+ *
4647
5081
  * Pass a `strictReadLabel` string to enable a dev-mode warning: any reactive
4648
5082
  * read inside `fn` that isn't inside a nested tracking scope will log a
4649
5083
  * warning naming the label.
@@ -4657,17 +5091,12 @@ function setStrictRead(v) {
4657
5091
  * log(snapshot);
4658
5092
  * }
4659
5093
  * );
5094
+ *
5095
+ * const color = createMemo<string>(() => fetchColor(), { loadingValue: "gray" });
5096
+ * const initialColor = untrack(color); // "gray" while the fetch is unresolved
4660
5097
  * ```
4661
5098
  */
4662
5099
  function untrack(fn, strictReadLabel) {
4663
- if (
4664
- GlobalQueue._externalUntrack === null &&
4665
- !tracking &&
4666
- !strictRead &&
4667
- !strictReadLabel &&
4668
- asyncTailFlights === 0
4669
- )
4670
- return fn();
4671
5100
  const prevTracking = tracking;
4672
5101
  const prevStrictRead = strictRead;
4673
5102
  tracking = false;
@@ -4676,7 +5105,7 @@ function untrack(fn, strictReadLabel) {
4676
5105
  untrackDepth++;
4677
5106
  }
4678
5107
  try {
4679
- if (GlobalQueue._externalUntrack) return GlobalQueue._externalUntrack(fn);
5108
+ if (externalUntrackHook) return externalUntrackHook(fn);
4680
5109
  return fn();
4681
5110
  } finally {
4682
5111
  tracking = prevTracking;
@@ -4748,7 +5177,7 @@ function prepareComputed(comp, refresh) {
4748
5177
  * False means the reader derives from the staged value.
4749
5178
  */
4750
5179
  function readerSeesCommitted(el, c) {
4751
- return !!(!c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN);
5180
+ return !c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN;
4752
5181
  }
4753
5182
  /** A28 — set when a node is staged (queuePendingNode) OUTSIDE a flush;
4754
5183
  * cleared when the next flush begins. The read sites test this one module
@@ -4827,6 +5256,7 @@ function pullComputed(owner, c) {
4827
5256
  if (height >= c._height && owner._parent !== c) c._height = height + 1;
4828
5257
  }
4829
5258
  function read(el) {
5259
+ DEV.hooks.onRead?.(el);
4830
5260
  if (asyncTailFlights !== 0 && !tracking && untrackDepth === 0)
4831
5261
  checkPostAwaitRead(
4832
5262
  el,
@@ -4877,7 +5307,13 @@ function read(el) {
4877
5307
  if (!c || el._pendingValue === NOT_PENDING || c._config & CONFIG_CHILDREN_FORBIDDEN)
4878
5308
  return el._value;
4879
5309
  // (A plain signal is never uninitialized — the carve-out is `serve`'s.)
4880
- if (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;
4881
5317
  stagedRead(c);
4882
5318
  return el._pendingValue;
4883
5319
  }
@@ -4893,7 +5329,15 @@ function read(el) {
4893
5329
  if (c) {
4894
5330
  if (tracking) {
4895
5331
  link(el, c);
4896
- if (owner._fn) pullComputed(owner, c);
5332
+ if (owner._fn) {
5333
+ pullComputed(owner, c);
5334
+ // F-6 (#4050): a memo over a verdict reader's value derives from a
5335
+ // display-ahead read — a verdict reader from this pass's end, as if
5336
+ // it probed inline: it reads held values as the screen and shows the
5337
+ // verdict now. Not an effect: a render effect is a frame reader
5338
+ // already, and a user effect's posture is its own reads'.
5339
+ if (!c._type) c._flags |= owner._config & CONFIG_VERDICT;
5340
+ }
4897
5341
  }
4898
5342
  // L2, as in the fast block above (tracked or not). A node born into the
4899
5343
  // future (uninitialized) has no committed value: every reader derives
@@ -4910,8 +5354,9 @@ function read(el) {
4910
5354
  }
4911
5355
  // Lanes: a lane's node (after the pull — the node is current). NOT_PENDING
4912
5356
  // falls through: lane work's read of a pending member throws like any.
4913
- if (el._config & CONFIG_OVERRIDE) {
4914
- const v = GlobalQueue._laneRead(c, el);
5357
+ // (CONFIG_OVERRIDE is the lane engine's: no engine, no lane's node.)
5358
+ if (laneReadHook && el._config & CONFIG_OVERRIDE) {
5359
+ const v = laneReadHook(c, el);
4915
5360
  if (v !== NOT_PENDING) return v;
4916
5361
  }
4917
5362
  // A stale reader of a held flight (rule 3, A15's reveal corollary) is served
@@ -4936,9 +5381,11 @@ function read(el) {
4936
5381
  ) {
4937
5382
  // A reader with no committed value yet sees the flight pending: a
4938
5383
  // boundary a lane mounts shows its fallback (A29's boundary exemption).
4939
- if (passLane !== null && !(c?._statusFlags & STATUS_UNINITIALIZED)) committed = true;
4940
- else if (c !== null && c._config & CONFIG_VERDICT)
4941
- committed = GlobalQueue._observeFlight(c, owner);
5384
+ // (A lane, and a verdict reader, exist only with their engines installed.)
5385
+ if (laneStageHook && passLane !== null && !(c?._statusFlags & STATUS_UNINITIALIZED))
5386
+ committed = true;
5387
+ else if (observeFlightHook && c !== null && c._config & CONFIG_VERDICT)
5388
+ committed = observeFlightHook(c, owner);
4942
5389
  }
4943
5390
  if (owner._statusFlags & STATUS_PENDING && !committed) {
4944
5391
  // A reader landing on a pending node throws; an untracked read of an
@@ -5007,6 +5454,9 @@ function read(el) {
5007
5454
  }
5008
5455
  }
5009
5456
  if (committed) return el._value;
5457
+ // D-41: an untracked read reads the screen, and a shown error is on it
5458
+ // until the answer that replaces it commits.
5459
+ if (!c && el._x?._shownError) throw el._x._shownError;
5010
5460
  const value = serve(el, c);
5011
5461
  if (
5012
5462
  !c &&
@@ -5053,14 +5503,24 @@ function serve(el, c) {
5053
5503
  return u;
5054
5504
  }
5055
5505
  if (readerSeesCommitted(el, c)) return el._value;
5056
- if (c._config & CONFIG_VERDICT && !(el._statusFlags & STATUS_UNINITIALIZED) && stagedScreen(c))
5506
+ if (
5507
+ observeFlightHook &&
5508
+ c._config & CONFIG_VERDICT &&
5509
+ !(el._statusFlags & STATUS_UNINITIALIZED) &&
5510
+ stagedScreen(c)
5511
+ )
5057
5512
  return el._value;
5058
5513
  stagedRead(c, el);
5514
+ // D-41: the staged answer is an error (`stageError`).
5515
+ if (el._config & CONFIG_STAGED_ERROR) throw el._pendingValue;
5059
5516
  return el._pendingValue;
5060
5517
  }
5061
5518
  /** A pass read a staging of this flush: it derives from what the flush may
5062
5519
  * yet hold (REACTIVE_STAGED_READ — the seam decides). Lane work too (plan sec. 28, a
5063
- * 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
5064
5524
  * the frame commits — one pass, the common case — and a held write if it
5065
5525
  * parks, which the seam repairs (`stagedReaders`): the pass re-derives on
5066
5526
  * the committed world and its lane's runs wait that round, so the held
@@ -5073,7 +5533,7 @@ function serve(el, c) {
5073
5533
  function stagedRead(c, el) {
5074
5534
  if (!(el?._statusFlags & STATUS_UNINITIALIZED)) {
5075
5535
  c._flags |= REACTIVE_STAGED_READ;
5076
- if (passLane !== null) stagedReaders.push(c);
5536
+ if (laneSeamsHook && passLane !== null) stagedReaders.push(c);
5077
5537
  }
5078
5538
  }
5079
5539
  /** A10 for a staged node: a verdict reader (the pass entered a window) that
@@ -5123,11 +5583,46 @@ function captureWriteSnapshot(el, current) {
5123
5583
  el._config |= CONFIG_HAS_SNAPSHOT;
5124
5584
  snapshotSources.add(el);
5125
5585
  }
5586
+ /**
5587
+ * A callback run before every signal or store write lands, while set, with
5588
+ * the written signal or the store proxy whose setter runs. The
5589
+ * server-components client sets it while slot occurrences are left
5590
+ * unmounted after hydration (deferred hydration): a write may be one an
5591
+ * unmounted component would have observed, so the client mounts what is
5592
+ * left before the write applies, and clears the hook. `null` otherwise.
5593
+ */
5594
+ let writeHook = null;
5595
+ /** @internal */
5596
+ function setWriteHook(hook) {
5597
+ writeHook = hook;
5598
+ }
5599
+ /**
5600
+ * `el`'s comparator verdict (`_equals`), the one guarded call for recompute,
5601
+ * setSignal (user setters and async landings) and the seam's
5602
+ * staged-equals-committed check.
5603
+ * - A comparator that read an unresolved async source has not shown the
5604
+ * values equal (#4111): falsy, a change, whose readers suspend in their own
5605
+ * tracked context.
5606
+ * - Any other throw is an error of `el`'s computation (A3, #2837): routed
5607
+ * through the status path so error boundaries contain it, instead of
5608
+ * unwinding a flush or a setter; answered "same" so nothing commits.
5609
+ * @internal
5610
+ */
5611
+ function isSame(el, a, b) {
5612
+ if (el._equals === isEqual) return a === b;
5613
+ try {
5614
+ return el._equals && el._equals(a, b);
5615
+ } catch (e) {
5616
+ if (!(e instanceof NotReadyError)) return (notifyStatus(el, STATUS_ERROR, e), true);
5617
+ }
5618
+ }
5126
5619
  function setSignal(el, v) {
5620
+ if (writeHook !== null) writeHook(el);
5127
5621
  if (
5128
5622
  !(el._config & CONFIG_OWNED_WRITE) &&
5129
5623
  !(context && context._config & CONFIG_CHILDREN_FORBIDDEN) &&
5130
- context
5624
+ context &&
5625
+ !disposalDepth
5131
5626
  ) {
5132
5627
  emitDiagnostic({
5133
5628
  code: "REACTIVE_WRITE_IN_OWNED_SCOPE",
@@ -5146,7 +5641,7 @@ function setSignal(el, v) {
5146
5641
  // Lanes: a write landing on a guess is its truth (A18 — an async guess's
5147
5642
  // own landing), judged against the guess; one landing on a lane's
5148
5643
  // derivation (its flight) is the lane's staging (lanes.ts).
5149
- if (el._config & CONFIG_OVERRIDE) return GlobalQueue._laneWrite(el, v);
5644
+ if (laneWriteHook && el._config & CONFIG_OVERRIDE) return laneWriteHook(el, v);
5150
5645
  // L2 (T4) / A34 (1): a write to a node whose staged state is held is a
5151
5646
  // second proposal for the slot — the same value again or another — and
5152
5647
  // the two cannot finish at different times: the writer's tick joins the
@@ -5154,8 +5649,12 @@ function setSignal(el, v) {
5154
5649
  if (el._config & CONFIG_HELD) joinFuture(txOf(el));
5155
5650
  // Uninitialized check first: the first commit has no previous value, so the
5156
5651
  // user comparator must not run against `undefined` (matches recompute).
5652
+ // D-41: a write over a shown error is a change whatever the last good
5653
+ // value was — the error leaves at its commit (`commitPendingNode`).
5157
5654
  const valueChanged =
5158
- !!(el._statusFlags & STATUS_UNINITIALIZED) || !el._equals || !el._equals(currentValue, v);
5655
+ !!(el._statusFlags & STATUS_UNINITIALIZED) ||
5656
+ !!el._x?._shownError ||
5657
+ !isSame(el, currentValue, v);
5159
5658
  if (!valueChanged) return v;
5160
5659
  // Attribution hook: this committed write is where a re-run chain begins.
5161
5660
  if (attrHooks !== null) attrHooks.write(el, currentValue, v);
@@ -5213,8 +5712,21 @@ function setMemo(el, v) {
5213
5712
  !(el._flags & REACTIVE_MANUAL_WRITE) &&
5214
5713
  txOf(el) !== flushTransaction;
5215
5714
  // The writer read the committed frame; its updater composes on it.
5216
- if (held && typeof v === "function") v = v(el._value);
5217
- const result = setSignal(el, v);
5715
+ if (held && typeof v === "function") {
5716
+ const value = v(el._value);
5717
+ v = () => value;
5718
+ }
5719
+ // #4110: the write replaces a failed derivation's outcome. It lands without
5720
+ // re-running `_fn`, and nothing else clears STATUS_ERROR, so without this
5721
+ // every later read keeps throwing the error the derivation left behind.
5722
+ // The error stays the display until the write commits (D-41).
5723
+ if (el._statusFlags & STATUS_ERROR) {
5724
+ el._x._shownError = el._x._error;
5725
+ notifyStatus(el, STATUS_NONE, undefined);
5726
+ }
5727
+ v = setSignal(el, v);
5728
+ // A write replaces an error a pass staged (D-41): it is a value again.
5729
+ el._config &= ~CONFIG_STAGED_ERROR;
5218
5730
  if (held) {
5219
5731
  // The one write that re-runs: the staging it replaced was derived from
5220
5732
  // inputs the writer never saw, so the hold re-derives over the write.
@@ -5222,7 +5734,7 @@ function setMemo(el, v) {
5222
5734
  insertIntoHeap(el, dirtyQueue);
5223
5735
  schedule();
5224
5736
  } else if (el._pendingValue !== NOT_PENDING) el._flags |= REACTIVE_MANUAL_WRITE;
5225
- return result;
5737
+ return v;
5226
5738
  }
5227
5739
  /**
5228
5740
  * Executes `fn` with the given `owner` set as the current owner. Any reactive
@@ -5289,7 +5801,8 @@ function markRefresh(node) {
5289
5801
  if (
5290
5802
  context &&
5291
5803
  !((node._config ?? 0) & CONFIG_OWNED_WRITE) &&
5292
- !(context._config & CONFIG_CHILDREN_FORBIDDEN)
5804
+ !(context._config & CONFIG_CHILDREN_FORBIDDEN) &&
5805
+ !disposalDepth
5293
5806
  ) {
5294
5807
  emitDiagnostic({
5295
5808
  code: "REACTIVE_WRITE_IN_OWNED_SCOPE",
@@ -5323,175 +5836,196 @@ function markRefresh(node) {
5323
5836
  }
5324
5837
 
5325
5838
  export {
5326
- REACTIVE_CHECK as $,
5327
- list as A,
5328
- clock as B,
5839
+ setPassLane as $,
5840
+ tracking as A,
5841
+ CONFIG_CHILDREN_FORBIDDEN as B,
5329
5842
  CONFIG_GUESS as C,
5330
- insertSubs as D,
5331
- EFFECT_RENDER as E,
5332
- laneDirty as F,
5843
+ passLane as D,
5844
+ STATUS_PENDING as E,
5845
+ EFFECT_RENDER as F,
5333
5846
  GlobalQueue as G,
5334
- merge as H,
5335
- newTransaction as I,
5336
- reruns as J,
5337
- CONFIG_STAGED as K,
5338
- releaseQueues as L,
5339
- ownFlights as M,
5847
+ sameLane as H,
5848
+ flushTransaction as I,
5849
+ REACTIVE_SCREEN_READ as J,
5850
+ REACTIVE_FRAME_READ as K,
5851
+ attrHooks as L,
5852
+ commitPendingNode as M,
5340
5853
  NOT_PENDING as N,
5341
- setPassLane as O,
5342
- REACTIVE_JOINED as P,
5343
- REACTIVE_STAGED_READ as Q,
5854
+ REACTIVE_IN_HEAP as O,
5855
+ REACTIVE_DIRTY as P,
5856
+ REACTIVE_CHECK as Q,
5344
5857
  REACTIVE_DISPOSED as R,
5345
- STATUS_PENDING as S,
5346
- stagedRead as T,
5347
- CONFIG_SLOT_NODE as U,
5348
- attrHooks as V,
5858
+ STATUS_UNINITIALIZED as S,
5859
+ CONFIG_INPUTS_PUBLISHED as T,
5860
+ releaseQueues as U,
5861
+ CONFIG_SLOT_NODE as V,
5349
5862
  holdNode as W,
5350
5863
  joinFuture as X,
5351
- commitPendingNode as Y,
5352
- REACTIVE_IN_HEAP as Z,
5353
- REACTIVE_DIRTY as _,
5354
- CONFIG_OVERRIDE as a,
5355
- untrack as a$,
5356
- CONFIG_INPUTS_PUBLISHED as a0,
5357
- setVerdict as a1,
5358
- context as a2,
5359
- REACTIVE_PROBED as a3,
5360
- bornInto as a4,
5361
- globalQueue as a5,
5362
- staleReader as a6,
5864
+ reruns as Y,
5865
+ CONFIG_STAGED as Z,
5866
+ merge as _,
5867
+ NotReadyError as a,
5868
+ setMemo as a$,
5869
+ REACTIVE_JOINED as a0,
5870
+ REACTIVE_STAGED_READ as a1,
5871
+ stagedRead as a2,
5872
+ setVerdict as a3,
5873
+ context as a4,
5874
+ installVerdictHooks as a5,
5875
+ REACTIVE_PROBED as a6,
5363
5876
  verdict as a7,
5364
5877
  link as a8,
5365
5878
  pullComputed as a9,
5366
- createEffectNode as aA,
5367
- recompute as aB,
5368
- EFFECT_USER as aC,
5369
- reportDiagnostic as aD,
5370
- computed as aE,
5371
- staleValues as aF,
5372
- CONFIG_AUTO_DISPOSE as aG,
5373
- EFFECT_TRACKED as aH,
5374
- unwrapStatusError as aI,
5375
- haltReactivity as aJ,
5376
- setStrictRead as aK,
5377
- StatusError as aL,
5378
- trimStaleDeps as aM,
5379
- setTrackedQueueCallback as aN,
5380
- setEffectStatusNotify as aO,
5381
- _hitUnhandledAsync as aP,
5382
- resetUnhandledAsync as aQ,
5383
- ROOT_ERROR_HOOK as aR,
5384
- registerGraph as aS,
5385
- runWithOwner as aT,
5386
- setMemo as aU,
5387
- $REFRESH as aV,
5388
- markRefresh as aW,
5389
- createRoot as aX,
5390
- getObserver as aY,
5391
- dispose as aZ,
5392
- TimeoutError as a_,
5879
+ cleanup as aA,
5880
+ read as aB,
5881
+ DEV as aC,
5882
+ CONFIG_TRANSPARENT as aD,
5883
+ NoOwnerError as aE,
5884
+ ContextNotFoundError as aF,
5885
+ createEffectNode as aG,
5886
+ recompute as aH,
5887
+ EFFECT_USER as aI,
5888
+ reportDiagnostic as aJ,
5889
+ computed as aK,
5890
+ staleValues as aL,
5891
+ CONFIG_AUTO_DISPOSE as aM,
5892
+ EFFECT_TRACKED as aN,
5893
+ heldRunHook as aO,
5894
+ unwrapStatusError as aP,
5895
+ haltReactivityAt as aQ,
5896
+ setStrictRead as aR,
5897
+ StatusError as aS,
5898
+ trimStaleDeps as aT,
5899
+ setTrackedQueueCallback as aU,
5900
+ setEffectStatusNotify as aV,
5901
+ _hitUnhandledAsync as aW,
5902
+ resetUnhandledAsync as aX,
5903
+ ROOT_ERROR_HOOK as aY,
5904
+ registerGraph as aZ,
5905
+ runWithOwner as a_,
5393
5906
  strictRead as aa,
5394
5907
  warnStrictReadUntracked as ab,
5395
- joinPass as ac,
5396
- markLateLinker as ad,
5397
- spectating as ae,
5398
- STATUS_ERROR as af,
5399
- unflushedValue as ag,
5400
- stagedReaders as ah,
5401
- enterAction as ai,
5402
- setEffectCallback as aj,
5403
- exitAction as ak,
5404
- isThenable as al,
5405
- actionDepth as am,
5406
- flush as an,
5407
- setQuestion as ao,
5408
- emitDiagnostic as ap,
5409
- enterCallback as aq,
5410
- exitCallback as ar,
5411
- getOwner as as,
5412
- inEffectCallback as at,
5413
- signal as au,
5414
- setSignal as av,
5415
- cleanup as aw,
5416
- read as ax,
5417
- NoOwnerError as ay,
5418
- ContextNotFoundError as az,
5419
- CONFIG_VERDICT as b,
5420
- dirtyQueue as b0,
5421
- CONFIG_REDERIVE as b1,
5422
- passTx as b2,
5423
- CONFIG_FRESH_READ as b3,
5424
- createOwner as b4,
5425
- REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE as b5,
5426
- ownedScopeWriteMessage as b6,
5427
- DEV as b7,
5428
- isEqual as b8,
5429
- CONFIG_MANUAL_WRITE as b9,
5430
- peekNextChildId as bA,
5431
- releaseSnapshotScope as bB,
5432
- resetErrorHalt as bC,
5433
- setConsoleFooter as bD,
5434
- setSnapshotCapture as bE,
5435
- setAttributionHooks as bF,
5436
- liveRootOwners as bG,
5437
- ownerPath as bH,
5438
- isSuppressed as bI,
5439
- blockersOf as bJ,
5440
- CONFIG_PLUMBING as bK,
5441
- anyExcluded as bL,
5442
- isExcluded as bM,
5443
- CONFIG_WIDE as bN,
5444
- GRAPH_SIZE_WARN_AT as bO,
5445
- noteFanOut as bP,
5446
- CONFIG_SUPERSEDES as bQ,
5447
- snapshotCaptureActive as ba,
5448
- captureWriteSnapshot as bb,
5449
- notePromotedWrite as bc,
5450
- slotSignal as bd,
5451
- stagedScreen as be,
5452
- CONFIG_PROMOTED as bf,
5453
- setSlotUnobserved as bg,
5454
- joinPassTx as bh,
5455
- asyncTailFlights as bi,
5456
- untrackDepth as bj,
5457
- checkPostAwaitRead as bk,
5458
- insertIntoHeap as bl,
5459
- isDisposed as bm,
5460
- handleAsync as bn,
5461
- addPendingSource as bo,
5462
- propagateStatus as bp,
5463
- forEachDependent as bq,
5464
- settlePendingSource as br,
5465
- notifyEpoch as bs,
5466
- SUPPORTS_PROXY as bt,
5467
- REACTIVE_ZOMBIE as bu,
5468
- OBSERVE as bv,
5469
- clearSnapshots as bw,
5470
- enforceLoadingBoundary as bx,
5471
- getNextChildId as by,
5472
- markSnapshotScope as bz,
5473
- REACTIVE_PROBE_UNANSWERED as c,
5474
- blocked as d,
5475
- REACTIVE_LANE_DIRTY as e,
5476
- enqueueSub as f,
5477
- CONFIG_AUTHORITATIVE as g,
5478
- tracking as h,
5479
- CONFIG_CHILDREN_FORBIDDEN as i,
5480
- sameLane as j,
5908
+ staleReader as ac,
5909
+ globalQueue as ad,
5910
+ joinPass as ae,
5911
+ markLateLinker as af,
5912
+ spectating as ag,
5913
+ unflushedValue as ah,
5914
+ CONFIG_STAGED_ERROR as ai,
5915
+ stagedReaders as aj,
5916
+ bornInto as ak,
5917
+ enterAction as al,
5918
+ setEffectCallback as am,
5919
+ exitAction as an,
5920
+ isThenable as ao,
5921
+ actionDepth as ap,
5922
+ flush as aq,
5923
+ setQuestion as ar,
5924
+ emitDiagnostic as as,
5925
+ enterCallback as at,
5926
+ exitCallback as au,
5927
+ getOwner as av,
5928
+ inEffectCallback as aw,
5929
+ setExternalHooks as ax,
5930
+ signal as ay,
5931
+ setSignal as az,
5932
+ CONFIG_AUTHORITATIVE as b,
5933
+ liveRootOwners as b$,
5934
+ $REFRESH as b0,
5935
+ markRefresh as b1,
5936
+ createRoot as b2,
5937
+ getObserver as b3,
5938
+ dispose as b4,
5939
+ TimeoutError as b5,
5940
+ untrack as b6,
5941
+ dirtyQueue as b7,
5942
+ CONFIG_REDERIVE as b8,
5943
+ passTx as b9,
5944
+ settlePendingSource as bA,
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_,
5971
+ CONFIG_FRESH_READ as ba,
5972
+ createOwner as bb,
5973
+ writeHook as bc,
5974
+ REACTIVE_WRITE_IN_OWNED_SCOPE_SIGNAL_MESSAGE as bd,
5975
+ ownedScopeWriteMessage as be,
5976
+ isEqual as bf,
5977
+ CONFIG_MANUAL_WRITE as bg,
5978
+ snapshotCaptureActive as bh,
5979
+ captureWriteSnapshot as bi,
5980
+ notePromotedWrite as bj,
5981
+ slotSignal as bk,
5982
+ stagedScreen as bl,
5983
+ CONFIG_PROMOTED as bm,
5984
+ setSlotUnobserved as bn,
5985
+ installStoreHooks as bo,
5986
+ joinPassTx as bp,
5987
+ asyncTailFlights as bq,
5988
+ untrackDepth as br,
5989
+ checkPostAwaitRead as bs,
5990
+ disposalDepth as bt,
5991
+ insertIntoHeap as bu,
5992
+ isDisposed as bv,
5993
+ handleAsync as bw,
5994
+ addPendingSource as bx,
5995
+ propagateStatus as by,
5996
+ forEachDependent as bz,
5997
+ CONFIG_OVERRIDE as c,
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,
6008
+ newTransaction as d,
6009
+ enqueueSub as e,
6010
+ CONFIG_VERDICT as f,
6011
+ REACTIVE_PROBE_UNANSWERED as g,
6012
+ blocked as h,
6013
+ installLaneHooks as i,
6014
+ REACTIVE_LANE_DIRTY as j,
5481
6015
  CONFIG_HELD as k,
5482
- flushTransaction as l,
5483
- STATUS_UNINITIALIZED as m,
6016
+ clock as l,
6017
+ insertSubs as m,
5484
6018
  nextQuestion as n,
5485
- NotReadyError as o,
5486
- passLane as p,
6019
+ ownFlights as o,
6020
+ laneDirty as p,
5487
6021
  question as q,
5488
6022
  resolveTx as r,
5489
6023
  schedule as s,
5490
6024
  txOf as t,
5491
- REACTIVE_SCREEN_READ as u,
5492
- REACTIVE_FRAME_READ as v,
5493
- REACTIVE_LANE_READ as w,
5494
- ext as x,
5495
- REACTIVE_RECOMPUTING_DEPS as y,
5496
- queuePendingNode as z
6025
+ REACTIVE_LANE_READ as u,
6026
+ ext as v,
6027
+ REACTIVE_RECOMPUTING_DEPS as w,
6028
+ queuePendingNode as x,
6029
+ list as y,
6030
+ STATUS_ERROR as z
5497
6031
  };