@solidjs/signals 2.0.0-rc.2 → 2.0.0-rc.20

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 (198) hide show
  1. package/dist/dev-shared.js +6505 -0
  2. package/dist/dev.attribution.js +3484 -0
  3. package/dist/dev.js +10227 -9508
  4. package/dist/observe/affects-marks.js +86 -0
  5. package/dist/observe/affects.js +112 -0
  6. package/dist/observe/attribution.js +7 -0
  7. package/dist/observe/boundaries.js +696 -0
  8. package/dist/observe/core/action.js +182 -0
  9. package/dist/observe/core/async.js +824 -0
  10. package/dist/observe/core/attribution-costs.js +66 -0
  11. package/dist/observe/core/attribution-feedback.js +280 -0
  12. package/dist/observe/core/attribution-hooks.js +83 -0
  13. package/dist/observe/core/attribution-queries.js +43 -0
  14. package/dist/observe/core/attribution.js +3079 -0
  15. package/dist/observe/core/constants.js +269 -0
  16. package/dist/observe/core/context.js +63 -0
  17. package/dist/observe/core/copies.js +10 -0
  18. package/dist/observe/core/core.js +1707 -0
  19. package/dist/observe/core/dev.js +394 -0
  20. package/dist/observe/core/effect.js +193 -0
  21. package/dist/observe/core/error-hooks.js +71 -0
  22. package/dist/observe/core/error.js +82 -0
  23. package/dist/observe/core/external.js +97 -0
  24. package/dist/observe/core/graph.js +162 -0
  25. package/dist/observe/core/heap.js +124 -0
  26. package/dist/observe/core/identities.js +62 -0
  27. package/dist/observe/core/invariants.js +12 -0
  28. package/dist/observe/core/lanes.js +972 -0
  29. package/dist/observe/core/owner.js +371 -0
  30. package/dist/observe/core/scheduler.js +1157 -0
  31. package/dist/observe/core/truth.js +47 -0
  32. package/dist/observe/core/verdict.js +470 -0
  33. package/dist/observe/deferred.js +186 -0
  34. package/dist/observe/flatten.js +79 -0
  35. package/dist/observe/from.js +248 -0
  36. package/dist/observe/index.js +71 -0
  37. package/dist/observe/map.js +593 -0
  38. package/dist/observe/reveal.js +212 -0
  39. package/dist/observe/signals.js +717 -0
  40. package/dist/observe/store/affects.js +142 -0
  41. package/dist/observe/store/edges.js +39 -0
  42. package/dist/observe/store/hooks.js +16 -0
  43. package/dist/observe/store/index.js +47 -0
  44. package/dist/observe/store/optimistic.js +713 -0
  45. package/dist/observe/store/projection.js +345 -0
  46. package/dist/observe/store/reconcile.js +281 -0
  47. package/dist/observe/store/store.js +2127 -0
  48. package/dist/observe/store/storePath.js +103 -0
  49. package/dist/observe/store/target.js +46 -0
  50. package/dist/observe/store/types.js +83 -0
  51. package/dist/observe/store/utils.js +1063 -0
  52. package/dist/prod/affects-marks.js +86 -0
  53. package/dist/prod/affects.js +98 -112
  54. package/dist/prod/attribution.js +45 -0
  55. package/dist/prod/boundaries.js +578 -468
  56. package/dist/prod/core/action.js +92 -59
  57. package/dist/prod/core/async.js +522 -336
  58. package/dist/prod/core/attribution-hooks.js +3 -0
  59. package/dist/prod/core/constants.js +168 -44
  60. package/dist/prod/core/context.js +12 -16
  61. package/dist/prod/core/copies.js +10 -0
  62. package/dist/prod/core/core.js +1345 -627
  63. package/dist/prod/core/dev.js +123 -1
  64. package/dist/prod/core/effect.js +113 -80
  65. package/dist/prod/core/error-hooks.js +71 -0
  66. package/dist/prod/core/error.js +18 -4
  67. package/dist/prod/core/external.js +6 -7
  68. package/dist/prod/core/graph.js +119 -72
  69. package/dist/prod/core/heap.js +85 -109
  70. package/dist/prod/core/identities.js +62 -0
  71. package/dist/prod/core/invariants.js +1 -32
  72. package/dist/prod/core/lanes.js +931 -132
  73. package/dist/prod/core/owner.js +192 -129
  74. package/dist/prod/core/scheduler.js +948 -642
  75. package/dist/prod/core/truth.js +47 -0
  76. package/dist/prod/core/verdict.js +409 -420
  77. package/dist/prod/deferred.js +184 -0
  78. package/dist/prod/flatten.js +77 -0
  79. package/dist/prod/from.js +248 -0
  80. package/dist/prod/index.js +40 -16
  81. package/dist/prod/map.js +481 -211
  82. package/dist/prod/reveal.js +212 -0
  83. package/dist/prod/signals.js +401 -84
  84. package/dist/prod/store/affects.js +142 -0
  85. package/dist/prod/store/edges.js +39 -0
  86. package/dist/prod/store/hooks.js +16 -0
  87. package/dist/prod/store/index.js +27 -18
  88. package/dist/prod/store/optimistic.js +707 -0
  89. package/dist/prod/store/projection.js +331 -0
  90. package/dist/prod/store/reconcile.js +281 -0
  91. package/dist/prod/store/store.js +1952 -267
  92. package/dist/prod/store/storePath.js +1 -1
  93. package/dist/prod/store/target.js +43 -0
  94. package/dist/prod/store/types.js +83 -0
  95. package/dist/prod/store/utils.js +997 -138
  96. package/dist/types/affects-marks.d.ts +14 -0
  97. package/dist/types/affects.d.ts +12 -30
  98. package/dist/types/attribution.d.ts +19 -0
  99. package/dist/types/attribution.prod.d.ts +22 -0
  100. package/dist/types/boundaries.d.ts +99 -124
  101. package/dist/types/carved.d.ts +1 -0
  102. package/dist/types/core/action.d.ts +23 -9
  103. package/dist/types/core/async.d.ts +34 -6
  104. package/dist/types/core/attribution-costs.d.ts +35 -0
  105. package/dist/types/core/attribution-feedback.d.ts +133 -0
  106. package/dist/types/core/attribution-hooks.d.ts +307 -8
  107. package/dist/types/core/attribution-queries.d.ts +24 -0
  108. package/dist/types/core/attribution.d.ts +802 -72
  109. package/dist/types/core/constants.d.ts +195 -38
  110. package/dist/types/core/copies.d.ts +54 -0
  111. package/dist/types/core/core.d.ts +295 -65
  112. package/dist/types/core/dev.d.ts +509 -22
  113. package/dist/types/core/effect.d.ts +1 -1
  114. package/dist/types/core/error-hooks.d.ts +71 -0
  115. package/dist/types/core/error.d.ts +18 -1
  116. package/dist/types/core/graph.d.ts +40 -1
  117. package/dist/types/core/heap.d.ts +5 -5
  118. package/dist/types/core/identities.d.ts +17 -0
  119. package/dist/types/core/index.d.ts +11 -7
  120. package/dist/types/core/invariants.d.ts +1 -26
  121. package/dist/types/core/lanes.d.ts +74 -43
  122. package/dist/types/core/owner.d.ts +27 -9
  123. package/dist/types/core/scheduler.d.ts +353 -102
  124. package/dist/types/core/truth.d.ts +18 -0
  125. package/dist/types/core/types.d.ts +97 -71
  126. package/dist/types/core/verdict.d.ts +35 -0
  127. package/dist/types/deferred.d.ts +28 -0
  128. package/dist/types/flatten.d.ts +8 -0
  129. package/dist/types/from.d.ts +47 -0
  130. package/dist/types/index.d.ts +33 -7
  131. package/dist/types/map.d.ts +4 -2
  132. package/dist/types/reveal.d.ts +19 -0
  133. package/dist/types/signals.d.ts +196 -45
  134. package/dist/types/store/affects.d.ts +12 -0
  135. package/dist/types/store/edges.d.ts +9 -0
  136. package/dist/types/store/hooks.d.ts +62 -0
  137. package/dist/types/store/index.d.ts +42 -15
  138. package/dist/types/store/optimistic.d.ts +24 -45
  139. package/dist/types/store/projection.d.ts +17 -68
  140. package/dist/types/store/reconcile.d.ts +3 -46
  141. package/dist/types/store/store.d.ts +133 -136
  142. package/dist/types/store/storePath.d.ts +1 -1
  143. package/dist/types/store/target.d.ts +166 -0
  144. package/dist/types/store/types.d.ts +105 -0
  145. package/dist/types/store/utils.d.ts +188 -0
  146. package/package.json +31 -26
  147. package/dist/node.cjs +0 -8745
  148. package/dist/prod/core/optimistic.js +0 -265
  149. package/dist/prod/store/next/optimistic.js +0 -385
  150. package/dist/prod/store/next/projection.js +0 -172
  151. package/dist/prod/store/next/reconcile.js +0 -331
  152. package/dist/prod/store/next/store.js +0 -1499
  153. package/dist/prod/store/next/target.js +0 -20
  154. package/dist/types/core/optimistic.d.ts +0 -6
  155. package/dist/types/store/next/optimistic.d.ts +0 -20
  156. package/dist/types/store/next/projection.d.ts +0 -8
  157. package/dist/types/store/next/reconcile.d.ts +0 -3
  158. package/dist/types/store/next/store.d.ts +0 -77
  159. package/dist/types/store/next/target.d.ts +0 -117
  160. package/dist/types-cjs/affects.d.cts +0 -47
  161. package/dist/types-cjs/boundaries.d.cts +0 -175
  162. package/dist/types-cjs/core/action.d.cts +0 -64
  163. package/dist/types-cjs/core/async.d.cts +0 -21
  164. package/dist/types-cjs/core/attribution-hooks.d.cts +0 -52
  165. package/dist/types-cjs/core/attribution.d.cts +0 -186
  166. package/dist/types-cjs/core/constants.d.cts +0 -97
  167. package/dist/types-cjs/core/context.d.cts +0 -36
  168. package/dist/types-cjs/core/core.d.cts +0 -163
  169. package/dist/types-cjs/core/dev.d.cts +0 -81
  170. package/dist/types-cjs/core/effect.d.cts +0 -30
  171. package/dist/types-cjs/core/error.d.cts +0 -47
  172. package/dist/types-cjs/core/external.d.cts +0 -15
  173. package/dist/types-cjs/core/graph.d.cts +0 -6
  174. package/dist/types-cjs/core/heap.d.cts +0 -22
  175. package/dist/types-cjs/core/index.d.cts +0 -13
  176. package/dist/types-cjs/core/invariants.d.cts +0 -59
  177. package/dist/types-cjs/core/lanes.d.cts +0 -44
  178. package/dist/types-cjs/core/optimistic.d.cts +0 -6
  179. package/dist/types-cjs/core/owner.d.cts +0 -124
  180. package/dist/types-cjs/core/scheduler.d.cts +0 -180
  181. package/dist/types-cjs/core/types.d.cts +0 -192
  182. package/dist/types-cjs/core/verdict.d.cts +0 -2
  183. package/dist/types-cjs/index.d.cts +0 -10
  184. package/dist/types-cjs/map.d.cts +0 -69
  185. package/dist/types-cjs/package.json +0 -3
  186. package/dist/types-cjs/signals.d.cts +0 -624
  187. package/dist/types-cjs/store/index.d.cts +0 -19
  188. package/dist/types-cjs/store/next/optimistic.d.cts +0 -20
  189. package/dist/types-cjs/store/next/projection.d.cts +0 -8
  190. package/dist/types-cjs/store/next/reconcile.d.cts +0 -3
  191. package/dist/types-cjs/store/next/store.d.cts +0 -77
  192. package/dist/types-cjs/store/next/target.d.cts +0 -117
  193. package/dist/types-cjs/store/optimistic.d.cts +0 -45
  194. package/dist/types-cjs/store/projection.d.cts +0 -70
  195. package/dist/types-cjs/store/reconcile.d.cts +0 -46
  196. package/dist/types-cjs/store/store.d.cts +0 -138
  197. package/dist/types-cjs/store/storePath.d.cts +0 -58
  198. package/dist/types-cjs/store/utils.d.cts +0 -74
@@ -0,0 +1,3079 @@
1
+ import { setAttributionHooks } from "./attribution-hooks.js";
2
+
3
+ import { NOT_PENDING, CONFIG_PLUMBING, CONFIG_KEY_BAG, CONFIG_OVERRIDE, CONFIG_GUESS, CONFIG_VERDICT } from "./constants.js";
4
+
5
+ import { OBSERVE, liveRootOwners, emitDiagnostic, ownerPath, reportDiagnostic, isSuppressed, anyExcluded, isExcluded, GRAPH_SIZE_WARN_AT, noteFanOut } from "./dev.js";
6
+
7
+ import { blockersOf, GlobalQueue } from "./scheduler.js";
8
+
9
+ /** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */ const FALLBACK_FLASH_MS = 150;
10
+
11
+ /**
12
+ * Under an owner the observer marked as its own (`OBSERVE.exclude`, and not
13
+ * re-admitted by a nearer `OBSERVE.include`), or framework plumbing itself
14
+ * (`CONFIG_PLUMBING` — the HMR memo between a component's root and its
15
+ * body, which is nobody's node): the engine records nothing about the node.
16
+ * Plumbing is a bit read and excludes the node alone, not what it owns; the
17
+ * observer exclusion is cached per node once any exists, before that a flag
18
+ * read.
19
+ */ function excludedNode(e) {
20
+ if ((e.l & CONFIG_PLUMBING) !== 0) return true;
21
+ if (!anyExcluded()) return false;
22
+ const n = e;
23
+ if (n.he === undefined) n.he = isExcluded(e);
24
+ return n.he;
25
+ }
26
+
27
+ let attributionActive = false;
28
+
29
+ let changeSeq = 0;
30
+
31
+ let runSeq = 0;
32
+
33
+ const defaultOptions = {
34
+ log: true,
35
+ // Tier-dependent (see `AttributionOptions.values`): dev output is for the
36
+ // developer at the console and shows everything; an observe build is a
37
+ // production artifact and carries no user data unless a holder asks. The
38
+ // literal folds per build — the observe engine ships `"none"` only.
39
+ values: "none",
40
+ checks: true,
41
+ stacks: false,
42
+ historyLimit: 200,
43
+ hotRuns: {
44
+ count: 120,
45
+ windowMs: 1e3
46
+ },
47
+ wideDeps: 200,
48
+ hotTime: {
49
+ budgetMs: 8,
50
+ windowMs: 1e3
51
+ },
52
+ unstableMemos: 4,
53
+ wastedRecompute: {
54
+ minRuns: 5,
55
+ ratio: .8,
56
+ budgetMs: 2,
57
+ windowMs: 1e3
58
+ },
59
+ fanOut: 250,
60
+ waterfalls: {
61
+ minFlightMs: 50
62
+ },
63
+ holds: {
64
+ infoMs: 100,
65
+ warnMs: 200
66
+ },
67
+ longHolds: {
68
+ infoMs: 500,
69
+ warnMs: 1e3
70
+ },
71
+ graphGrowth: {
72
+ visits: 3,
73
+ ratio: 1.25
74
+ },
75
+ abandonedFlights: {
76
+ count: 3,
77
+ windowMs: 1e3
78
+ },
79
+ fallbackFlashes: true,
80
+ optimisticReverts: true,
81
+ stackedHolds: {
82
+ count: 3
83
+ }
84
+ };
85
+
86
+ let options = {
87
+ ...defaultOptions
88
+ };
89
+
90
+ let history = [];
91
+
92
+ /**
93
+ * The engine's records go out on the core's channel, `OBSERVE.records` —
94
+ * the one every runtime emits on — under the types `RecordTypes` declares
95
+ * for it (`rerun`, `create`, `effect`, `flush`, `flight`, `fallback`,
96
+ * `interaction`, `hold`, `navigation`, `graph`), each with the live node as
97
+ * `live` where the record has one. `records.observed(type)` is the
98
+ * pre-check the listener-gated records cost nothing without; the channel
99
+ * is process-wide and the subscriptions on it are the consumer's, so the
100
+ * engine neither holds nor clears listeners of its own.
101
+ */ const records = OBSERVE.records;
102
+
103
+ /** @internal The engine's clock: `performance.now()` where it exists. */ const now = typeof performance !== "undefined" ? () => performance.now() : () => Date.now();
104
+
105
+ const frames = [];
106
+
107
+ const folds = [];
108
+
109
+ /** @internal */ function registerFold(e) {
110
+ folds.push(e);
111
+ }
112
+
113
+ /** @internal Root cause names of a cause chain — the writes/landings/refreshes the chain bottoms out in. */ function rootsOf(e, n) {
114
+ for (const t of e) {
115
+ if (t.kind === "derived" && t.causes && t.causes.length > 0) rootsOf(t.causes, n); else n.add(t.name);
116
+ }
117
+ }
118
+
119
+ function nodeName(e) {
120
+ return e._name ?? "anonymous";
121
+ }
122
+
123
+ /** The record vocabulary for a computation's kind: effects carry a `_type`, memos do not. */ function nodeKind(e) {
124
+ return e.i ? "effect" : "memo";
125
+ }
126
+
127
+ /**
128
+ * Whether a re-run record has an audience — a listener on the channel, an
129
+ * imported fold (`costs`/`feedback`), or the console log. Read at recompute
130
+ * START (the subscription diff needs the deps as they were), so a listener
131
+ * arriving mid-run hears the next one. Without an audience the engine still
132
+ * runs every check and keeps every per-node fact; only the record — its
133
+ * cause list copy, dep diff, previews — is not built, and `history("rerun")`
134
+ * stays empty.
135
+ */ function wantsRerun() {
136
+ return options.log || folds.length > 0 || records.observed("rerun");
137
+ }
138
+
139
+ /**
140
+ * A short preview of a written value for a record — only ever called under
141
+ * `values: "full"` (`stampWrite`, OPTIMISTIC_REVERTED): the other levels
142
+ * carry no previews, and the check is made at the call site so the value is
143
+ * not even looked at.
144
+ */ function preview(e) {
145
+ if (e === null) return "null";
146
+ switch (typeof e) {
147
+ case "undefined":
148
+ return "undefined";
149
+
150
+ case "string":
151
+ return JSON.stringify(e.length > 40 ? e.slice(0, 40) + "…" : e);
152
+
153
+ case "number":
154
+ case "boolean":
155
+ case "bigint":
156
+ return String(e);
157
+
158
+ case "function":
159
+ return "[function]";
160
+
161
+ case "symbol":
162
+ return e.toString();
163
+
164
+ default:
165
+ return Array.isArray(e) ? `Array(${e.length})` : `[${e.constructor?.name ?? "object"}]`;
166
+ }
167
+ }
168
+
169
+ function captureStack() {
170
+ if (!options.stacks) return undefined;
171
+ const e = (new Error).stack?.split("\n") ?? [];
172
+ // Drop the message line and every frame inside the reactive core; the first
173
+ // remaining frames are the user code that performed the write.
174
+ return e.slice(1).filter(e => !/(?:^|[/\\])(?:packages[/\\])?signals[/\\](src|dist)[/\\]/.test(e)).slice(0, 3).map(e => e.trim());
175
+ }
176
+
177
+ /** Sentinel for "no value transition to record" (refresh() stamps). */ const NO_VALUES = Symbol("no-values");
178
+
179
+ // --- Provenance -------------------------------------------------------------
180
+
181
+ // Who performed a write is not a graph fact — the graph only sees the write.
182
+ // The engine keeps an ambient answer: a stack of imperative frames the core
183
+ // announces (effect callbacks, action steps) and the interaction the web
184
+ // runtime declares around event dispatch. A write stamps the innermost frame;
185
+ // frames nested under an interaction carry it. Effects run in a later flush
186
+ // than the click that caused them, so their frame inherits the interaction
187
+ // from the run's cause chain instead (recorded at recomputeEnd).
188
+ const EXTERNAL_ORIGIN = {
189
+ kind: "external"
190
+ };
191
+
192
+ const originFrames = [];
193
+
194
+ /** Per action invocation (keyed by its iterator): the interaction its first step ran under. */ const actionInteractions = new WeakMap;
195
+
196
+ /**
197
+ * The interaction frame the core's `withInteraction` opened (via the
198
+ * `interactionStart`/`interactionEnd` hooks) for the duration of a handler.
199
+ * Frames nest strictly, so the enclosing one is kept on a stack to restore.
200
+ * The core pins the engine per frame: an `interactionEnd` can arrive after
201
+ * `disable()` cleared the stack, so popping an empty stack is tolerated.
202
+ */ let currentInteraction = null;
203
+
204
+ const interactionStack = [];
205
+
206
+ /**
207
+ * The element label an interaction record carries, from the one the runtime
208
+ * described (`tag`, then `#id` or `[name=…]`, then the element's text in
209
+ * quotes — `button#next "Next →"`), cut to what `options.values` allows: the
210
+ * text is the part after the first ` "`; under `"labels"` it stays on a
211
+ * `button` or an `a`, under `"none"` never. The ref's own string is never
212
+ * mutated — the runtime's object is the runtime's.
213
+ */ function targetLabel(e) {
214
+ const n = options.values;
215
+ if (n === "full") return e;
216
+ const t = e.indexOf(' "');
217
+ if (t === -1) return e;
218
+ if (n === "labels") {
219
+ const n = e.slice(0, t);
220
+ const o = n.search(/[#[]/);
221
+ const i = o === -1 ? n : n.slice(0, o);
222
+ if (i === "button" || i === "a") return e;
223
+ }
224
+ return e.slice(0, t);
225
+ }
226
+
227
+ function interactionStart(e) {
228
+ interactionStack.push(currentInteraction);
229
+ // No `ref.event`: the WeakMap answers undefined.
230
+ const n = eventInteractions.get(e.event);
231
+ if (n?.joinable && openInteractions.has(n)) {
232
+ n.open = true;
233
+ currentInteraction = n.event.origin;
234
+ return;
235
+ }
236
+ // One clock read: the frame opens now; the interaction began at `ref.at`
237
+ // when the runtime dated it (the event's own timestamp), else now too.
238
+ const t = now();
239
+ const o = {
240
+ kind: "interaction",
241
+ name: e.type,
242
+ at: e.at ?? t
243
+ };
244
+ if (e.target) o.target = targetLabel(e.target);
245
+ currentInteraction = o;
246
+ openInteraction(o, t, e.event);
247
+ }
248
+
249
+ function interactionEnd(e) {
250
+ const n = currentInteraction;
251
+ currentInteraction = interactionStack.length ? interactionStack.pop() : null;
252
+ if (n !== null) closeInteraction(n, e);
253
+ }
254
+
255
+ /** The interaction an origin runs under (itself, when it is one). */ function interactionOf(e) {
256
+ if (e === undefined) return undefined;
257
+ return e.kind === "interaction" ? e : e.interaction;
258
+ }
259
+
260
+ /** The interaction a cause list traces back to — root writes only, derived links walked. */ function interactionIn(e) {
261
+ for (const n of e) {
262
+ const e = n.kind === "derived" ? n.causes !== undefined ? interactionIn(n.causes) : undefined : interactionOf(n.origin);
263
+ if (e !== undefined) return e;
264
+ }
265
+ return undefined;
266
+ }
267
+
268
+ function currentOrigin() {
269
+ const e = originFrames[originFrames.length - 1];
270
+ if (e !== undefined) return e;
271
+ return currentInteraction ?? EXTERNAL_ORIGIN;
272
+ }
273
+
274
+ /** The root origin a cause list traces back to — the stamp of the nearest root write, derived links walked. */ function originIn(e) {
275
+ for (const n of e) {
276
+ const e = n.kind === "derived" ? n.causes !== undefined ? originIn(n.causes) : undefined : n.origin;
277
+ if (e !== undefined && e.kind !== "external") return e;
278
+ }
279
+ return undefined;
280
+ }
281
+
282
+ /**
283
+ * The `currentOrigin` hook: what a runtime recording its own fact right now
284
+ * (a server-function call) should stamp it with. Inside a recompute the
285
+ * fact belongs to the change that caused the run — a `createAsync` calling
286
+ * the server on a navigation's write is the navigation's, and through it the
287
+ * click's — walked down past create runs the way `trackFlightStart` does,
288
+ * since a node born inside a parent's run inherits the parent's causality.
289
+ * Outside one — or when the causes were themselves external (a memo a
290
+ * handler pulls, stale from a timer's write) — it is the write's answer
291
+ * (`currentOrigin`), minus the external sentinel: "none known" is
292
+ * `undefined` on a record, as `HoldEvent.origin` has it. The objects
293
+ * returned are the engine's own frames, so the caller's record joins
294
+ * `InteractionEvent.origin` / `NavigationEvent.origin` by identity.
295
+ */ function ambientOrigin() {
296
+ for (let e = frames.length - 1; e >= 0; e--) {
297
+ const n = frames[e].causes;
298
+ if (n !== null) {
299
+ const e = originIn(n);
300
+ if (e !== undefined) return e;
301
+ break;
302
+ }
303
+ }
304
+ const e = currentOrigin();
305
+ return e === EXTERNAL_ORIGIN ? undefined : e;
306
+ }
307
+
308
+ const effectFrames = new WeakMap;
309
+
310
+ /**
311
+ * The nodes of the open effect frames, innermost last — parallel to the
312
+ * `effect` entries of `originFrames`, so the frame on top resolves to its
313
+ * node without a map. A stack, not a single slot: a render effect created
314
+ * inside a callback runs its own callback synchronously.
315
+ */ const effectStack = [];
316
+
317
+ /**
318
+ * A write just stamped `origin`. If it is the innermost effect frame (a
319
+ * root write's origin is always the innermost open frame, see `stampWrite`)
320
+ * and no earlier write registered it, register it now with the node on top
321
+ * of the effect stack. An origin that is an effect frame but not the top is
322
+ * inherited — reached through an earlier write's record — and that write
323
+ * registered it.
324
+ */ function noteEffectOrigin(e) {
325
+ if (e.kind !== "effect" || effectFrames.has(e)) return;
326
+ if (originFrames[originFrames.length - 1] !== e) return;
327
+ const n = effectStack[effectStack.length - 1];
328
+ if (n !== undefined) effectFrames.set(e, {
329
+ node: n,
330
+ causes: n.pe
331
+ });
332
+ }
333
+
334
+ /** The interaction the innermost open frame runs under, else the ambient one. */ function enclosingInteraction() {
335
+ const e = originFrames[originFrames.length - 1];
336
+ return (e !== undefined ? interactionOf(e) : undefined) ?? currentInteraction ?? undefined;
337
+ }
338
+
339
+ function pushFrame(e, n, t, o) {
340
+ const i = {
341
+ kind: e
342
+ };
343
+ if (n) i.name = n;
344
+ const s = t ?? currentInteraction ?? undefined;
345
+ if (s !== undefined) i.interaction = s;
346
+ if (o !== undefined) {
347
+ const e = o;
348
+ if (e.me !== undefined) i.run = e.me;
349
+ // The frame → node map is filled by the first write inside the callback
350
+ // (noteEffectOrigin), not here: most callbacks never write.
351
+ effectStack.push(o);
352
+ }
353
+ originFrames.push(i);
354
+ }
355
+
356
+ function popFrame(e) {
357
+ // Frames are strictly nested; a mismatch means enable() landed mid-frame
358
+ // (the opener never pushed) — leave the stack alone rather than pop a stranger.
359
+ const n = originFrames[originFrames.length - 1];
360
+ if (n !== undefined && n.kind === e) {
361
+ originFrames.pop();
362
+ if (e === "effect") effectStack.pop();
363
+ }
364
+ }
365
+
366
+ /**
367
+ * `withOrigin` opened a declared frame. The frame object IS the origin every
368
+ * write inside stamps, and the key the navigation record hangs off (see
369
+ * "Navigations" below), so a hold or re-run that later resolves a write's
370
+ * origin lands on the same record. It runs under the interaction of the frame
371
+ * it opened inside — a `navigate()` from an action step, whose ambient
372
+ * interaction is long gone but whose frame remembers it — else the ambient
373
+ * one (a link click's handler).
374
+ */ function originStart(e) {
375
+ // A redirect hop re-enters the pending navigation's frame — the same object,
376
+ // so its writes stamp the same origin and replace the pending write without
377
+ // superseding it (see "Navigations").
378
+ if (e.redirect !== undefined && e.redirect > 0) {
379
+ const n = lastOpenNavigation();
380
+ if (n !== undefined) {
381
+ redirectNavigation(n, e);
382
+ originFrames.push(n.event.origin);
383
+ return;
384
+ }
385
+ }
386
+ // The initial navigation is the document's: its request is the time origin
387
+ // unless the router says otherwise, and it left nowhere.
388
+ const n = e.initial === true;
389
+ const t = {
390
+ kind: e.kind,
391
+ at: e.at ?? (n ? 0 : now())
392
+ };
393
+ if (!n && e.from !== undefined) t.from = e.from;
394
+ // Declared beats ambient: a router that awaited before writing hands back
395
+ // the origin it captured in the request (`undefined` when the request ran
396
+ // under none — still a declaration); what is on the stack now is whatever
397
+ // happened to be running.
398
+ const o = "interaction" in e ? interactionOf(e.interaction) : enclosingInteraction();
399
+ if (o !== undefined) t.interaction = o;
400
+ originFrames.push(t);
401
+ openNavigation(t, e);
402
+ }
403
+
404
+ function originEnd() {
405
+ const e = originFrames[originFrames.length - 1];
406
+ popFrame("navigation");
407
+ if (e !== undefined && e.kind === "navigation") closeNavigation(e);
408
+ }
409
+
410
+ /** `click on button#next "Next →"`, `effect "syncTitle"`, `action "save"`, `navigation to /users/:id`, … */ function formatOrigin(e) {
411
+ switch (e.kind) {
412
+ case "interaction":
413
+ return `${e.name} on ${e.target ?? "an element"}`;
414
+
415
+ case "effect":
416
+ return `effect${e.name ? ` "${e.name}"` : ""}`;
417
+
418
+ case "action":
419
+ return `action${e.name ? ` "${e.name}"` : ""}`;
420
+
421
+ case "async":
422
+ return `async landing${e.name ? ` on "${e.name}"` : ""}`;
423
+
424
+ case "navigation":
425
+ {
426
+ // The route pattern is the name consumers group by; the concrete path
427
+ // follows when it adds information, then the destinations a redirect
428
+ // chain abandoned on the way.
429
+ const n = e.name ?? e.to;
430
+ if (n === undefined) return "navigation";
431
+ const t = [];
432
+ if (e.to !== undefined && e.to !== n) t.push(e.to);
433
+ const o = navStates.get(e)?.event.redirects;
434
+ if (o !== undefined) t.push(`redirected from ${o.map(e => e.to ?? e.name ?? "?").join(" → ")}`);
435
+ const i = navStates.get(e)?.event.initial === true;
436
+ return `${i ? "initial " : ""}navigation to ${n}${t.length > 0 ? ` (${t.join(", ")})` : ""}`;
437
+ }
438
+
439
+ default:
440
+ return "outside the reactive system";
441
+ }
442
+ }
443
+
444
+ /** Record a root change (setSignal / refresh / async landing) on the node. */
445
+ /**
446
+ * HUGE_FAN_OUT from the engine's lower `fanOut` threshold (see dev.ts): a
447
+ * committed root invalidation reaching hundreds of subscribers re-runs all
448
+ * of them this flush. Counts the subscriber list itself (an engine-only
449
+ * walk, on the write; the core keeps no per-node count — a live `_subCount`
450
+ * was a post-construction field that forked node shapes). Stops at
451
+ * GRAPH_SIZE_WARN_AT, where the always-on core check takes over, so the two
452
+ * never fire for the same write; the once-per-node dedupe is the core's.
453
+ * HMR plumbing subscribers (the refresh memo of every mounted instance, on
454
+ * a component's registration signal) are not counted toward `fanOut`; the
455
+ * handover is on the full list, which is what the core counts.
456
+ */ function checkFanOut(e, n) {
457
+ const t = options.fanOut;
458
+ if (typeof t !== "number") return;
459
+ let o = 0;
460
+ let i = 0;
461
+ for (let n = e.o; n !== null; n = n.t) {
462
+ o++;
463
+ if (!(n.u.l & CONFIG_PLUMBING)) i++;
464
+ }
465
+ if (i < t || o >= GRAPH_SIZE_WARN_AT) return;
466
+ noteFanOut(e, i, n);
467
+ }
468
+
469
+ function stampWrite(e, n, t = NO_VALUES, o = NO_VALUES) {
470
+ const i = {
471
+ seq: ++changeSeq,
472
+ kind: n,
473
+ name: nodeName(e),
474
+ nodeId: devId(e)
475
+ };
476
+ // The previews are the record's one look at the written values: under
477
+ // any level but `"full"` they are not taken, so the record never carries
478
+ // them (nor does the `HeldWrite` copied from it, nor a sentence built on it).
479
+ if (o !== NO_VALUES && options.values === "full") {
480
+ i.prev = t === NO_VALUES ? undefined : preview(t);
481
+ i.value = preview(o);
482
+ }
483
+ i.origin = n === "async" ? asyncOrigin(e) : currentOrigin();
484
+ i.at = now();
485
+ i.stack = captureStack();
486
+ const s = e.ge;
487
+ e.ge = i;
488
+ noteNavigationWrite(s, i);
489
+ noteInteractionWrite(i.origin, excludedNode(e));
490
+ if (n === "write") trackEffectWrite(e, i, o);
491
+ // stampWrite is the single funnel for committed root invalidations (sync
492
+ // writes, refresh(), async landings), which makes it the one place the
493
+ // engine's fan-out check needs to live.
494
+ checkFanOut(e, n);
495
+ }
496
+
497
+ /** Record a derived change (memo produced a new value) with its causes. */ function stampDerived(e, n) {
498
+ e.ge = {
499
+ seq: ++changeSeq,
500
+ kind: "derived",
501
+ name: nodeName(e),
502
+ nodeId: devId(e),
503
+ causes: n
504
+ };
505
+ }
506
+
507
+ /**
508
+ * Collect the deps whose committed change is newer than this node's previous
509
+ * run. Called at recompute entry, while `_deps` still holds the previous
510
+ * run's links. A refresh() stamp on the node itself also counts — that is a
511
+ * self-invalidation, not a dep change.
512
+ */ function collectCauses(e) {
513
+ const n = e.we ?? 0;
514
+ const t = [];
515
+ const o = e.ge;
516
+ if (o !== undefined && o.seq > n && o.kind === "refresh") t.push(o);
517
+ for (let o = e.C; o !== null; o = o.G) {
518
+ const e = o.S.ge;
519
+ if (e !== undefined && e.seq > n) t.push(e);
520
+ }
521
+ return t;
522
+ }
523
+
524
+ /** Advance the node's seen-cursor to the present. Call after every run. */ function markSeen(e) {
525
+ e.we = changeSeq;
526
+ }
527
+
528
+ /**
529
+ * Snapshot the dep identities of the node's last pass (call before a run
530
+ * replaces them, or after it to read the fresh set). The validated prefix
531
+ * [`_deps`..`_depsTail`] is that pass's set; links past the tail are a
532
+ * previous pass's, kept linked while the frame they fed is still the
533
+ * committed one (A30 — a staged memo pass, or an effect pass whose run is
534
+ * still owed) and not part of the subscription diff.
535
+ */ function captureDeps(e) {
536
+ const n = [];
537
+ for (let t = e.C; t !== null; t = t.G) {
538
+ n.push(t.S);
539
+ if (t === e._) break;
540
+ }
541
+ return n;
542
+ }
543
+
544
+ /**
545
+ * Wide-scope warning — the coarse-read / helper-leak signature: one scope
546
+ * subscribed to hundreds of sources re-runs when ANY of them change. Fired from
547
+ * recordRerun for re-runs and directly from recompute for creation runs (a
548
+ * memo can be born too wide). Re-warns only on 50% further growth. Counts
549
+ * what the scope's author can act on: HMR plumbing sources are not counted,
550
+ * and a framework scope wide by construction (CONFIG_WIDE) is not judged —
551
+ * the always-on HUGE_FAN_IN still is.
552
+ */ function checkDepWidth(e) {
553
+ const n = options.wideDeps;
554
+ // Only dev sets CONFIG_WIDE (see constants.ts).
555
+ if (n === false || false) return;
556
+ let t = 0;
557
+ const o = [];
558
+ for (let n = e.C; n !== null; n = n.G) {
559
+ // HMR plumbing is a dev-only hop the scope cannot read past: not a source.
560
+ if (!(n.S.l & CONFIG_PLUMBING)) {
561
+ t++;
562
+ if (o.length < 12) o.push(nodeName(n.S));
563
+ }
564
+ if (n === e._) break;
565
+ // the validated prefix, as captureDeps
566
+ }
567
+ const i = e;
568
+ if (t < n || t < (i.ve ?? 0) * 1.5) return;
569
+ i.ve = t;
570
+ const s = `[WIDE_SCOPE_DEPS] ${nodeKind(e)} "${nodeName(e)}" is subscribed to ${t} sources — ` + `it re-runs when any of them change. Narrow its reads or split it into smaller memos. ` + `Sources: ${o.join(", ")}${t > o.length ? ", …" : ""}`;
571
+ reportDiagnostic(emitDiagnostic({
572
+ code: "WIDE_SCOPE_DEPS",
573
+ kind: "perf",
574
+ severity: "warn",
575
+ message: s,
576
+ nodeName: nodeName(e),
577
+ data: {
578
+ depCount: t,
579
+ deps: o
580
+ }
581
+ }, e));
582
+ }
583
+
584
+ const hotCauses = new Map;
585
+
586
+ const HOT_FANOUT_FIRST_MILESTONE = 5;
587
+
588
+ /**
589
+ * Hot-scope warning — flags a scope that re-ran more than `count` times
590
+ * inside one `windowMs` window. Warned once per window, with the most recent
591
+ * cause chain named so the leaking signal is identified in the message.
592
+ * Fan-out spam is folded per root cause (see HotCauseWindow above).
593
+ */ function checkHotRuns(e, n) {
594
+ const t = options.hotRuns;
595
+ if (t === false) return;
596
+ const o = e;
597
+ const i = Date.now();
598
+ if (o.ke === undefined || i - o.ke > t.windowMs) {
599
+ o.ke = i;
600
+ o.ye = 0;
601
+ o.$e = false;
602
+ }
603
+ o.ye = (o.ye ?? 0) + 1;
604
+ if (o.$e || o.ye < t.count) return;
605
+ o.$e = true;
606
+ // Root-cause key: the set of originating writes behind this scope's latest
607
+ // re-run. Scopes hot from the SAME roots share one aggregation window.
608
+ const s = new Set;
609
+ rootsOf(n, s);
610
+ const r = s.size > 0 ? [ ...s ].sort().join(", ") : "(untracked)";
611
+ let a = hotCauses.get(r);
612
+ if (a === undefined || i - a.winStart > t.windowMs) {
613
+ a = {
614
+ winStart: i,
615
+ scopes: 0,
616
+ runs: 0,
617
+ nextMilestone: HOT_FANOUT_FIRST_MILESTONE
618
+ };
619
+ hotCauses.set(r, a);
620
+ }
621
+ a.scopes++;
622
+ a.runs += o.ye;
623
+ if (a.scopes === 1) {
624
+ const s = n.map(e => `"${e.name}" (${e.kind})`).join(", ");
625
+ const r = `[HOT_SCOPE_RERUNS] ${nodeKind(e)} "${nodeName(e)}" re-ran ${o.ye} times ` + `in ${Math.max(1, i - o.ke)}ms — a hot signal is likely leaking into this ` + `scope. Latest cause: ${s || "(untracked pull)"}`;
626
+ reportDiagnostic(emitDiagnostic({
627
+ code: "HOT_SCOPE_RERUNS",
628
+ kind: "perf",
629
+ severity: "warn",
630
+ message: r,
631
+ nodeName: nodeName(e),
632
+ data: {
633
+ runs: o.ye,
634
+ windowMs: t.windowMs,
635
+ causes: n.map(e => e.name)
636
+ }
637
+ }, e));
638
+ return;
639
+ }
640
+ // Additional scopes hot from the same cause: silent until a milestone —
641
+ // the culprit is the cause, and it has already been named once.
642
+ if (a.scopes < a.nextMilestone) return;
643
+ a.nextMilestone *= 10;
644
+ const c = `[HOT_SCOPE_FANOUT] ${a.scopes} scopes have gone hot (${a.runs} re-runs) within ` + `${t.windowMs}ms, all driven by ${r} — one hot cause is re-running a large part ` + `of the graph. Per-scope warnings are suppressed; fix the cause. If consumers ask keyed ` + `questions of it, invert it: a store used as a map keyed by id, one key per consumer.`;
645
+ // The subject is the shared CAUSE, not this victim scope — no single owner
646
+ // path locates it, so the event carries none.
647
+ reportDiagnostic(emitDiagnostic({
648
+ code: "HOT_SCOPE_FANOUT",
649
+ kind: "perf",
650
+ severity: "warn",
651
+ message: c,
652
+ nodeName: r,
653
+ data: {
654
+ cause: r,
655
+ scopes: a.scopes,
656
+ runs: a.runs,
657
+ windowMs: t.windowMs
658
+ }
659
+ }, null));
660
+ }
661
+
662
+ /**
663
+ * Time-budget warning — the counterpart of checkHotRuns for the
664
+ * few-but-expensive scope: warns when one scope's summed self-time within a
665
+ * window exceeds the budget. Warned once per window.
666
+ */ function checkHotTime(e, n, t) {
667
+ const o = options.hotTime;
668
+ if (o === false) return;
669
+ const i = e;
670
+ const s = now();
671
+ if (i.be === undefined || s - i.be > o.windowMs) {
672
+ i.be = s;
673
+ i._e = 0;
674
+ i.Oe = false;
675
+ }
676
+ i._e = (i._e ?? 0) + n;
677
+ if (i.Oe || i._e < o.budgetMs) return;
678
+ i.Oe = true;
679
+ const r = t.map(e => `"${e.name}" (${e.kind})`).join(", ");
680
+ const a = `[HOT_SCOPE_TIME] ${nodeKind(e)} "${nodeName(e)}" spent ` + `${i._e.toFixed(1)}ms of compute inside one ${o.windowMs}ms window ` + `(budget ${o.budgetMs}ms). Latest cause: ${r || "(untracked pull)"}`;
681
+ reportDiagnostic(emitDiagnostic({
682
+ code: "HOT_SCOPE_TIME",
683
+ kind: "perf",
684
+ severity: "warn",
685
+ message: a,
686
+ nodeName: nodeName(e),
687
+ data: {
688
+ spentMs: i._e,
689
+ budgetMs: o.budgetMs,
690
+ windowMs: o.windowMs,
691
+ causes: t.map(e => e.name)
692
+ }
693
+ }, e));
694
+ }
695
+
696
+ /**
697
+ * A scope whose equality gate closes almost every time: it re-ran because
698
+ * an input changed, computed, compared equal to its last value and told
699
+ * nobody — the run was pure cost. `costs().wastedMs` sums this; the finding
700
+ * names it while it happens, with the input that keeps triggering it. The
701
+ * fix is upstream: an equality boundary on the part of the input the scope
702
+ * depends on, or a narrower read. Plain runs only — a held or overlay run
703
+ * may be replayed and is never blamed as waste. A run that committed
704
+ * `undefined` arrives here as changed (see `recomputeEnd`): its work is a
705
+ * side effect (a projection's draft writes, a write from inside a memo), not
706
+ * its output.
707
+ */ function checkWastedRecompute(e, n, t, o, i, s) {
708
+ const r = options.wastedRecompute;
709
+ if (r === false || t !== "plain") return;
710
+ // This runs on every re-run: fields on the node (one property read each,
711
+ // like hotRuns) and the run's own `at` — no clock read, no map lookup.
712
+ const a = e;
713
+ if (a.Fe === undefined || n - a.Fe > r.windowMs) {
714
+ a.Fe = n;
715
+ a.Ae = 0;
716
+ a.Re = 0;
717
+ a.Le = 0;
718
+ a.Ce = false;
719
+ }
720
+ a.Ae = a.Ae + 1;
721
+ if (!o) {
722
+ a.Re = a.Re + 1;
723
+ a.Le = a.Le + i;
724
+ }
725
+ if (a.Ce || a.Ae < r.minRuns || a.Re / a.Ae < r.ratio || a.Le < r.budgetMs) return;
726
+ a.Ce = true;
727
+ const c = {
728
+ runs: a.Ae,
729
+ wasted: a.Re,
730
+ wastedMs: a.Le
731
+ };
732
+ const d = s.map(e => `"${e.name}" (${e.kind})`).join(", ");
733
+ const f = `[WASTED_RECOMPUTE] ${nodeKind(e)} "${nodeName(e)}" re-ran ${c.runs} times in ` + `${r.windowMs}ms and ${c.wasted} of those produced the same value — ` + `${c.wastedMs.toFixed(1)}ms of compute the equality gate then discarded. Its inputs ` + `change without changing its result: put an equality boundary upstream (a memo over the ` + `part of the input it reads, or an \`equals\` on the source), or read a narrower slice ` + `(the property, not the object). Latest cause: ${d || "(untracked pull)"}`;
734
+ reportDiagnostic(emitDiagnostic({
735
+ code: "WASTED_RECOMPUTE",
736
+ kind: "perf",
737
+ severity: "warn",
738
+ message: f,
739
+ nodeName: nodeName(e),
740
+ data: {
741
+ runs: c.runs,
742
+ wasted: c.wasted,
743
+ wastedMs: c.wastedMs,
744
+ windowMs: r.windowMs,
745
+ causes: s.map(e => e.name)
746
+ }
747
+ }, e));
748
+ }
749
+
750
+ function recordRerun(e, n, t, o, i, s) {
751
+ const r = n.causes;
752
+ const a = e;
753
+ const c = a.pe;
754
+ const d = n.interaction;
755
+ if (excludedNode(e)) {
756
+ // The observer's own computation: keep the per-node bookkeeping its
757
+ // effect phase reads (see effectRunStart) and record nothing.
758
+ a.Me = d;
759
+ a.me = undefined;
760
+ a.pe = r;
761
+ return;
762
+ }
763
+ // The facts every run leaves on the node, record or not: the run sequence
764
+ // (`ChangeOrigin.run` joins an effect-phase write to it), the run count,
765
+ // and what the effect phase inherits — it runs later in the flush with no
766
+ // cause list of its own, so it takes this run's interaction and causes
767
+ // (see effectRunStart / pushFrame).
768
+ const f = ++runSeq;
769
+ const u = a.De = (a.De ?? 0) + 1;
770
+ a.Me = d;
771
+ a.me = f;
772
+ a.pe = r;
773
+ noteInteractionRun(d, t.selfMs, false);
774
+ if (openFlush !== null) noteFlushRun(openFlush, false, d);
775
+ // The checks read the facts, not the record, so they run with or without
776
+ // an audience for it.
777
+ const l = nodeKind(e);
778
+ if (l === "effect") checkEffectCycle(e, r);
779
+ checkRelayTear(e, r, c);
780
+ checkHotRuns(e, r);
781
+ checkHotTime(e, t.selfMs, r);
782
+ checkWastedRecompute(e, n.start, i, o, t.selfMs, r);
783
+ checkDepWidth(e);
784
+ // The record: built only when something wanted it at run start (see
785
+ // `wantsRerun`) — a listener, a fold, the log.
786
+ const h = n.prevDeps;
787
+ if (h === null) return;
788
+ // Subscription diff: `prevDeps` was captured at run entry; `_deps` now
789
+ // holds the fresh set. A changed set is the "helper edit changed distant
790
+ // call sites" signal — surfaced per-event and in the console format.
791
+ const p = captureDeps(e);
792
+ const m = new Set(h);
793
+ const g = new Set(p);
794
+ const w = [];
795
+ const v = [];
796
+ for (const e of p) if (!m.has(e)) w.push(nodeName(e));
797
+ for (const e of h) if (!g.has(e)) v.push(nodeName(e));
798
+ const k = {
799
+ run: f,
800
+ at: n.start,
801
+ nodeRuns: u,
802
+ nodeKind: l,
803
+ nodeName: nodeName(e),
804
+ nodeId: devId(e),
805
+ causes: r,
806
+ depCount: p.length,
807
+ depsAdded: w,
808
+ depsRemoved: v,
809
+ selfMs: t.selfMs,
810
+ totalMs: t.totalMs,
811
+ changed: o,
812
+ phase: i,
813
+ held: s
814
+ };
815
+ if (d !== undefined) k.interaction = d;
816
+ history.push(k);
817
+ if (history.length > options.historyLimit) history.shift();
818
+ for (const n of folds) n.rerun?.(e, k);
819
+ records.emit("rerun", k, e);
820
+ if (options.log) logRerun(k);
821
+ }
822
+
823
+ function formatCause(e, n, t) {
824
+ const o = " ".repeat(n + 1);
825
+ let i = `${o}← ${e.kind === "derived" ? "memo" : "signal"} "${e.name}" ${e.kind === "derived" ? "changed" : e.kind} (#${e.seq})`;
826
+ if (e.prev !== undefined) i += ` ${e.prev} → ${e.value}`;
827
+ if (e.origin !== undefined && e.origin.kind !== "external") {
828
+ i += ` — ${formatOrigin(e.origin)}`;
829
+ const n = e.origin.interaction;
830
+ if (n !== undefined) i += ` (under ${formatOrigin(n)})`;
831
+ }
832
+ t.push(i);
833
+ if (e.stack) for (const n of e.stack) t.push(`${o} ${n}`);
834
+ if (e.causes && n < 10) {
835
+ for (const o of e.causes) formatCause(o, n + 1, t);
836
+ }
837
+ }
838
+
839
+ function formatRerun(e) {
840
+ const n = [ `[why-run] ${e.nodeKind} "${e.nodeName}" ran (run ${e.nodeRuns}, ` + `${e.selfMs.toFixed(2)}ms${e.changed ? "" : ", unchanged"}` + `${e.phase === "plain" ? "" : `, ${e.phase}`}${e.held ? ", held" : ""})` + (e.causes.length === 0 ? " — no tracked cause (pull or retry)" : "") ];
841
+ for (const t of e.causes) formatCause(t, 0, n);
842
+ if (e.depsAdded.length > 0 || e.depsRemoved.length > 0) {
843
+ const t = [ ...e.depsAdded.map(e => `+"${e}"`), ...e.depsRemoved.map(e => `-"${e}"`) ].join(" ");
844
+ n.push(` deps changed: ${t} (${e.depCount} total)`);
845
+ }
846
+ return n.join("\n");
847
+ }
848
+
849
+ /**
850
+ * Console face of a re-run: the headline as a collapsed group with the
851
+ * why-chain and dep delta inside, so a busy console stays scannable (one line
852
+ * per run, evidence a click away). Consoles without grouping get the text.
853
+ */ function logRerun(e) {
854
+ const n = formatRerun(e);
855
+ const t = n.indexOf("\n");
856
+ if (t === -1 || typeof console.groupCollapsed !== "function") {
857
+ console.log(n);
858
+ return;
859
+ }
860
+ console.groupCollapsed(n.slice(0, t));
861
+ console.log(n.slice(t + 1));
862
+ console.groupEnd();
863
+ }
864
+
865
+ /**
866
+ * Values eligible for the unstable-output check: plain objects and arrays
867
+ * only. Promises, iterators, Dates, Maps, class instances etc. all have no
868
+ * (or unrepresentative) own enumerable keys, so a shallow compare would
869
+ * false-positive on them — a fresh Promise is a genuinely new value.
870
+ */ function isPlainShape(e) {
871
+ if (e === null || typeof e !== "object") return false;
872
+ if (Array.isArray(e)) return true;
873
+ const n = Object.getPrototypeOf(e);
874
+ return n === Object.prototype || n === null;
875
+ }
876
+
877
+ /** Shallow structural equivalence, capped so hot paths stay cheap. */ const UNSTABLE_KEY_CAP = 64;
878
+
879
+ function shallowEquivalent(e, n) {
880
+ const t = Array.isArray(e);
881
+ if (t !== Array.isArray(n)) return false;
882
+ if (t) {
883
+ const t = e;
884
+ const o = n;
885
+ if (t.length !== o.length || t.length > UNSTABLE_KEY_CAP) return false;
886
+ for (let e = 0; e < t.length; e++) if (t[e] !== o[e]) return false;
887
+ return true;
888
+ }
889
+ // Every own key, symbols included: a symbol-keyed box holding a fresh value
890
+ // each run (web's `dynamic` boxes its in-flight promise) is not equivalent.
891
+ const o = Reflect.ownKeys(e);
892
+ if (o.length > UNSTABLE_KEY_CAP || o.length !== Reflect.ownKeys(n).length) return false;
893
+ for (const t of o) {
894
+ if (!(t in n) || e[t] !== n[t]) return false;
895
+ }
896
+ return true;
897
+ }
898
+
899
+ /**
900
+ * Unstable-output warning — the fan-out amplifier signature: a memo whose
901
+ * committed value is referentially new but structurally identical run after
902
+ * run has an equality gate that never closes, so ALL its subscribers re-run
903
+ * on EVERY upstream change. Checked only on plain (non-overlay) changed runs;
904
+ * a genuinely different value (or a non-plain shape) resets the streak.
905
+ */ function checkUnstableOutput(e, n, t) {
906
+ const o = options.unstableMemos;
907
+ // typeof guard: an explicit `unstableMemos: undefined` in enable() options
908
+ // clobbers the default through the spread — treat any non-number as off.
909
+ if (typeof o !== "number") return;
910
+ // merge()'s function-source memo: a key bag no user configures (#4256).
911
+ if (e.l & CONFIG_KEY_BAG) return;
912
+ const i = e;
913
+ if (n === t || // paranoia: changed runs should never hit this
914
+ !isPlainShape(n) || !isPlainShape(t) || !shallowEquivalent(n, t)) {
915
+ i.He = 0;
916
+ i.We = false;
917
+ return;
918
+ }
919
+ i.He = (i.He ?? 0) + 1;
920
+ if (i.We || i.He < o) return;
921
+ i.We = true;
922
+ const s = Array.isArray(t) ? "array" : "object";
923
+ const r = `[UNSTABLE_MEMO_OUTPUT] memo "${nodeName(e)}" produced a new-but-equivalent ${s} on ` + `${i.He} consecutive runs — its equality gate never closes, so every ` + `subscriber re-runs on every upstream change. Return stable references or pass an ` + `\`equals\` option.`;
924
+ reportDiagnostic(emitDiagnostic({
925
+ code: "UNSTABLE_MEMO_OUTPUT",
926
+ kind: "perf",
927
+ severity: "warn",
928
+ message: r,
929
+ nodeName: nodeName(e),
930
+ data: {
931
+ runs: i.He,
932
+ shape: s
933
+ }
934
+ }, e));
935
+ }
936
+
937
+ const EFFECT_CYCLE_MAX_HOPS = 6;
938
+
939
+ const reportedCycles = new Set;
940
+
941
+ let nextDevId = 0;
942
+
943
+ const devIds = new WeakMap;
944
+
945
+ function devId(e) {
946
+ let n = devIds.get(e);
947
+ if (n === undefined) devIds.set(e, n = ++nextDevId);
948
+ return n;
949
+ }
950
+
951
+ /** @internal The id the engine's records name `node` by, if it has one yet — read without assigning. */ function nodeIdOf(e) {
952
+ return devIds.get(e);
953
+ }
954
+
955
+ function rootWrites(e, n) {
956
+ for (const t of e) {
957
+ if (t.kind === "derived") {
958
+ if (t.causes !== undefined) rootWrites(t.causes, n);
959
+ } else n.push(t);
960
+ }
961
+ }
962
+
963
+ /**
964
+ * The effect writes leading from an earlier run of `target` to the run whose
965
+ * `causes` these are, in causal order (target's own write first), or null.
966
+ */ function findEffectCycle(e, n, t, o) {
967
+ const i = [];
968
+ rootWrites(n, i);
969
+ for (const n of i) {
970
+ const i = n.origin;
971
+ if (i === undefined || i.kind !== "effect") continue;
972
+ const s = effectFrames.get(i);
973
+ if (s === undefined) continue;
974
+ if (s.node === e) return [ {
975
+ effect: s.node,
976
+ write: n
977
+ } ];
978
+ if (o >= EFFECT_CYCLE_MAX_HOPS || t.has(s.node) || s.causes === undefined) continue;
979
+ t.add(s.node);
980
+ const r = findEffectCycle(e, s.causes, t, o + 1);
981
+ if (r !== null) {
982
+ r.push({
983
+ effect: s.node,
984
+ write: n
985
+ });
986
+ return r;
987
+ }
988
+ }
989
+ return null;
990
+ }
991
+
992
+ /** Names of the memos between a direct cause of a run and `write`, root-first. */ function derivedPath(e, n, t) {
993
+ for (const o of e) {
994
+ if (o === n) return true;
995
+ if (o.kind === "derived" && o.causes !== undefined) {
996
+ t.unshift(o.name);
997
+ if (derivedPath(o.causes, n, t)) return true;
998
+ t.shift();
999
+ }
1000
+ }
1001
+ return false;
1002
+ }
1003
+
1004
+ function describeWrite(e) {
1005
+ if (e.kind === "refresh") return `refreshed "${e.name}"`;
1006
+ const n = e.prev !== undefined ? ` (${e.prev} → ${e.value})` : "";
1007
+ return `wrote "${e.name}"${n}`;
1008
+ }
1009
+
1010
+ function checkEffectCycle(e, n) {
1011
+ const t = findEffectCycle(e, n, new Set([ e ]), 0);
1012
+ if (t === null) return;
1013
+ const o = t.map(e => devId(e.effect)).sort((e, n) => e - n).join(",");
1014
+ if (reportedCycles.has(o)) return;
1015
+ reportedCycles.add(o);
1016
+ const i = t.length + 1;
1017
+ let s;
1018
+ if (t.length === 1) {
1019
+ const [{write: o}] = t;
1020
+ const i = [];
1021
+ derivedPath(n, o, i);
1022
+ const r = i.length > 0 ? ` through ${i.map(e => `memo "${e}"`).join(" → ")}` : "";
1023
+ s = `[EFFECT_WRITES_OWN_SOURCE] effect "${nodeName(e)}" re-ran because of its own write: it ` + `${describeWrite(o)}, which fed back into its inputs${r}. Two flushes to settle, ` + `and the screen rendered the pre-write value in between. The written value is a function ` + `of what the effect reads — compute it in a memo (or normalize where the source is ` + `written) instead of correcting it after the fact.`;
1024
+ } else {
1025
+ const e = t.map(e => `"${nodeName(e.effect)}"`);
1026
+ const n = t.map((n, t) => `effect ${e[t]}${t > 0 ? " re-ran and" : ""} ${describeWrite(n.write)}`).join("; ");
1027
+ s = `[EFFECT_WRITES_OWN_SOURCE] effects ${[ ...e, e[0] ].join(" → ")} relay writes in a ` + `cycle: ${n}; which fed back into effect ${e[0]}'s inputs — ${i} flushes to ` + `settle after each change, each rendering an intermediate state. Every relayed value is a ` + `function of the original inputs: derive them in memos and drop the writes.`;
1028
+ }
1029
+ const r = t.length === 1 ? "warn" : "info";
1030
+ emitDiagnostic({
1031
+ code: "EFFECT_WRITES_OWN_SOURCE",
1032
+ kind: "perf",
1033
+ severity: r,
1034
+ message: s,
1035
+ nodeName: nodeName(e),
1036
+ data: {
1037
+ effects: t.map(e => nodeName(e.effect)),
1038
+ writes: t.map(e => ({
1039
+ effect: nodeName(e.effect),
1040
+ kind: e.write.kind,
1041
+ name: e.write.name,
1042
+ prev: e.write.prev,
1043
+ value: e.write.value
1044
+ })),
1045
+ flushes: i
1046
+ }
1047
+ }, e);
1048
+ }
1049
+
1050
+ const RELAY_WARN_AT = 3;
1051
+
1052
+ const relays = new Map;
1053
+
1054
+ const copyWrites = new WeakSet;
1055
+
1056
+ const copyReported = new WeakSet;
1057
+
1058
+ /** The node each root write record was stamped on (records are serializable and cannot hold it). */ const recordNodes = new WeakMap;
1059
+
1060
+ /**
1061
+ * Write-side bookkeeping for the relay heuristics: who has written this
1062
+ * signal, and whether an effect just copied its compute output into it.
1063
+ */ function trackEffectWrite(e, n, t) {
1064
+ const o = e;
1065
+ const i = n.origin;
1066
+ if (i !== undefined) noteEffectOrigin(i);
1067
+ const s = i !== undefined && i.kind === "effect" ? effectFrames.get(i) : undefined;
1068
+ const r = s === undefined ? 0 : devId(s.node);
1069
+ recordNodes.set(n, e);
1070
+ o.Pe = o.Pe === undefined || o.Pe === r ? r : null;
1071
+ // An apply half that takes no argument cannot be copying its compute
1072
+ // output: a match is coincidence (`createEffect(open, () =>
1073
+ // setFlipped(measure()))`).
1074
+ if (s !== undefined && t !== undefined && t === s.node.le && (s.node.Ge?.length ?? 1)) {
1075
+ copyWrites.add(n);
1076
+ o.Ue = o.qe === r ? (o.Ue ?? 0) + 1 : 1;
1077
+ o.qe = r;
1078
+ if (o.Ue >= 2 && o.Pe === r) checkCopyEffect(s.node, e);
1079
+ } else o.Ue = 0;
1080
+ }
1081
+
1082
+ /** The effect's source whose current value the compute output is, if any (the prop-to-state port). */ function passthroughSource(e) {
1083
+ for (let n = e.C; n !== null; n = n.G) if (n.S.le === e.le) return nodeName(n.S);
1084
+ return undefined;
1085
+ }
1086
+
1087
+ /** The repair for a write that is the effect's compute output. */ function copyRepair(e, n) {
1088
+ const t = passthroughSource(e);
1089
+ return t !== undefined ? `The written value is "${t}" itself: read "${t}" where "${n}" is read ` + `(or createMemo it if a stable derivation is needed) and delete the effect.` : `The written value is the effect's compute output — by contract a pure function of ` + `what it tracks: make "${n}" a memo of that computation and delete the effect.`;
1090
+ }
1091
+
1092
+ function checkCopyEffect(e, n) {
1093
+ if (copyReported.has(n)) return;
1094
+ copyReported.add(n);
1095
+ const t = nodeName(n);
1096
+ const o = `[EFFECT_RELAY_TEAR] effect "${nodeName(e)}" writes its compute output into ` + `"${t}" on every run, and nothing else writes "${t}" — it is derived state kept ` + `one flush late: everything reading it paints a frame behind everything reading the ` + `source. ${copyRepair(e, t)}`;
1097
+ reportDiagnostic(emitDiagnostic({
1098
+ code: "EFFECT_RELAY_TEAR",
1099
+ kind: "perf",
1100
+ severity: "warn",
1101
+ message: o,
1102
+ nodeName: nodeName(e),
1103
+ data: {
1104
+ relay: nodeName(e),
1105
+ wrote: t,
1106
+ copy: true,
1107
+ passthrough: passthroughSource(e) ?? null,
1108
+ soleWriter: true
1109
+ }
1110
+ }, e));
1111
+ }
1112
+
1113
+ /**
1114
+ * `victim` re-ran with `causes`; its previous run had `prevCauses`. A tear is
1115
+ * a re-run whose root writes ALL came from effects (no independent outside
1116
+ * cause) and at least one of which was made by a run that shares a root
1117
+ * write with the victim's previous run.
1118
+ */ function checkRelayTear(e, n, t) {
1119
+ if (t === undefined || n.length === 0) return;
1120
+ const o = [];
1121
+ rootWrites(n, o);
1122
+ if (o.length === 0) return;
1123
+ let i;
1124
+ let s;
1125
+ let r;
1126
+ for (const n of o) {
1127
+ const o = n.origin;
1128
+ if (o === undefined || o.kind !== "effect") return;
1129
+ const a = effectFrames.get(o);
1130
+ // Own-source cycles are EFFECT_WRITES_OWN_SOURCE's; a create-run relay
1131
+ // is initial sync, not a tear for one change.
1132
+ if (a === undefined || a.node === e || a.causes === undefined) return;
1133
+ if (r === undefined) {
1134
+ const e = [];
1135
+ rootWrites(a.causes, e);
1136
+ const o = [];
1137
+ rootWrites(t, o);
1138
+ const c = e.find(e => o.includes(e));
1139
+ if (c !== undefined) {
1140
+ r = c;
1141
+ i = a;
1142
+ s = n;
1143
+ }
1144
+ }
1145
+ }
1146
+ if (r === undefined || i === undefined || s === undefined) return;
1147
+ const a = `${devId(i.node)}:${s.name}`;
1148
+ let c = relays.get(a);
1149
+ if (c === undefined) relays.set(a, c = {
1150
+ count: 0,
1151
+ warned: false
1152
+ });
1153
+ c.count++;
1154
+ const d = copyWrites.has(s);
1155
+ const f = recordNodes.get(s);
1156
+ const u = f !== undefined && f.Pe === devId(i.node);
1157
+ // Derivable outright: the value is the compute output and nothing else
1158
+ // writes the signal. A copy INTO a signal that has other writers is the
1159
+ // "reset editable state from a source" shape — the tear is real, but a memo
1160
+ // is not the answer, so it stays advisory like any other non-derivable tear.
1161
+ const l = d && u;
1162
+ // An apply half that ignores its argument writes from outside the graph
1163
+ // (layout, time, a ref; a signal read there is STRICT_READ's): measuring,
1164
+ // so it stays advisory however often it repeats.
1165
+ const h = l || c.count >= RELAY_WARN_AT && (i.node.Ge?.length ?? 1) ? "warn" : "info";
1166
+ // First sighting always reports (advisory); afterwards only the escalation.
1167
+ if (c.count > 1 && (h !== "warn" || c.warned)) return;
1168
+ // One verdict per derivable signal: the copy report (checkCopyEffect) and
1169
+ // the tear report carry the same repair.
1170
+ if (l && f !== undefined) {
1171
+ if (copyReported.has(f)) return;
1172
+ copyReported.add(f);
1173
+ }
1174
+ if (h === "warn") c.warned = true;
1175
+ const p = e.i ? "effect" : "memo";
1176
+ const m = nodeName(i.node);
1177
+ const g = l ? copyRepair(i.node, s.name) : d ? `The written value is the effect's compute output, but "${s.name}" has other ` + `writers — editable state reset from a source. If the reset is the intent, the tear ` + `is its cost; if "${s.name}" only ever mirrors the source, drop the local copy ` + `and read the source.` : (u ? `Nothing else writes "${s.name}". ` : "") + `If "${s.name}" is computed from what the effect reads, make it a memo so readers ` + `get it in the same flush; if the write reads something outside the graph (layout, ` + `time), the tear is the cost of measuring.`;
1178
+ const w = `[EFFECT_RELAY_TEAR] ${p} "${nodeName(e)}" ran twice for one write of ` + `"${r.name}": once in the flush where "${r.name}" changed, and again after ` + `effect "${m}" relayed it by writing "${s.name}" — the first frame showed the ` + `new "${r.name}" with the stale "${s.name}"` + (c.count > 1 ? ` (${c.count} times so far)` : "") + `. ${g}`;
1179
+ emitDiagnostic({
1180
+ code: "EFFECT_RELAY_TEAR",
1181
+ kind: "perf",
1182
+ severity: h,
1183
+ message: w,
1184
+ nodeName: nodeName(e),
1185
+ data: {
1186
+ victim: nodeName(e),
1187
+ root: r.name,
1188
+ relay: m,
1189
+ wrote: s.name,
1190
+ copy: d,
1191
+ passthrough: d ? passthroughSource(i.node) ?? null : null,
1192
+ soleWriter: u,
1193
+ occurrences: c.count
1194
+ }
1195
+ }, e);
1196
+ }
1197
+
1198
+ // --- Immutable updates in stores ---------------------------------------------
1199
+
1200
+ // `draft.user = { ...draft.user, name }` / `draft.items = [...draft.items,
1201
+ // x]` / `draft.items = draft.items.filter(...)` — the React habit of
1202
+ // producing a fresh container to change one leaf. The store tracks leaves, so
1203
+ // a fresh container is pure cost: every reader of `user` (any path below it)
1204
+ // re-runs for the one leaf that moved, where a draft mutation would re-run
1205
+ // only the readers of `name`. The store's notify sees both containers at the
1206
+ // write and reports a leaf census (identity on unwrapped values, capped); the
1207
+ // verdict is the whole detector: a replacement whose leaves are mostly the
1208
+ // SAME values is a spread-copy, and one whose leaves are mostly different is
1209
+ // new data (reconcile's job — and UNSTABLE_LIST_IDENTITY's, downstream). Once
1210
+ // per store path.
1211
+ const immutableReported = new Set;
1212
+
1213
+ function checkImmutableUpdate(e, n, t, o, i, s) {
1214
+ if (immutableReported.has(e) || t < 2) return;
1215
+ // Push/filter/splice copies change the length by a little; a wholesale
1216
+ // resize is a different operation even if some items survive.
1217
+ if (n && Math.abs(i - t) > Math.max(1, t >> 2)) return;
1218
+ // At least half the leaves carried over unchanged, and at least one did.
1219
+ if (o === 0 || o * 2 < t) return;
1220
+ const r = t - o;
1221
+ const a = n ? "array" : "object";
1222
+ const c = n ? `mutate the draft in place (push/splice/index assignment) so only the touched ` + `indices notify` : `assign the leaf on the draft (\`${e}.<key> = …\`) so only readers of that key re-run`;
1223
+ const d = `[IMMUTABLE_UPDATE_IN_STORE] "${e}" was replaced with a fresh ${a} whose ` + `${n ? "items" : "leaves"} are mostly the same values (${o} of ${t} unchanged` + `${r > 0 ? `, ${r} changed` : ""}) — a spread-copy update. The store already ` + `tracks ${n ? "items" : "leaves"}; a new container makes every reader of "${e}" ` + `re-run for the ${r === 1 ? "one that" : "few that"} moved. Instead, ${c}. For ` + `data arriving from outside (a fetch result), merge it with reconcile(data, key)(${e}).`;
1224
+ // The subject is the store's own owner, not the writer's context: the
1225
+ // finding is about the store, and its writes legitimately arrive from
1226
+ // outside the graph (an event handler, an adapter). Falls back to the
1227
+ // ambient context (emitDiagnostic's default) when the store recorded none.
1228
+ const f = emitDiagnostic({
1229
+ code: "IMMUTABLE_UPDATE_IN_STORE",
1230
+ kind: "perf",
1231
+ severity: "warn",
1232
+ message: d,
1233
+ nodeName: e,
1234
+ data: {
1235
+ path: e,
1236
+ shape: a,
1237
+ total: t,
1238
+ unchanged: o,
1239
+ changed: r
1240
+ }
1241
+ }, s);
1242
+ // Paths are not unique across stores: an excluded owner's store (an
1243
+ // adapter's own "store.list") must not spend the app's once-per-path slot.
1244
+ if (isSuppressed(f)) return;
1245
+ immutableReported.add(e);
1246
+ }
1247
+
1248
+ // --- Unstable list identity -----------------------------------------------------
1249
+
1250
+ // `<For>` keyed by identity (the default) treats every new object as a new
1251
+ // row. When a re-fetch hands back fresh objects for the same records, or a
1252
+ // spread-copy rebuilds the array, most rows are disposed and recreated —
1253
+ // DOM, state, focus, and all — for data that did not change. mapArray knows
1254
+ // exactly which items exited and entered; pairing them (by `id` when the
1255
+ // items carry one, else by position) and sampling shallow equivalence turns
1256
+ // that into a verdict: churn that replaced equivalent records is unstable
1257
+ // identity, not a new list. A key function that still churns has the same
1258
+ // disease one level up (its keys are not stable). Once per list.
1259
+ const LIST_CHURN_SAMPLE = 8;
1260
+
1261
+ const listIdentityWarned = new WeakSet;
1262
+
1263
+ function recordId(e) {
1264
+ if (e === null || typeof e !== "object") return undefined;
1265
+ const n = e;
1266
+ return n.id ?? n.key ?? n.Be ?? undefined;
1267
+ }
1268
+
1269
+ function checkListIdentity(e, n, t, o, i) {
1270
+ if (listIdentityWarned.has(e)) return;
1271
+ // Most of the list turned over, and the turnover was a swap (rows out ≈ rows in).
1272
+ if (t.length < 2 || t.length * 2 < o) return;
1273
+ if (Math.abs(n.length - t.length) > Math.max(1, t.length >> 2)) return;
1274
+ // Pair exited with entered: by record id when present, else by position.
1275
+ const s = new Map;
1276
+ for (const e of n) {
1277
+ const n = recordId(e);
1278
+ if (n !== undefined) s.set(n, e);
1279
+ }
1280
+ let r = 0;
1281
+ let a = 0;
1282
+ const c = Math.max(1, Math.floor(t.length / LIST_CHURN_SAMPLE));
1283
+ for (let e = 0; e < t.length && r < LIST_CHURN_SAMPLE; e += c) {
1284
+ const o = t[e];
1285
+ const i = recordId(o);
1286
+ const c = i !== undefined ? s.get(i) : n[e];
1287
+ if (c === undefined || !isPlainShape(c) || !isPlainShape(o)) continue;
1288
+ r++;
1289
+ if (shallowEquivalent(c, o)) a++;
1290
+ }
1291
+ if (r === 0 || a * 2 < r) return;
1292
+ listIdentityWarned.add(e);
1293
+ const d = nodeName(e);
1294
+ const f = i ? `The key function returned different keys for equivalent records — return a stable ` + `field (\`keyed: item => item.id\`), not the object or a computed value that changes ` + `with the fetch.` : `Key the list by a stable field (\`keyed: item => item.id\`), or merge the data into ` + `a store with reconcile(data, "id") so the same records keep the same identity.`;
1295
+ const u = `[UNSTABLE_LIST_IDENTITY] list "${d}" recreated ${t.length} of ${o} rows on ` + `an update where the entering items are equivalent to the ones they replaced ` + `(${a} of ${r} sampled pairs identical field-for-field) — fresh objects ` + `for the same records, so identity keying threw away every row's DOM and state and ` + `rebuilt it. ${f}`;
1296
+ reportDiagnostic(emitDiagnostic({
1297
+ code: "UNSTABLE_LIST_IDENTITY",
1298
+ kind: "perf",
1299
+ severity: "warn",
1300
+ message: u,
1301
+ nodeName: d,
1302
+ data: {
1303
+ removed: n.length,
1304
+ created: t.length,
1305
+ length: o,
1306
+ sampled: r,
1307
+ equivalent: a,
1308
+ keyed: i
1309
+ }
1310
+ }, e));
1311
+ }
1312
+
1313
+ /** Provenance of an async landing: the flight, under the interaction that started it. */ function asyncOrigin(e) {
1314
+ const n = {
1315
+ kind: "async",
1316
+ name: nodeName(e)
1317
+ };
1318
+ const t = liveFlights.get(e)?.interaction;
1319
+ if (t !== undefined) n.interaction = t;
1320
+ return n;
1321
+ }
1322
+
1323
+ // WeakMaps: an errored/abandoned flight must not leak its node or block GC.
1324
+ const liveFlights = new WeakMap;
1325
+
1326
+ const landedFlights = new WeakMap;
1327
+
1328
+ // Flight-object identity → earliest known start lives on the core's slot
1329
+ // (`OBSERVE.attribution.markFlight`/`flightOrigin`), so marks made with no
1330
+ // engine loaded — a router's preloads, `lazy()`'s import — and the server's
1331
+ // boundary check share it. Fed by markFlight() and by first sightings at
1332
+ // registration.
1333
+ let waterfallLog = [];
1334
+
1335
+ /** Deepest landed-flight cause reachable through a cause list (derived links included). */ function flightCauseIn(e) {
1336
+ let n = null;
1337
+ for (const t of e) {
1338
+ let e = null;
1339
+ if (t.kind === "async") e = landedFlights.get(t) ?? null; else if (t.kind === "derived" && t.causes) e = flightCauseIn(t.causes);
1340
+ if (e !== null && (n === null || e.chain.length > n.chain.length)) n = e;
1341
+ }
1342
+ return n;
1343
+ }
1344
+
1345
+ /**
1346
+ * The nearest enclosing frame with causes: create runs carry null (a node
1347
+ * born inside a parent's recompute inherits the parent's causality — the
1348
+ * boundary-reveal case, and the lazy sibling whose first pull is gated behind
1349
+ * an earlier not-ready read), so walk down to the first re-run frame.
1350
+ */ function enclosingCauses() {
1351
+ for (let e = frames.length - 1; e >= 0; e--) {
1352
+ const n = frames[e].causes;
1353
+ if (n !== null) return n;
1354
+ }
1355
+ return null;
1356
+ }
1357
+
1358
+ /** The flight's face as a record, at the moment it landed or was superseded. */ function emitFlight(e, n, t, o) {
1359
+ if (excludedNode(e)) return;
1360
+ const i = {
1361
+ nodeId: devId(e),
1362
+ nodeName: nodeName(e),
1363
+ at: n.origin,
1364
+ durationMs: t - n.origin,
1365
+ outcome: o
1366
+ };
1367
+ const s = ownerPath(e);
1368
+ if (s !== undefined) i.ownerPath = s;
1369
+ if (n.interaction !== undefined) i.interaction = n.interaction;
1370
+ records.emit("flight", i, e);
1371
+ }
1372
+
1373
+ function trackFlightStart(e, n) {
1374
+ const t = now();
1375
+ const o = OBSERVE.attribution.flightOrigin(n) ?? t;
1376
+ if (o === t) OBSERVE.attribution.markFlight(n, t);
1377
+ // Census: a flight still in the air when the node starts another was
1378
+ // superseded — its answer will be discarded.
1379
+ const i = liveFlights.get(e);
1380
+ for (const n of folds) n.flightStart?.(e, i !== undefined);
1381
+ if (i !== undefined) {
1382
+ if (records.observed("flight")) emitFlight(e, i, t, "abandoned");
1383
+ checkAbandonedFlights(e, i);
1384
+ }
1385
+ const s = enclosingCauses();
1386
+ const r = {
1387
+ origin: o,
1388
+ startSeq: changeSeq,
1389
+ chain: []
1390
+ };
1391
+ // Provenance: the flight belongs to whatever interaction caused the
1392
+ // recompute that started it (a create run under a click's handler — a
1393
+ // freshly mounted async node — inherits the ambient interaction instead).
1394
+ const a = s !== null ? interactionIn(s) : currentInteraction ?? undefined;
1395
+ if (a !== undefined) r.interaction = a;
1396
+ if (options.waterfalls !== false && s !== null) {
1397
+ let e = flightCauseIn(s);
1398
+ // The sequentiality test. A marked/previously-seen flight whose origin
1399
+ // predates the upstream landing was in the air alongside it: parallel.
1400
+ if (e !== null && o < e.landedAt) e = null;
1401
+ if (e !== null) r.chain = [ ...e.chain, {
1402
+ name: e.name,
1403
+ ms: e.ms
1404
+ } ];
1405
+ }
1406
+ liveFlights.set(e, r);
1407
+ }
1408
+
1409
+ /**
1410
+ * Flight landed (whether or not the value committed — the wall time was
1411
+ * spent either way). Attach the measurement to the landing's fresh "async"
1412
+ * stamp so downstream flights can chain through it, then judge the chain.
1413
+ */ function finalizeFlight(e) {
1414
+ const n = liveFlights.get(e);
1415
+ if (n === undefined) return;
1416
+ liveFlights.delete(e);
1417
+ const t = now();
1418
+ const o = t - n.origin;
1419
+ for (const n of folds) n.flightLanded?.(e, o);
1420
+ if (records.observed("flight")) emitFlight(e, n, t, "landed");
1421
+ const i = e.ge;
1422
+ // Only a stamp this landing produced may carry the measurement — a stale
1423
+ // async record from a previous landing must not be re-labeled.
1424
+ if (i !== undefined && i.kind === "async" && i.seq > n.startSeq) landedFlights.set(i, {
1425
+ name: nodeName(e),
1426
+ ms: o,
1427
+ chain: n.chain,
1428
+ landedAt: t
1429
+ });
1430
+ checkWaterfall(e, n.chain, o);
1431
+ }
1432
+
1433
+ function checkWaterfall(e, n, t) {
1434
+ const o = options.waterfalls;
1435
+ if (o === false) return;
1436
+ if (n.length > 0) {
1437
+ waterfallLog.push({
1438
+ chain: [ ...n, {
1439
+ name: nodeName(e),
1440
+ ms: t
1441
+ } ],
1442
+ sequentialMs: n.reduce((e, n) => e + n.ms, t)
1443
+ });
1444
+ if (waterfallLog.length > options.historyLimit) waterfallLog.shift();
1445
+ }
1446
+ // The verdict: trailing run of links that were each a real wait. A fast
1447
+ // tail (settled preload/cache hit) or a fast upstream breaks the sequence.
1448
+ if (t < o.minFlightMs) return;
1449
+ let i = 1;
1450
+ let s = t;
1451
+ for (let e = n.length - 1; e >= 0 && n[e].ms >= o.minFlightMs; e--) {
1452
+ i++;
1453
+ s += n[e].ms;
1454
+ }
1455
+ if (i < 2) return;
1456
+ const r = e;
1457
+ if ((r.Ke ?? 0) >= i) return;
1458
+ r.Ke = i;
1459
+ const a = [ ...n.slice(n.length - (i - 1)), {
1460
+ name: nodeName(e),
1461
+ ms: t
1462
+ } ];
1463
+ const c = a.map(e => `"${e.name}" (${e.ms.toFixed(0)}ms)`).join(" → ");
1464
+ const d = `[ASYNC_WATERFALL] ${i} sequential async flights — ${c} — ` + `${s.toFixed(0)}ms serialized: each began only after the previous resolved ` + `(as far as this graph can see). If a later request doesn't need the earlier ` + `response, derive both from the same inputs so they start together; if the ` + `dependency is intrinsic, preload the dependent data or join the requests ` + `server-side. If this work WAS already started elsewhere (a preloader or request ` + `cache), have that layer stamp its promises with attribution.markFlight() ` + `from "@solidjs/signals/attribution".`;
1465
+ // Depth 2 is advisory-only (structured consumers see it; the console does
1466
+ // not): a 2-chain can be an intrinsic data dependency or an unmarked
1467
+ // preload. A 3+ chain that survived the origin test is near-certainly
1468
+ // structural — that one earns the console.
1469
+ const f = i > 2 ? "warn" : "info";
1470
+ emitDiagnostic({
1471
+ code: "ASYNC_WATERFALL",
1472
+ kind: "perf",
1473
+ severity: f,
1474
+ message: d,
1475
+ nodeName: nodeName(e),
1476
+ data: {
1477
+ chain: a.map(e => ({
1478
+ name: e.name,
1479
+ ms: e.ms
1480
+ })),
1481
+ sequentialMs: s
1482
+ }
1483
+ }, e);
1484
+ }
1485
+
1486
+ const holdStates = new WeakMap;
1487
+
1488
+ let activeHold = null;
1489
+
1490
+ let holdLog = [];
1491
+
1492
+ /** A lane's derivation (L2: a node carrying the lane's value that is not a
1493
+ * written guess — lanes.ts) is a lane pass's result, not a write anyone
1494
+ * made: not an acknowledgement. */ function isCompanion(e) {
1495
+ return (e.l & (CONFIG_OVERRIDE | CONFIG_GUESS)) === CONFIG_OVERRIDE;
1496
+ }
1497
+
1498
+ const HOLD_CENSUS_CAP = 1e4;
1499
+
1500
+ function acknowledge(e, n, t, o) {
1501
+ const i = `${n}:${t}`;
1502
+ const s = e.acknowledgements.get(i);
1503
+ const r = o !== null ? ownerPath(o) : undefined;
1504
+ // A later census (a merged transition, the settle pass) may find the
1505
+ // reader an earlier one missed.
1506
+ if (s === undefined) e.acknowledgements.set(i, r !== undefined ? {
1507
+ kind: n,
1508
+ source: t,
1509
+ reader: r
1510
+ } : {
1511
+ kind: n,
1512
+ source: t
1513
+ }); else if (r !== undefined) s.reader ??= r;
1514
+ }
1515
+
1516
+ /** The declarations made against the hold: the guesses of its lanes
1517
+ * (lanes.ts `_laneGuesses`) and its `affects()` marks (`_marks`). */ function censusRegistrations(e, n) {
1518
+ const t = {
1519
+ left: HOLD_CENSUS_CAP
1520
+ };
1521
+ const o = GlobalQueue.xe?.(e);
1522
+ if (o !== undefined) for (const e of o) acknowledge(n, "optimistic", nodeName(e), reachesEffect(e, t));
1523
+ if (e.I !== null) for (const o of e.I) acknowledge(n, "affects", nodeName(o), reachesEffect(o, t));
1524
+ }
1525
+
1526
+ /**
1527
+ * Does anything that paints read `companion` — an effect, through however many
1528
+ * memos? A subscriber alone is not acknowledgement: memos compute eagerly, so a
1529
+ * router's `createMemo(() => isPending(location))` subscribes to the companion
1530
+ * whether or not the app ever renders the memo. Only an effect is the screen.
1531
+ * Returns the first effect found (the reader), or null.
1532
+ */ function reachesEffect(e, n) {
1533
+ const t = new Set([ e ]);
1534
+ const o = [ e ];
1535
+ while (o.length > 0 && n.left-- > 0) {
1536
+ const e = o.pop();
1537
+ for (let n = e.o; n !== null; n = n.t) {
1538
+ const e = n.u;
1539
+ if (e.i) return e;
1540
+ if (!t.has(e)) {
1541
+ t.add(e);
1542
+ o.push(e);
1543
+ }
1544
+ }
1545
+ }
1546
+ return null;
1547
+ }
1548
+
1549
+ /** Verdict readers an effect reads, anywhere downstream of the hold's nodes
1550
+ * (L2: a pass that entered an `isPending`/`latest` window is marked
1551
+ * CONFIG_VERDICT; the observe tier notes which windows — verdict.ts
1552
+ * `_devWindows`). The reader acknowledges the node it derives from. */ function censusCompanions(e, n) {
1553
+ const t = new Set;
1554
+ const o = [ ...e ];
1555
+ const i = {
1556
+ left: HOLD_CENSUS_CAP
1557
+ };
1558
+ while (o.length > 0 && t.size < HOLD_CENSUS_CAP) {
1559
+ const e = o.pop();
1560
+ if (t.has(e)) continue;
1561
+ t.add(e);
1562
+ for (let t = e.o; t !== null; t = t.t) {
1563
+ const s = t.u;
1564
+ if (s.l & CONFIG_VERDICT) {
1565
+ const t = s.Ve ?? 0;
1566
+ const o = s.i ? s : reachesEffect(s, i);
1567
+ if (t & 1 && o !== null) acknowledge(n, "isPending", nodeName(e), o);
1568
+ if (t & 2 && o !== null) acknowledge(n, "latest", nodeName(e), o);
1569
+ }
1570
+ o.push(s);
1571
+ }
1572
+ }
1573
+ }
1574
+
1575
+ function holdState(e) {
1576
+ let n = holdStates.get(e);
1577
+ if (n === undefined) {
1578
+ n = {
1579
+ start: now(),
1580
+ flushes: 0,
1581
+ blockers: new Set,
1582
+ acknowledgements: new Map,
1583
+ painted: 0,
1584
+ action: false
1585
+ };
1586
+ holdStates.set(e, n);
1587
+ }
1588
+ return n;
1589
+ }
1590
+
1591
+ function trackHoldStart(e) {
1592
+ // Navigations and interactions learn they are held regardless of hold
1593
+ // tracking: their settle must wait for the transition either way (see flushEnd).
1594
+ markNavigationsHeld(e);
1595
+ markInteractionsHeld(e);
1596
+ if (options.holds === false) return;
1597
+ const n = holdState(e);
1598
+ n.flushes++;
1599
+ if (e.tt > 0) n.action = true;
1600
+ for (const t of blockersOf(e)) n.blockers.add(t);
1601
+ censusRegistrations(e, n);
1602
+ activeHold = n;
1603
+ }
1604
+
1605
+ function trackHoldMerge(e, n) {
1606
+ mergeInteractionsHeld(e, n);
1607
+ const t = holdStates.get(n);
1608
+ if (t === undefined) return;
1609
+ holdStates.delete(n);
1610
+ const o = holdState(e);
1611
+ if (t.start < o.start) o.start = t.start;
1612
+ o.flushes += t.flushes;
1613
+ o.painted += t.painted;
1614
+ o.action ||= t.action;
1615
+ for (const e of t.blockers) o.blockers.add(e);
1616
+ for (const [e, n] of t.acknowledgements) if (!o.acknowledgements.has(e)) o.acknowledgements.set(e, n);
1617
+ }
1618
+
1619
+ function trackHoldSettled(e) {
1620
+ const n = holdStates.get(e);
1621
+ if (n === undefined) {
1622
+ // No hold was recorded (the transition completed in its first flush, or
1623
+ // hold tracking is off): navigations and interactions staged in it still
1624
+ // settle here.
1625
+ settleNavigations(e, undefined);
1626
+ settleInteractionsHeld(e, undefined);
1627
+ return;
1628
+ }
1629
+ holdStates.delete(e);
1630
+ // Root writes only: a memo in _pendingNodes is a derived hold, and the
1631
+ // question is whether the USER's input went unanswered.
1632
+ const t = [];
1633
+ let o = null;
1634
+ let i;
1635
+ let s;
1636
+ let r = -Infinity;
1637
+ for (const n of e.Ye) {
1638
+ if (typeof n.X === "function" || isCompanion(n)) continue;
1639
+ const e = n.ge;
1640
+ if (e === undefined || e.kind !== "write") continue;
1641
+ if (o === null) o = n;
1642
+ const a = {
1643
+ name: nodeName(n),
1644
+ prev: e.prev,
1645
+ value: e.value
1646
+ };
1647
+ if (e.origin !== undefined) a.origin = e.origin;
1648
+ t.push(a);
1649
+ // Earliest interaction among the held writes: the user has been waiting
1650
+ // since the first thing they did that this transaction is holding.
1651
+ const c = interactionOf(e.origin);
1652
+ if (c !== undefined && (i === undefined || c.at < i.at)) i = c;
1653
+ // The declared frame the writes belong to — earliest navigation, by the
1654
+ // same reasoning.
1655
+ if (e.origin?.kind === "navigation" && (s === undefined || e.origin.at < s.at)) s = e.origin;
1656
+ // Latest write: a signal written twice while held carries the later
1657
+ // stamp, so this is the user's final input, not their first.
1658
+ if (e.at !== undefined && e.at > r) r = e.at;
1659
+ }
1660
+ if (t.length === 0) {
1661
+ settleNavigations(e, undefined);
1662
+ settleInteractionsHeld(e, undefined);
1663
+ return;
1664
+ }
1665
+ censusRegistrations(e, n);
1666
+ censusCompanions([ ...e.Ye, ...n.blockers ], n);
1667
+ const a = now();
1668
+ // The hold began no later than its first parked flush; an interaction stamp
1669
+ // reaches further back (dispatch). A node rewritten mid-hold keeps only its
1670
+ // latest record, so the surviving interaction may be a later one — the
1671
+ // flush clock keeps the first wait from being forgotten.
1672
+ const c = Math.min(n.start, i !== undefined ? i.at : Infinity);
1673
+ const d = a - c;
1674
+ const f = r === -Infinity ? d : Math.min(d, a - r);
1675
+ const u = [ ...n.acknowledgements.values() ];
1676
+ // The verdicts, stamped on the record so a consumer — in-process or
1677
+ // offline — applies the engine's own tiering rather than a threshold of
1678
+ // its own: silent is duration-free (the finding adds the floor); long is
1679
+ // the tail against `longHolds.infoMs` as the options stand at settle.
1680
+ const l = options.longHolds;
1681
+ const h = {
1682
+ at: c,
1683
+ holdMs: d,
1684
+ tailMs: f,
1685
+ flushes: n.flushes,
1686
+ heldWrites: t,
1687
+ blockers: [ ...n.blockers ].map(nodeName),
1688
+ acknowledgements: u,
1689
+ paintedDuringHold: n.painted,
1690
+ action: n.action,
1691
+ silent: n.painted === 0 && u.length === 0,
1692
+ long: l !== false && l !== undefined && f >= l.infoMs
1693
+ };
1694
+ if (i !== undefined) h.interaction = i;
1695
+ if (s !== undefined) h.origin = s;
1696
+ holdLog.push(h);
1697
+ if (holdLog.length > options.historyLimit) holdLog.shift();
1698
+ // Bottom-up delivery: the hold, then the navigations it held, then the
1699
+ // interactions those belong to — each record complete when its parent is.
1700
+ // `live` is the first held root write's signal: the subject the findings
1701
+ // below name.
1702
+ records.emit("hold", h, o);
1703
+ checkStackedHolds(e, h, o);
1704
+ settleNavigations(e, h);
1705
+ settleInteractionsHeld(e, h);
1706
+ for (const e of folds) e.hold?.(h);
1707
+ if (h.silent) checkSilentHold(h, o); else checkLongHold(h, o);
1708
+ }
1709
+
1710
+ function describeHeldWrites(e) {
1711
+ return e.heldWrites.map(e => e.prev !== undefined ? `"${e.name}" (${e.prev} → ${e.value})` : `"${e.name}"`).join(", ");
1712
+ }
1713
+
1714
+ function describeBlockers(e, n) {
1715
+ return e.blockers.length > 0 ? ` ${n} ${e.blockers.map(e => `"${e}"`).join(", ")}` : "";
1716
+ }
1717
+
1718
+ /**
1719
+ * The boundary repair, shared by LONG_HOLD and a long SILENT_HOLD: a wait
1720
+ * this long should show a fallback, not a stale screen. A `Loading` boundary
1721
+ * lifts the write out of the hold only when it has not revealed yet or its
1722
+ * `on` prop changed — a revealed boundary with no `on` IS the stale screen.
1723
+ */ function boundaryRepair(e) {
1724
+ const n = e.heldWrites[0]?.name ?? "key";
1725
+ return `A wait this long is past what a stale screen should carry: show a fallback instead. Put ` + `the reader behind a Loading boundary keyed on what changed — <Loading on={${n}()} ` + `fallback={…}> — so the write commits at once and the fallback shows where the data lands; ` + `a boundary that has already revealed keeps the old content unless \`on\` changes. If the ` + `data itself is the problem, preload it or cache it so the wait never gets this long.`;
1726
+ }
1727
+
1728
+ function holdData(e) {
1729
+ const n = {
1730
+ holdMs: e.holdMs,
1731
+ tailMs: e.tailMs,
1732
+ flushes: e.flushes,
1733
+ heldWrites: e.heldWrites.map(e => e.name),
1734
+ blockers: e.blockers,
1735
+ action: e.action
1736
+ };
1737
+ if (e.interaction !== undefined) n.interaction = {
1738
+ type: e.interaction.name,
1739
+ target: e.interaction.target
1740
+ };
1741
+ if (e.origin?.kind === "navigation") n.navigation = navigationData(e.origin);
1742
+ return n;
1743
+ }
1744
+
1745
+ /**
1746
+ * Who the verdict sentence starts from: the interaction, with the navigation
1747
+ * it performed in parentheses — `click on a.nav (navigation to /users/:id)`;
1748
+ * the navigation alone when nothing user-dispatched is known (a redirect);
1749
+ * empty when neither is.
1750
+ */ function holdActor(e) {
1751
+ const n = e.origin !== undefined ? formatOrigin(e.origin) : "";
1752
+ if (e.interaction !== undefined) return `${formatOrigin(e.interaction)}${n ? ` (${n})` : ""}`;
1753
+ return n;
1754
+ }
1755
+
1756
+ function checkSilentHold(e, n) {
1757
+ const t = options.holds;
1758
+ if (t === false) return;
1759
+ if (e.holdMs < t.infoMs) return;
1760
+ const o = e.holdMs.toFixed(0);
1761
+ const i = describeHeldWrites(e);
1762
+ const s = describeBlockers(e, "waiting on");
1763
+ // With the interaction (or navigation) stamped the sentence starts from
1764
+ // what the user did; without it, from the writes.
1765
+ const r = holdActor(e);
1766
+ const a = r ? `${r} ` : "";
1767
+ let c = e.action ? `[SILENT_HOLD] ${a}${a ? "started an action that" : "an action"} held ${i} for ` + `${o}ms${s} and the screen showed nothing for the whole round-trip: no optimistic ` + `value, no isPending() reader, no affects() mark, and no effect ran while it was held. ` + `Pair the action with a createOptimistic/createOptimisticStore write for the expected ` + `outcome (it reverts on failure), or co-write a createOptimistic(false) "saving" flag ` + `the UI reads.` : `[SILENT_HOLD] ${a}${a ? "wrote" : "writes to"} ${i}${a ? "; the write was" : " were"} ` + `held ${o}ms${s} and the screen showed nothing for the wait: no ` + `isPending()/latest() reader downstream, no optimistic value, no affects() mark, and no ` + `effect ran while it was held — the interaction was dead for ${o}ms. Show the wait: ` + `read isPending(() => ${e.blockers[0] ?? "source"}()) to render a busy state, or ` + `latest(${e.heldWrites[0].name}) to reveal the new input immediately while the data ` + `catches up. The hold itself is correct — do not "fix" this by moving the write off the ` + `async path.`;
1768
+ const d = e.long;
1769
+ if (d) c += ` ${boundaryRepair(e)}`;
1770
+ const f = e.holdMs >= t.warnMs ? "warn" : "info";
1771
+ const u = holdData(e);
1772
+ u.long = d;
1773
+ emitDiagnostic({
1774
+ code: "SILENT_HOLD",
1775
+ kind: "responsiveness",
1776
+ severity: f,
1777
+ message: c,
1778
+ nodeName: nodeName(n),
1779
+ data: u
1780
+ }, n);
1781
+ }
1782
+
1783
+ function checkLongHold(e, n) {
1784
+ const t = options.longHolds;
1785
+ if (t === false || t === undefined) return;
1786
+ if (e.tailMs < t.infoMs) return;
1787
+ const o = e.tailMs.toFixed(0);
1788
+ const i = describeHeldWrites(e);
1789
+ const s = describeBlockers(e, "waiting on");
1790
+ const r = holdActor(e);
1791
+ const a = r ? `${r} ` : "";
1792
+ const c = e.acknowledgements.length > 0 ? `${e.acknowledgements.map(e => `"${e.kind}:${e.source}"`).join(", ")} said it was pending` : `an effect painted meanwhile`;
1793
+ const d = e.tailMs < e.holdMs - 1 ? ` after the last input (${e.holdMs.toFixed(0)}ms in all)` : "";
1794
+ const f = `[LONG_HOLD] ${a}${a ? "wrote" : "writes to"} ${i}; the screen kept the old ` + `content for ${o}ms${d}${s} — ${c}, but the hold ran on well past ` + `the point where "loading" over stale content reads as broken. ${boundaryRepair(e)}`;
1795
+ const u = e.tailMs >= t.warnMs ? "warn" : "info";
1796
+ const l = holdData(e);
1797
+ l.acknowledgements = e.acknowledgements;
1798
+ emitDiagnostic({
1799
+ code: "LONG_HOLD",
1800
+ kind: "responsiveness",
1801
+ severity: u,
1802
+ message: f,
1803
+ nodeName: nodeName(n),
1804
+ data: l
1805
+ }, n);
1806
+ }
1807
+
1808
+ /**
1809
+ * The person saw the guess, then the correction. An optimistic value is a
1810
+ * promise the UI makes about the outcome; when the outcome differs — the
1811
+ * action failed and the override lifted back to the old value, or the
1812
+ * source answered with something else — the screen changes twice for one
1813
+ * intent. Expected on failure and correct by construction (the override
1814
+ * reverts; that is the feature), so `info`: a count that grows for one
1815
+ * source is what says the guess, or the failure rate, is wrong. Judged by
1816
+ * the node's own equality, so a structurally equal replacement is not a
1817
+ * revert.
1818
+ */ function checkOptimisticRevert(e, n, t, o) {
1819
+ // The runtime's own optimistic nodes are not guesses the person saw: an
1820
+ // `isPending()` companion goes true while pending and back to false at
1821
+ // commit by design — the acknowledgement SILENT_HOLD asks for — and a
1822
+ // derived override promotes rather than reverts. Same predicate the hold
1823
+ // census uses to skip them.
1824
+ if (!options.optimisticReverts || isCompanion(e)) return;
1825
+ // Method call, as every commit path does: store slot nodes share one
1826
+ // comparator that reads `this._host` (#3687).
1827
+ const i = e.je;
1828
+ if (i && i.call(e, n, t)) return;
1829
+ const s = nodeName(e);
1830
+ // The two values are user data: quoted only under `values: "full"`; the
1831
+ // other levels say what happened without saying what was shown.
1832
+ const r = options.values === "full";
1833
+ const a = o === "superseded" ? "settled to" : "reverted to";
1834
+ const c = `[OPTIMISTIC_REVERTED] the optimistic value of ${s} ` + (r ? `showed ${preview(n)}; it ${a} ${preview(t)}.` : `${o === "superseded" ? "was superseded by the settled value" : "reverted at settle"}.`) + ` The person saw the guess, then the correction. A revert on failure is the feature; one ` + `that recurs says the guess is wrong for this input or the action fails often — show the ` + `failure where the value renders (the action's catch, an Errored boundary) rather than ` + `letting the value snap back on its own.`;
1835
+ const d = {
1836
+ source: s,
1837
+ how: o
1838
+ };
1839
+ if (r) {
1840
+ d.shown = preview(n);
1841
+ d.truth = preview(t);
1842
+ }
1843
+ emitDiagnostic({
1844
+ code: "OPTIMISTIC_REVERTED",
1845
+ kind: "responsiveness",
1846
+ severity: "info",
1847
+ message: c,
1848
+ nodeName: s,
1849
+ data: d
1850
+ }, e);
1851
+ }
1852
+
1853
+ // --- Graph growth -----------------------------------------------------------------
1854
+ /**
1855
+ * Per route: at its last `visits` compared settles, oldest first, the
1856
+ * graph's size and the part of it already alive as a navigation opened.
1857
+ */ const routeCounts = new Map;
1858
+
1859
+ /**
1860
+ * The route the app last arrived on: a navigation that stays on it (only
1861
+ * its params or query change) continues the visit and is not compared.
1862
+ */ let visitRoute;
1863
+
1864
+ /**
1865
+ * Every node met as a navigation opened, weakly, for the session: one set,
1866
+ * not one per navigation (see `trackGraph`). Never reset — a node in it was
1867
+ * alive as a navigation opened whichever window met it.
1868
+ */ const opened = new WeakSet;
1869
+
1870
+ /**
1871
+ * The live graph's size — a walk, not a counter: nothing is charged at node
1872
+ * creation, disposal, write or re-run; the engine asks at a navigation's
1873
+ * settle. Two passes. The owner tree from the registered top-level roots
1874
+ * gives `owners` and seeds the computations. Then each computation's
1875
+ * dependency links give `edges` and the `signals` and computations they
1876
+ * reach, and each newly met source's subscriber list gives the computations
1877
+ * that read it — including one created with no owner, which no chain holds
1878
+ * and only its sources keep alive: the subscription leak a heap snapshot
1879
+ * finds. Measured at 5–10 ns per link; a 50k-owner graph walks in under a
1880
+ * millisecond. Dormant nodes are spliced out of their chain and are not
1881
+ * counted unless a subscription still reaches them.
1882
+ */ function graphSize() {
1883
+ return walkGraph();
1884
+ }
1885
+
1886
+ /**
1887
+ * The walk behind `graphSize()`. Given `before` alone, it adds every node it
1888
+ * meets to that set: what is alive as a navigation opens. Given `before` and
1889
+ * `survived`, it also counts into `survived` the nodes it meets that are in
1890
+ * the set, and an edge when both its ends are: what outlived a navigation.
1891
+ * Nothing is retained — the set is weak.
1892
+ */ function walkGraph(e, n) {
1893
+ const t = liveRootOwners();
1894
+ const o = {
1895
+ roots: 0,
1896
+ owners: 0,
1897
+ computations: 0,
1898
+ signals: 0,
1899
+ edges: 0
1900
+ };
1901
+ const i = new Set;
1902
+ // A worklist, not recursion: a subscriber chain can be thousands deep.
1903
+ const s = [];
1904
+ const r = [];
1905
+ const count = (t, i) => {
1906
+ o[i]++;
1907
+ if (n !== undefined) {
1908
+ if (e.has(t)) n[i]++;
1909
+ } else if (e !== undefined) e.add(t);
1910
+ };
1911
+ const meet = e => {
1912
+ if (i.has(e)) return;
1913
+ i.add(e);
1914
+ count(e, typeof e.X === "function" ? "computations" : "signals");
1915
+ s.push(e);
1916
+ };
1917
+ for (const e of t) {
1918
+ count(e, "roots");
1919
+ count(e, "owners");
1920
+ // Iterative: a deep tree must not grow the call stack.
1921
+ let n = e.ze;
1922
+ for (;;) {
1923
+ while (n !== null) {
1924
+ count(n, "owners");
1925
+ if (typeof n.X === "function") meet(n);
1926
+ if (n.ze !== null) r.push(n);
1927
+ n = n.Xe;
1928
+ }
1929
+ const e = r.pop();
1930
+ if (e === undefined) break;
1931
+ n = e.ze;
1932
+ }
1933
+ }
1934
+ for (let t = 0; t < s.length; t++) {
1935
+ const i = s[t];
1936
+ // Whoever reads this node: owned computations are already known; an
1937
+ // ownerless one is met only here.
1938
+ for (let e = i.o; e !== null; e = e.t) meet(e.u);
1939
+ if (typeof i.X === "function") for (let t = i.C; t !== null; t = t.G) {
1940
+ o.edges++;
1941
+ meet(t.S);
1942
+ if (n !== undefined && e.has(i) && e.has(t.S)) n.edges++;
1943
+ }
1944
+ }
1945
+ return o;
1946
+ }
1947
+
1948
+ /**
1949
+ * openNavigation, while the growth check is on: what is alive as a
1950
+ * navigation opens, added to what earlier ones met, for its settle to tell
1951
+ * apart from what it created.
1952
+ */ function snapshotGraph() {
1953
+ walkGraph(opened);
1954
+ return opened;
1955
+ }
1956
+
1957
+ /**
1958
+ * settleNavigation: the app has finished moving to a route — count the live
1959
+ * graph. One record per settle for a listener; the growth check compares
1960
+ * this settle of the route with its previous ones. A count that climbs on
1961
+ * consecutive visits is something the previous visit left behind: a
1962
+ * `createRoot` in an effect with no dispose, a subscription a component
1963
+ * registered outside its owner. Nothing here runs per node; the walk is
1964
+ * the cost, at navigation cadence.
1965
+ *
1966
+ * A count taken at an arrival also holds everything the visit just created,
1967
+ * so a page whose correctly owned content is bigger each visit climbs too.
1968
+ * What tells the two apart is what outlived a navigation: the nodes alive
1969
+ * as this or any earlier navigation opened (`opened`) that are still alive
1970
+ * at this settle. A visit's own content is not among them; what an earlier
1971
+ * visit left behind is, whatever the content does. A series reports only
1972
+ * when both climb.
1973
+ *
1974
+ * The set is the session's, not this navigation's own snapshot: the walk
1975
+ * meets a long-lived source only through a page that reads it, so a
1976
+ * per-navigation snapshot holds a module-level signal (an HMR registry, a
1977
+ * global store) only when the page just left read it too, and arriving from
1978
+ * pages that share ever more with this one read as growth (#4101). Kept
1979
+ * across navigations, such a source counts at every settle once met, the
1980
+ * same each visit; only what keeps accumulating climbs.
1981
+ */ const GRAPH_SERIES = [ "owners", "computations", "signals", "edges" ];
1982
+
1983
+ function trackGraph(e) {
1984
+ const n = e.event;
1985
+ const t = e.before;
1986
+ e.before = undefined;
1987
+ const o = options.graphGrowth;
1988
+ if (o === false && !records.observed("graph")) return;
1989
+ const i = t && {
1990
+ roots: 0,
1991
+ owners: 0,
1992
+ computations: 0,
1993
+ signals: 0,
1994
+ edges: 0
1995
+ };
1996
+ const s = walkGraph(t, i);
1997
+ const r = n.name ?? n.to;
1998
+ const a = {
1999
+ at: now(),
2000
+ ...s,
2001
+ navigation: n
2002
+ };
2003
+ if (r !== undefined) a.route = r;
2004
+ records.emit("graph", a, undefined);
2005
+ if (o === false || r === undefined) return;
2006
+ // A navigation that stays on the route pattern continues the visit: the
2007
+ // page it kept outlived the move by design, so comparing it would read the
2008
+ // page itself as growth. Only arrivals that enter the route are compared —
2009
+ // a leak made during a visit is still alive when the next one starts. A
2010
+ // superseded navigation never showed, so the app is still where it was.
2011
+ const c = visitRoute;
2012
+ if (n.outcome !== "superseded") visitRoute = r;
2013
+ // The initial declaration is not a visit (it settles before route content
2014
+ // mounts — the same gate the feedback folds apply as `event.initial !== true`);
2015
+ // `initial` is `true | undefined`, set only by openNavigation, so truthiness is exact.
2016
+ // A navigation opened before the check was on has no snapshot to compare.
2017
+ if (n.initial || r === c || i === undefined) return;
2018
+ let d = routeCounts.get(r);
2019
+ if (d === undefined) routeCounts.set(r, d = []);
2020
+ d.push({
2021
+ size: s,
2022
+ survived: i
2023
+ });
2024
+ if (d.length > o.visits) d.shift();
2025
+ if (d.length < o.visits) return;
2026
+ const f = d.map(e => e.size);
2027
+ const u = d.map(e => e.survived);
2028
+ const climbs = (e, n) => {
2029
+ for (let t = 1; t < e.length; t++) if (e[t][n] <= e[t - 1][n]) return false;
2030
+ return e[e.length - 1][n] >= e[0][n] * o.ratio;
2031
+ };
2032
+ // Any series climbing on every visit to the ratio — in the totals and in
2033
+ // what outlived each navigation — is growth; which ones did says what
2034
+ // leaked — computations with owners flat is an ownerless effect per visit,
2035
+ // edges alone is a subscription per visit to something long-lived, owners
2036
+ // is an undisposed root.
2037
+ const l = GRAPH_SERIES.filter(e => climbs(f, e) && climbs(u, e));
2038
+ if (l.length === 0) return;
2039
+ // The count is the whole graph's, so a leak shows at every route's settle;
2040
+ // the first route to complete its climb reports, naming the others seen.
2041
+ const h = [ ...routeCounts.keys() ];
2042
+ const join = (e, n) => e.map(e => e[n]).join(" → ");
2043
+ const p = l.map(e => `${e} ${join(u, e)} (of ${join(f, e)})`).join("; ");
2044
+ const m = l.includes("owners") ? "a createRoot() in an effect or handler with no dispose, or a Portal or panel mounted per visit" : l.includes("computations") ? "an effect or memo created with no owner (a module-level or callback createEffect) that only its sources keep alive" : "a subscription per visit to something long-lived — a global store or signal read by a computation that outlives the visit";
2045
+ const g = `[GRAPH_GROWTH] the live graph grew on ${o.visits} consecutive visits to ${r}, and so did ` + `the part that outlived each navigation there: ${p} ` + `(${s.roots} roots)${h.length > 1 ? `, across visits to ${h.join(", ")}` : ""}. Each ` + `visit left something behind that the next did not reclaim — the shape says ${m}. Dispose ` + `what a visit creates (onCleanup, or return the disposer from onSettled) and own it under the ` + `route's component so leaving the route tears it down. A cache that never evicts grows alike.`;
2046
+ const w = {
2047
+ route: r,
2048
+ grew: l,
2049
+ history: f,
2050
+ survived: u,
2051
+ roots: s.roots,
2052
+ routes: h
2053
+ };
2054
+ if (n.interaction !== undefined) w.interaction = {
2055
+ type: n.interaction.name,
2056
+ target: n.interaction.target
2057
+ };
2058
+ reportDiagnostic(emitDiagnostic({
2059
+ code: "GRAPH_GROWTH",
2060
+ kind: "perf",
2061
+ severity: "warn",
2062
+ message: g,
2063
+ data: w
2064
+ }, null));
2065
+ // Judged once for the graph, not once per route: the next verdict needs
2066
+ // another `visits` climbing settles.
2067
+ routeCounts.clear();
2068
+ }
2069
+
2070
+ // --- Feedback-table findings -----------------------------------------------------
2071
+ /** Per async source: abandoned flights inside the current window. Engine-side; the node carries nothing. */ const abandonWindows = new WeakMap;
2072
+
2073
+ /**
2074
+ * One source abandoning flight after flight inside a window: every input
2075
+ * asked again and the earlier answers were thrown away — the
2076
+ * request-per-keystroke signature `feedback().flights[].abandoned` counts,
2077
+ * as a finding while it happens. Warned once per window per source. A
2078
+ * framework node superseding its own flights by design (CONFIG_SUPERSEDES)
2079
+ * is not judged; its flights are still recorded.
2080
+ */ function checkAbandonedFlights(e, n) {
2081
+ const t = options.abandonedFlights;
2082
+ // Only dev sets CONFIG_SUPERSEDES (see constants.ts).
2083
+ if (t === false || false || excludedNode(e)) return;
2084
+ const o = Date.now();
2085
+ let i = abandonWindows.get(e);
2086
+ if (i === undefined || o - i.start > t.windowMs) {
2087
+ i = {
2088
+ start: o,
2089
+ count: 0,
2090
+ warned: false
2091
+ };
2092
+ abandonWindows.set(e, i);
2093
+ }
2094
+ i.count++;
2095
+ if (i.warned || i.count < t.count) return;
2096
+ i.warned = true;
2097
+ const s = nodeName(e);
2098
+ const r = n.interaction !== undefined ? ` (${formatOrigin(n.interaction)} started the first)` : "";
2099
+ const a = `[ABANDONED_FLIGHTS] "${s}" abandoned ${i.count} flights in ${t.windowMs}ms` + `${r}: each was superseded by the next before it landed, so every input asked again and the ` + `answers were discarded. Put a debounced or equality-gated derivation between the input and ` + `the fetch, or key the fetch on what changes rather than on every keystroke; a preload the ` + `flights share (markFlight) reads as one flight, not many.`;
2100
+ const c = {
2101
+ source: s,
2102
+ abandoned: i.count,
2103
+ windowMs: t.windowMs
2104
+ };
2105
+ if (n.interaction !== undefined) c.interaction = {
2106
+ type: n.interaction.name,
2107
+ target: n.interaction.target
2108
+ };
2109
+ emitDiagnostic({
2110
+ code: "ABANDONED_FLIGHTS",
2111
+ kind: "responsiveness",
2112
+ severity: "warn",
2113
+ message: a,
2114
+ nodeName: s,
2115
+ data: c
2116
+ }, e);
2117
+ }
2118
+
2119
+ /**
2120
+ * A fallback that appeared and vanished: the other end of the SILENT_HOLD
2121
+ * spectrum — feedback for a wait too short to need it, which reads as a
2122
+ * flicker. `info`, once per flash; `feedback().fallbacks[].flashes` is the
2123
+ * count. The repair is a preload, a cache, or lifting the fetch above the
2124
+ * boundary so the data is there before the boundary asks.
2125
+ */ function checkFallbackFlash(e, n, t) {
2126
+ if (e !== undefined && excludedNode(e)) return;
2127
+ const o = e !== undefined ? ownerPath(e)?.join(" › ") : undefined;
2128
+ const i = t !== undefined ? `${formatOrigin(t)}: ` : "";
2129
+ const s = `[FALLBACK_FLASH] ${i}the Loading fallback${o ? ` at ${o}` : ""} showed for ` + `${n.toFixed(0)}ms — a spinner that appeared and vanished, feedback for a wait too short ` + `to need it. Preload or cache the data so it is there before the boundary asks, or lift the ` + `read above the boundary; a fallback under ${FALLBACK_FLASH_MS}ms reads as a flicker.`;
2130
+ const r = {
2131
+ shownMs: n
2132
+ };
2133
+ if (t !== undefined) r.interaction = {
2134
+ type: t.name,
2135
+ target: t.target
2136
+ };
2137
+ emitDiagnostic({
2138
+ code: "FALLBACK_FLASH",
2139
+ kind: "responsiveness",
2140
+ severity: "info",
2141
+ message: s,
2142
+ data: r
2143
+ }, e ?? null);
2144
+ }
2145
+
2146
+ /**
2147
+ * Several interactions waiting in one hold when it commits: the person kept
2148
+ * clicking or typing while the first answer was in the air, and all of them
2149
+ * waited on the same source. The pile is the symptom; the hold's own verdict
2150
+ * (SILENT_HOLD / LONG_HOLD) is the cause, so the repair is the same
2151
+ * acknowledgement, plus a control that does not accept the repeat.
2152
+ */ function checkStackedHolds(e, n, t) {
2153
+ const o = options.stackedHolds;
2154
+ if (o === false) return;
2155
+ let i = 0;
2156
+ for (const n of openInteractions) if (n.heldIn.has(e)) i++;
2157
+ if (i < o.count) return;
2158
+ const s = describeBlockers(n, "waiting on");
2159
+ const r = `[STACKED_HOLDS] ${i} interactions queued behind one hold${s} for ` + `${n.holdMs.toFixed(0)}ms: the person kept ${n.interaction?.name === "input" ? "typing" : "clicking"} ` + `while the first answer was in the air, and every repeat waited on the same source. ` + `Acknowledge the wait where the control is (isPending() to disable or dim it) so the repeats ` + `stop, or debounce the input; the hold itself is judged by SILENT_HOLD/LONG_HOLD.`;
2160
+ const a = holdData(n);
2161
+ a.interactions = i;
2162
+ emitDiagnostic({
2163
+ code: "STACKED_HOLDS",
2164
+ kind: "responsiveness",
2165
+ severity: "warn",
2166
+ message: r,
2167
+ nodeName: nodeName(t),
2168
+ data: a
2169
+ }, t);
2170
+ }
2171
+
2172
+ const navStates = new WeakMap;
2173
+
2174
+ /** Opened, not yet settled. */ const openNavs = new Set;
2175
+
2176
+ let navigationLog = [];
2177
+
2178
+ /** Drains completed since enable() — the clock `writeDrain` reads. */ let drainSeq = 0;
2179
+
2180
+ /**
2181
+ * Navigations settled in a landing, waiting for their graph count: the hook
2182
+ * fires before the landing's commits dispose the frame parked for the page
2183
+ * left, which the walk would still reach through the sources both pages read
2184
+ * (#3989). `land()` runs only inside `flush()`'s drain, so `flushEnd` counts
2185
+ * them, after those commits — the instant a one-drain navigation is counted.
2186
+ */ const landedGraphs = [];
2187
+
2188
+ /**
2189
+ * Copy what the router currently says onto the frame (what writes stamped —
2190
+ * `formatOrigin` reads it) and the event. Called when the frame opens, when a
2191
+ * redirect re-describes it, and when the record settles, so a description
2192
+ * refined during the hold is what every consumer ends up reading.
2193
+ */ function syncNavigation(e) {
2194
+ const {event: n, ref: t} = e;
2195
+ const o = n.origin;
2196
+ if (t.name === undefined) {
2197
+ delete o.name;
2198
+ delete n.name;
2199
+ } else o.name = n.name = t.name;
2200
+ if (t.to === undefined) {
2201
+ delete o.to;
2202
+ delete n.to;
2203
+ } else o.to = n.to = t.to;
2204
+ if (t.params === undefined) {
2205
+ delete o.params;
2206
+ delete n.params;
2207
+ } else o.params = n.params = t.params;
2208
+ }
2209
+
2210
+ function openNavigation(e, n) {
2211
+ const t = {
2212
+ at: e.at,
2213
+ writes: 0,
2214
+ origin: e
2215
+ };
2216
+ if (n.initial === true) t.initial = true;
2217
+ if (e.from !== undefined) t.from = e.from;
2218
+ if (e.interaction !== undefined) t.interaction = e.interaction;
2219
+ const o = {
2220
+ event: t,
2221
+ ref: n,
2222
+ open: 1,
2223
+ held: false,
2224
+ writeDrain: drainSeq
2225
+ };
2226
+ // The initial declaration is never compared (see `trackGraph`).
2227
+ if (options.graphGrowth !== false && n.initial !== true) o.before = snapshotGraph();
2228
+ syncNavigation(o);
2229
+ navStates.set(e, o);
2230
+ openNavs.add(o);
2231
+ navigationLog.push(t);
2232
+ if (navigationLog.length > options.historyLimit) navigationLog.shift();
2233
+ noteInteractionNavigation(t);
2234
+ }
2235
+
2236
+ /** The navigation a redirect hop folds onto: the most recently opened one still pending. */ function lastOpenNavigation() {
2237
+ let e;
2238
+ for (const n of openNavs) e = n;
2239
+ return e;
2240
+ }
2241
+
2242
+ /** A redirect re-describes `state`: the current destination becomes a hop it abandoned. */ function redirectNavigation(e, n) {
2243
+ const t = e.event;
2244
+ // As the router last described the destination being left behind.
2245
+ syncNavigation(e);
2246
+ const o = {
2247
+ at: n.at ?? now()
2248
+ };
2249
+ if (t.name !== undefined) o.name = t.name;
2250
+ if (t.to !== undefined) o.to = t.to;
2251
+ if (t.params !== undefined) o.params = t.params;
2252
+ (t.redirects ??= []).push(o);
2253
+ e.ref = n;
2254
+ e.open++;
2255
+ syncNavigation(e);
2256
+ }
2257
+
2258
+ function closeNavigation(e) {
2259
+ const n = navStates.get(e);
2260
+ if (n === undefined || --n.open > 0) return;
2261
+ syncNavigation(n);
2262
+ // Nothing to wait for: no write survived the equality gate (navigating to
2263
+ // where we already are), or a drain inside the frame already committed
2264
+ // them (`flush(() => setLocation(…))`) with no hold.
2265
+ if (n.event.writes === 0 || !n.held && drainSeq > n.writeDrain) settleNavigation(n, "committed");
2266
+ }
2267
+
2268
+ /** stampWrite: the record replacing `prior` on a node was just stamped. */ function noteNavigationWrite(e, n) {
2269
+ const t = n.origin;
2270
+ if (t.kind === "navigation") {
2271
+ const e = navStates.get(t);
2272
+ if (e !== undefined) {
2273
+ e.event.writes++;
2274
+ e.writeDrain = drainSeq;
2275
+ }
2276
+ }
2277
+ // The node now carries a different frame's record: whatever `prior`'s
2278
+ // navigation was waiting to show on it will never land as that navigation.
2279
+ const o = e?.origin;
2280
+ if (o !== undefined && o !== t && o.kind === "navigation") {
2281
+ const e = navStates.get(o);
2282
+ if (e !== undefined && openNavs.has(e)) settleNavigation(e, "superseded");
2283
+ }
2284
+ }
2285
+
2286
+ /** holdStart: `t`'s staged writes are parked — their navigations settle with `t`. */ function markNavigationsHeld(e) {
2287
+ if (openNavs.size === 0) return;
2288
+ for (const n of e.Ye) {
2289
+ const e = n.ge?.origin;
2290
+ if (e?.kind !== "navigation") continue;
2291
+ const t = navStates.get(e);
2292
+ if (t !== undefined) t.held = true;
2293
+ }
2294
+ }
2295
+
2296
+ /** transitionSettled: `t` commits — the navigations whose writes it staged are done. */ function settleNavigations(e, n) {
2297
+ if (openNavs.size === 0) return;
2298
+ for (const t of e.Ye) {
2299
+ const e = t.ge?.origin;
2300
+ if (e?.kind !== "navigation") continue;
2301
+ const o = navStates.get(e);
2302
+ if (o === undefined || !openNavs.has(o)) continue;
2303
+ // A navigation whose frame is still open when its transition commits
2304
+ // (`until()` inside the frame) settles here too: its writes are through.
2305
+ settleNavigation(o, o.held ? "held" : "committed", n, true);
2306
+ }
2307
+ }
2308
+
2309
+ let openFlush = null;
2310
+
2311
+ function noteFlushRun(e, n, t) {
2312
+ if (n) e.created++; else e.runs++;
2313
+ if (t === undefined || e.interaction === null) return;
2314
+ if (e.interaction === undefined) e.interaction = t; else if (e.interaction !== t) e.interaction = null;
2315
+ }
2316
+
2317
+ function trackFlushStart() {
2318
+ if (!records.observed("flush")) return;
2319
+ openFlush = {
2320
+ at: now(),
2321
+ runs: 0,
2322
+ created: 0,
2323
+ held: false,
2324
+ interaction: undefined
2325
+ };
2326
+ }
2327
+
2328
+ /** The key for swaps no transaction carries — an object, so the map below can be weak. */ const DRAIN = {
2329
+ drain: true
2330
+ };
2331
+
2332
+ const openFallbacks = new WeakMap;
2333
+
2334
+ // Weak on the transaction: one that is dropped without settling or merging
2335
+ // (its boundary disposed, its queues discarded) takes its staged opens with it.
2336
+ const stagedFallbacks = new WeakMap;
2337
+
2338
+ /** Bumped by `resetTracking` — the engine's install generation. */ let trackingGen = 0;
2339
+
2340
+ function trackFallback(e, n, t, o) {
2341
+ if (t) {
2342
+ // Folds count showings too, and the flash finding needs the show's
2343
+ // clock: an open is kept for any of the three audiences.
2344
+ const t = records.observed("fallback");
2345
+ if (!t && folds.length === 0 && options.fallbackFlashes === false) return;
2346
+ // The wait is the enclosing recompute's cause's interaction — the read
2347
+ // that registered the pending source runs inside one — else the ambient.
2348
+ const i = enclosingCauses();
2349
+ const s = o ?? DRAIN;
2350
+ const r = {
2351
+ at: now(),
2352
+ gen: trackingGen,
2353
+ record: t,
2354
+ boundary: e,
2355
+ tree: n,
2356
+ interaction: i !== null ? interactionIn(i) : currentInteraction ?? undefined,
2357
+ staged: s
2358
+ };
2359
+ openFallbacks.set(e, r);
2360
+ const a = stagedFallbacks.get(s);
2361
+ if (a === undefined) stagedFallbacks.set(s, [ r ]); else a.push(r);
2362
+ return;
2363
+ }
2364
+ const i = openFallbacks.get(e);
2365
+ if (i === undefined) return;
2366
+ openFallbacks.delete(e);
2367
+ if (i.staged !== null) {
2368
+ // Cleared before its commit: never on screen.
2369
+ const e = stagedFallbacks.get(i.staged);
2370
+ e.splice(e.indexOf(i), 1);
2371
+ if (e.length === 0) stagedFallbacks.delete(i.staged);
2372
+ return;
2373
+ }
2374
+ if (i.gen !== trackingGen) return;
2375
+ // The hide has the subtree the first show may have lacked.
2376
+ const s = n ?? i.tree;
2377
+ for (const n of folds) n.fallback?.(e, s, false);
2378
+ const r = now() - i.at;
2379
+ if (options.fallbackFlashes !== false && r < FALLBACK_FLASH_MS) checkFallbackFlash(s, r, i.interaction);
2380
+ if (!i.record) return;
2381
+ const a = {
2382
+ at: i.at,
2383
+ shownMs: r
2384
+ };
2385
+ const c = s !== undefined ? ownerPath(s) : undefined;
2386
+ if (c !== undefined) a.ownerPath = c;
2387
+ if (i.interaction !== undefined) a.interaction = i.interaction;
2388
+ records.emit("fallback", a, s);
2389
+ }
2390
+
2391
+ /** flushEnd: the drain's committed swaps have rendered — those fallbacks are on screen from here. */ function displayFallbacks() {
2392
+ const e = stagedFallbacks.get(DRAIN);
2393
+ if (e === undefined) return;
2394
+ stagedFallbacks.delete(DRAIN);
2395
+ const n = now();
2396
+ for (const t of e) {
2397
+ t.staged = null;
2398
+ t.at = n;
2399
+ if (t.gen === trackingGen) for (const e of folds) e.fallback?.(t.boundary, t.tree, true);
2400
+ }
2401
+ }
2402
+
2403
+ /** transitionSettled (`from` a transaction, into this drain) and
2404
+ * transitionMerged (`from` the outgoing, into the surviving transaction):
2405
+ * the swaps staged under `from` now land with `into`. */ function rebaseFallbacks(e, n) {
2406
+ const t = stagedFallbacks.get(e);
2407
+ if (t === undefined) return;
2408
+ stagedFallbacks.delete(e);
2409
+ for (const e of t) e.staged = n;
2410
+ const o = stagedFallbacks.get(n);
2411
+ if (o === undefined) stagedFallbacks.set(n, t); else o.push(...t);
2412
+ }
2413
+
2414
+ /** flushEnd: every open, closed, unheld navigation's (and interaction's) writes just committed. */ function trackFlushEnd() {
2415
+ // The drain's own record first: the interactions it settles below waited on it.
2416
+ const e = openFlush;
2417
+ if (e !== null) {
2418
+ openFlush = null;
2419
+ const n = {
2420
+ at: e.at,
2421
+ durationMs: now() - e.at,
2422
+ runs: e.runs,
2423
+ created: e.created,
2424
+ held: e.held
2425
+ };
2426
+ if (e.interaction != null) n.interaction = e.interaction;
2427
+ records.emit("flush", n, undefined);
2428
+ }
2429
+ drainSeq++;
2430
+ // The swaps this drain committed have rendered.
2431
+ displayFallbacks();
2432
+ // Landed first: they settled before anything this drain settles below.
2433
+ for (let e = 0; e < landedGraphs.length; e++) trackGraph(landedGraphs[e]);
2434
+ landedGraphs.length = 0;
2435
+ for (const e of openNavs) if (e.open === 0 && !e.held) settleNavigation(e, "committed");
2436
+ for (const e of openInteractions) maybeSettleInteraction(e);
2437
+ }
2438
+
2439
+ function settleNavigation(e, n, t, o) {
2440
+ if (!openNavs.has(e)) return;
2441
+ openNavs.delete(e);
2442
+ syncNavigation(e);
2443
+ const i = e.event;
2444
+ i.settledMs = now() - i.at;
2445
+ i.outcome = n;
2446
+ if (t !== undefined) i.hold = t;
2447
+ // The initial declaration is not a navigation the person waited on: its
2448
+ // `settledMs` is document start → router built, which would read as the
2449
+ // route's responsiveness in the feedback tables. Consumers get the record.
2450
+ if (i.initial !== true) for (const e of folds) e.navigation?.(i);
2451
+ records.emit("navigation", i, undefined);
2452
+ if (o) landedGraphs.push(e); else trackGraph(e);
2453
+ // The interaction that performed it may have been waiting only on this.
2454
+ const s = openInteractionOf(i.interaction);
2455
+ if (s !== undefined) maybeSettleInteraction(s);
2456
+ }
2457
+
2458
+ const interactionStates = new WeakMap;
2459
+
2460
+ /** `InteractionRef.event` → the record its frames join (while `joinable`). Weak: the Event is never held. */ const eventInteractions = new WeakMap;
2461
+
2462
+ /** Opened, not yet settled. */ const openInteractions = new Set;
2463
+
2464
+ let interactionLog = [];
2465
+
2466
+ function openInteraction(e, n, t) {
2467
+ const o = {
2468
+ name: e.name,
2469
+ at: e.at,
2470
+ handlerMs: 0,
2471
+ writes: 0,
2472
+ runs: 0,
2473
+ created: 0,
2474
+ runMs: 0,
2475
+ holds: [],
2476
+ navigations: [],
2477
+ origin: e
2478
+ };
2479
+ if (e.target !== undefined) o.target = e.target;
2480
+ // The runtime may date the frame from the event's own timestamp (before
2481
+ // any queued task ran); the gap to here is the input delay the browser's
2482
+ // INP counts first.
2483
+ if (n > o.at) o.inputDelayMs = n - o.at;
2484
+ const i = {
2485
+ event: o,
2486
+ open: true,
2487
+ opened: n,
2488
+ writeDrain: drainSeq,
2489
+ heldIn: new Set,
2490
+ held: false,
2491
+ excludedWrites: 0,
2492
+ awaiting: 0,
2493
+ actioned: false,
2494
+ joinable: t !== undefined
2495
+ };
2496
+ interactionStates.set(e, i);
2497
+ openInteractions.add(i);
2498
+ interactionLog.push(o);
2499
+ if (interactionLog.length > options.historyLimit) interactionLog.shift();
2500
+ if (t !== undefined) {
2501
+ eventInteractions.set(t, i);
2502
+ // A microtask is too early: native dispatch runs microtasks between
2503
+ // listeners. The callback must not close over `key`: a throttled timer
2504
+ // would hold the Event strongly long past its dispatch.
2505
+ setTimeout(() => {
2506
+ i.joinable = false;
2507
+ if (openInteractions.has(i)) maybeSettleInteraction(i, i.settleAt);
2508
+ });
2509
+ }
2510
+ }
2511
+
2512
+ /**
2513
+ * How long the record waits on a handler's returned promise before settling
2514
+ * without it: a promise that never settles (a hung request, a listener
2515
+ * awaiting an event that never comes) must not keep the record open forever.
2516
+ */ const ASYNC_HANDLER_CAP_MS = 1e4;
2517
+
2518
+ /** interactionEnd: the handler returned. */ function closeInteraction(e, n) {
2519
+ const t = interactionStates.get(e);
2520
+ if (t === undefined) return;
2521
+ t.open = false;
2522
+ const o = now();
2523
+ t.event.handlerMs = o - t.opened;
2524
+ // `async () => { await save(); set(…) }` returns a promise and continues
2525
+ // past the frame; the person's wait is that continuation. The record stays
2526
+ // open until it settles (or the cap), then judges whether anything on
2527
+ // screen could have shown the wait.
2528
+ const i = n !== null && (typeof n === "object" || typeof n === "function") && typeof n.then === "function" ? n : null;
2529
+ if (i !== null) {
2530
+ t.awaiting++;
2531
+ let e = false;
2532
+ let n;
2533
+ const settle = () => {
2534
+ if (e) return;
2535
+ e = true;
2536
+ if (n !== undefined) clearTimeout(n);
2537
+ if (!openInteractions.has(t) || --t.awaiting > 0) return;
2538
+ const o = now();
2539
+ // From the last handler's return: the frames of one event share a record.
2540
+ t.event.continuationMs = o - (t.opened + t.event.handlerMs);
2541
+ checkUntrackedAsyncHandler(t);
2542
+ maybeSettleInteraction(t, o);
2543
+ };
2544
+ n = setTimeout(settle, ASYNC_HANDLER_CAP_MS);
2545
+ // A pending cap must not hold a Node process (tests, SSR harnesses) open.
2546
+ n.unref?.();
2547
+ // Both branches: a rejected handler promise still ended the wait.
2548
+ i.then(settle, settle);
2549
+ return;
2550
+ }
2551
+ maybeSettleInteraction(t, o);
2552
+ }
2553
+
2554
+ /**
2555
+ * The handler awaited past its frame with nothing in the graph carrying the
2556
+ * wait: no root write before the `await` (a pending flag, an optimistic
2557
+ * value) and no action step (an action's steps stay attributed across
2558
+ * yields, and its holds are judged by SILENT_HOLD). From the person's side
2559
+ * the click did nothing for `continuationMs`; from the engine's side the
2560
+ * wait is invisible — no hold opened, so no hold could be acknowledged.
2561
+ * Thresholds are the hold thresholds: it is the same wait.
2562
+ */ function checkUntrackedAsyncHandler(e) {
2563
+ const n = options.holds;
2564
+ if (n === false) return;
2565
+ const t = e.event;
2566
+ const o = t.continuationMs;
2567
+ if (e.actioned || t.writes > 0 || o < n.infoMs) return;
2568
+ const i = formatOrigin(t.origin);
2569
+ const s = `[UNTRACKED_ASYNC_HANDLER] ${i}'s handler awaited ${o.toFixed(0)}ms past its frame with no ` + `write before the await: no hold opened, so nothing on screen could show the wait — the ` + `interaction was dead for ${o.toFixed(0)}ms. Make the async work an action() (its steps stay ` + `attributed across yields and its hold is judged), or write the pending state first: a ` + `createOptimistic(false) "saving" flag the UI reads.`;
2570
+ const r = o >= n.warnMs ? "warn" : "info";
2571
+ const a = {
2572
+ interaction: {
2573
+ type: t.name,
2574
+ target: t.target
2575
+ },
2576
+ continuationMs: o,
2577
+ capped: o >= ASYNC_HANDLER_CAP_MS
2578
+ };
2579
+ emitDiagnostic({
2580
+ code: "UNTRACKED_ASYNC_HANDLER",
2581
+ kind: "responsiveness",
2582
+ severity: r,
2583
+ message: s,
2584
+ data: a
2585
+ }, null);
2586
+ }
2587
+
2588
+ /** The open record a frame runs under, if any. */ function openInteractionOf(e) {
2589
+ const n = interactionOf(e);
2590
+ if (n === undefined) return undefined;
2591
+ const t = interactionStates.get(n);
2592
+ return t !== undefined && openInteractions.has(t) ? t : undefined;
2593
+ }
2594
+
2595
+ /** stampWrite: a root write stamped `origin`. */ function noteInteractionWrite(e, n) {
2596
+ const t = openInteractionOf(e);
2597
+ if (t === undefined) return;
2598
+ if (n) {
2599
+ t.excludedWrites++;
2600
+ return;
2601
+ }
2602
+ t.event.writes++;
2603
+ t.writeDrain = drainSeq;
2604
+ }
2605
+
2606
+ /** recordRerun / a create run: work attributed to the interaction. */ function noteInteractionRun(e, n, t) {
2607
+ const o = openInteractionOf(e);
2608
+ if (o === undefined) return;
2609
+ o.event[t ? "created" : "runs"]++;
2610
+ o.event.runMs += n;
2611
+ }
2612
+
2613
+ /** openNavigation: a navigation frame opened under the interaction. */ function noteInteractionNavigation(e) {
2614
+ const n = openInteractionOf(e.interaction);
2615
+ if (n !== undefined) n.event.navigations.push(e);
2616
+ }
2617
+
2618
+ /** holdStart: `t` parked writes — the interactions that performed them wait for `t`. */ function markInteractionsHeld(e) {
2619
+ if (openInteractions.size === 0) return;
2620
+ for (const n of e.Ye) {
2621
+ const t = openInteractionOf(n.ge?.origin);
2622
+ if (t !== undefined) {
2623
+ t.heldIn.add(e);
2624
+ t.held = true;
2625
+ }
2626
+ }
2627
+ }
2628
+
2629
+ /** transitionMerged: whoever waited for `outgoing` now waits for `target`. */ function mergeInteractionsHeld(e, n) {
2630
+ for (const t of openInteractions) if (t.heldIn.delete(n)) t.heldIn.add(e);
2631
+ }
2632
+
2633
+ /** transitionSettled: `t` committed — its holders' writes are through. */ function settleInteractionsHeld(e, n) {
2634
+ if (openInteractions.size === 0) return;
2635
+ for (const t of openInteractions) {
2636
+ if (!t.heldIn.delete(e)) continue;
2637
+ if (n !== undefined) t.event.holds.push(n);
2638
+ maybeSettleInteraction(t);
2639
+ }
2640
+ }
2641
+
2642
+ function maybeSettleInteraction(e, n = now()) {
2643
+ const t = e.event;
2644
+ if (e.open || e.awaiting > 0 || e.heldIn.size > 0) return;
2645
+ // A drain must have committed the last write (the handler's, or a redirect
2646
+ // hop's after the click's own drain) — the handler returning is not the
2647
+ // screen having it.
2648
+ if (t.writes > 0 && drainSeq <= e.writeDrain) return;
2649
+ for (const e of t.navigations) if (e.outcome === undefined) return;
2650
+ if (e.joinable) {
2651
+ e.settleAt = n;
2652
+ return;
2653
+ }
2654
+ openInteractions.delete(e);
2655
+ // Every write went to an excluded subject and nothing of the app's ran: the
2656
+ // click was on the observer's own UI (a devtools panel's button). Not a
2657
+ // fact about the app — forget it rather than report a dead interaction.
2658
+ if (t.writes === 0 && e.excludedWrites > 0 && t.runs === 0 && t.created === 0) {
2659
+ const e = interactionLog.indexOf(t);
2660
+ if (e !== -1) interactionLog.splice(e, 1);
2661
+ return;
2662
+ }
2663
+ t.settledMs = n - t.at;
2664
+ t.outcome = t.writes === 0 ? "idle" : e.held ? "held" : "committed";
2665
+ records.emit("interaction", t, undefined);
2666
+ }
2667
+
2668
+ /** The serializable face of a navigation origin for diagnostic `data`. */ function navigationData(e) {
2669
+ const n = {};
2670
+ if (e.name !== undefined) n.name = e.name;
2671
+ if (e.to !== undefined) n.to = e.to;
2672
+ if (e.from !== undefined) n.from = e.from;
2673
+ if (e.params !== undefined) n.params = e.params;
2674
+ return n;
2675
+ }
2676
+
2677
+ // The engine's implementation of the core's dev hook points. Installed by
2678
+ // enable(), uninstalled by disable() — while uninstalled the core pays one
2679
+ // null check per site and nothing else.
2680
+ let asyncStartSeq = 0;
2681
+
2682
+ let asyncStartTime = 0;
2683
+
2684
+ let asyncStartValue;
2685
+
2686
+ const engineHooks = {
2687
+ interactionStart: interactionStart,
2688
+ interactionEnd: interactionEnd,
2689
+ originStart: originStart,
2690
+ originEnd: originEnd,
2691
+ flushStart() {
2692
+ trackFlushStart();
2693
+ },
2694
+ flushEnd() {
2695
+ trackFlushEnd();
2696
+ },
2697
+ recomputeStart(e, n) {
2698
+ const t = n ? null : collectCauses(e);
2699
+ frames.push({
2700
+ start: now(),
2701
+ childMs: 0,
2702
+ causes: t,
2703
+ // The record's dep diff needs the deps as they were: captured here,
2704
+ // and only when the record will have an audience (see wantsRerun).
2705
+ prevDeps: n || !wantsRerun() ? null : captureDeps(e),
2706
+ // Mirror recompute's own prev-value resolution: an earlier run in the
2707
+ // same flush may still be holding in _pendingValue.
2708
+ prevValue: e.ue !== NOT_PENDING ? e.ue : e.le,
2709
+ // A create run inherits the interaction of whatever is building it: the
2710
+ // enclosing recompute (a parent's fn creating children) or, at the top
2711
+ // of the recompute stack, the effect callback / handler frame.
2712
+ interaction: t !== null ? interactionIn(t) : frames.length > 0 ? frames[frames.length - 1].interaction : enclosingInteraction()
2713
+ });
2714
+ },
2715
+ derivedChanged(e) {
2716
+ const n = frames[frames.length - 1];
2717
+ stampDerived(e, n !== undefined && n.causes !== null ? n.causes : []);
2718
+ },
2719
+ recomputeEnd(e, n, t, o, i, s) {
2720
+ const r = frames.pop();
2721
+ // enable() can land mid-recompute: no opening frame, nothing to report.
2722
+ if (r === undefined) return;
2723
+ const a = now() - r.start;
2724
+ if (frames.length > 0) frames[frames.length - 1].childMs += a;
2725
+ const c = Math.max(0, a - r.childMs);
2726
+ // Effect-output honesty: effects run with `_equals: false`, so core
2727
+ // reports EVERY effect recompute as changed — which made effect waste
2728
+ // invisible to costs() (and compiled JSX bindings are effects: the
2729
+ // fan-out waste a naive selected-row produces is all effects). The
2730
+ // engine re-derives the fact from its own snapshot: an identical
2731
+ // committed compute output is an unchanged run. `undefined` outputs are
2732
+ // exempt — a side-effect-only compute's work IS its effect phase, and
2733
+ // identity of `undefined` proves nothing.
2734
+ if (t && r.causes !== null && e.i) {
2735
+ const n = e.ue !== NOT_PENDING ? e.ue : e.le;
2736
+ if (n !== undefined && n === r.prevValue) t = false;
2737
+ }
2738
+ // Unstable-output check: memos only, non-create, plain runs with a
2739
+ // committed change. The fresh value sits in `_pendingValue` for held
2740
+ // plain-flush memo commits and in `_value` for direct ones. Overlay runs
2741
+ // are excluded — an optimistic re-derive legitimately produces fresh
2742
+ // equivalents while the lane settles.
2743
+ if (r.causes !== null && t && !o && !i && !e.i) checkUnstableOutput(e, r.prevValue, e.ue !== NOT_PENDING ? e.ue : e.le);
2744
+ // The same exemption for memos: a run that committed `undefined` did its
2745
+ // work as a side effect (a projection's draft writes or reconcile, a
2746
+ // write from inside a memo), so core's `undefined === undefined` gate
2747
+ // proves nothing. Reported as changed, so no consumer counts it as waste.
2748
+ if (!t && (e.ue !== NOT_PENDING ? e.ue : e.le) === undefined) t = true;
2749
+ if (r.causes !== null) recordRerun(e, r, {
2750
+ selfMs: c,
2751
+ totalMs: a
2752
+ }, t, o ? "optimistic" : i ? "held" : "plain", s); else if (!excludedNode(e)) {
2753
+ // Creation runs still get the wide-scope check: a memo can be born with
2754
+ // its coarse-read problem already in place — and their time is charged
2755
+ // to the interaction building them (the interaction record's `created`).
2756
+ checkDepWidth(e);
2757
+ noteInteractionRun(r.interaction, c, true);
2758
+ // The first effect callback runs with no re-run record to inherit from;
2759
+ // hand it the interaction that built the node (see effectRunStart).
2760
+ e.Me = r.interaction;
2761
+ if (openFlush !== null) noteFlushRun(openFlush, true, r.interaction);
2762
+ if (records.observed("create")) {
2763
+ const n = {
2764
+ at: r.start,
2765
+ nodeKind: nodeKind(e),
2766
+ nodeName: nodeName(e),
2767
+ nodeId: devId(e),
2768
+ depCount: captureDeps(e).length,
2769
+ selfMs: c,
2770
+ totalMs: a,
2771
+ phase: o ? "optimistic" : i ? "held" : "plain",
2772
+ held: s
2773
+ };
2774
+ if (r.interaction !== undefined) n.interaction = r.interaction;
2775
+ records.emit("create", n, e);
2776
+ }
2777
+ }
2778
+ markSeen(e);
2779
+ },
2780
+ write(e, n, t) {
2781
+ stampWrite(e, "write", n, t);
2782
+ },
2783
+ refreshed(e) {
2784
+ stampWrite(e, "refresh");
2785
+ },
2786
+ flightStart(e, n) {
2787
+ trackFlightStart(e, n);
2788
+ },
2789
+ asyncStart(e) {
2790
+ asyncStartSeq = e.ge?.seq ?? 0;
2791
+ asyncStartTime = e.ie;
2792
+ asyncStartValue = e.le;
2793
+ },
2794
+ asyncEnd(e, n, t, o) {
2795
+ if (o) {
2796
+ // Core calls this unconditionally (hook calls cannot live inside its
2797
+ // try blocks — see attribution-hooks.ts), so committed-ness is detected
2798
+ // here against the asyncStart snapshot: a direct commit moves `_value`
2799
+ // (or `_time`, for a same-reference commit under `equals: false`), and
2800
+ // a transition hold parks the value in `_pendingValue`. A landing the
2801
+ // equality gate swallowed moves none of them and must leave no stamp.
2802
+ const o = e.le !== asyncStartValue || e.ie !== asyncStartTime || e.ue === t;
2803
+ if (o) stampWrite(e, "async", n === undefined ? NO_VALUES : n, t);
2804
+ // Flight over either way — an equality-swallowed landing still spent
2805
+ // the wall time (finalizeFlight only chains through a fresh stamp).
2806
+ finalizeFlight(e);
2807
+ return;
2808
+ }
2809
+ // Landed through setSignal: reclassify its "write" stamp as an async
2810
+ // landing — but only if it actually stamped (the value changed) since
2811
+ // asyncStart; a no-change landing must leave no fresh stamp behind.
2812
+ const i = e.ge;
2813
+ if (i !== undefined && i.seq > asyncStartSeq && i.kind === "write") stampWrite(e, "async", NO_VALUES, t);
2814
+ finalizeFlight(e);
2815
+ },
2816
+ effectRunStart(e) {
2817
+ pushFrame("effect", nodeName(e), e.Me, e);
2818
+ // The frame's own `at` doubles as the record's start: set only while
2819
+ // listened, so a listener arriving mid-callback finds no start and the
2820
+ // end emits nothing for it — and an unlistened callback pays no clock read.
2821
+ if (records.observed("effect")) originFrames[originFrames.length - 1].at = now();
2822
+ },
2823
+ effectRunEnd(e) {
2824
+ const n = originFrames[originFrames.length - 1];
2825
+ if (n !== undefined && n.kind === "effect" && n.at !== undefined) {
2826
+ if (!excludedNode(e)) {
2827
+ const t = {
2828
+ at: n.at,
2829
+ durationMs: now() - n.at,
2830
+ nodeId: devId(e),
2831
+ nodeName: nodeName(e)
2832
+ };
2833
+ if (n.run !== undefined) t.run = n.run;
2834
+ if (n.interaction !== undefined) t.interaction = n.interaction;
2835
+ records.emit("effect", t, e);
2836
+ }
2837
+ }
2838
+ popFrame("effect");
2839
+ if (activeHold !== null) activeHold.painted++;
2840
+ },
2841
+ actionStepStart(e, n) {
2842
+ // Steps after a yield resume from a promise callback with no ambient
2843
+ // interaction; the one that started the action (its first step) is the
2844
+ // action's interaction for every step.
2845
+ let t = actionInteractions.get(e);
2846
+ if (t === undefined && !actionInteractions.has(e)) {
2847
+ t = currentInteraction ?? undefined;
2848
+ actionInteractions.set(e, t);
2849
+ // The handler's async work is an action's: tracked across yields.
2850
+ const n = t !== undefined ? interactionStates.get(t) : undefined;
2851
+ if (n !== undefined) n.actioned = true;
2852
+ }
2853
+ pushFrame("action", n, t);
2854
+ },
2855
+ actionStepEnd() {
2856
+ popFrame("action");
2857
+ },
2858
+ holdStart(e) {
2859
+ if (openFlush !== null) openFlush.held = true;
2860
+ trackHoldStart(e);
2861
+ },
2862
+ holdEnd() {
2863
+ activeHold = null;
2864
+ },
2865
+ transitionSettled(e) {
2866
+ trackHoldSettled(e);
2867
+ rebaseFallbacks(e, DRAIN);
2868
+ },
2869
+ transitionMerged(e, n) {
2870
+ trackHoldMerge(e, n);
2871
+ rebaseFallbacks(n, e);
2872
+ },
2873
+ storeReplaced(e, n, t, o, i, s) {
2874
+ checkImmutableUpdate(e, n, t, o, i, s);
2875
+ },
2876
+ listChurn(e, n, t, o, i) {
2877
+ checkListIdentity(e, n, t, o, i);
2878
+ },
2879
+ boundaryFallback(e, n, t, o) {
2880
+ // The folds hear the show at its display (see trackFallback), not here.
2881
+ trackFallback(e, n, t, o ?? null);
2882
+ },
2883
+ optimisticReverted(e, n, t, o) {
2884
+ checkOptimisticRevert(e, n, t, o);
2885
+ },
2886
+ currentOrigin() {
2887
+ return ambientOrigin();
2888
+ }
2889
+ };
2890
+
2891
+ const holds = [];
2892
+
2893
+ /**
2894
+ * The windows: what a consumer reads back — the ring buffers, the once-only
2895
+ * warning memories (so a new window can warn again), the hot-cause windows,
2896
+ * the folds' tables. Reset by every `enable()` (each opens a fresh window)
2897
+ * and by the last `disable()`. Never touches the live tracking state —
2898
+ * open frames, open interactions and navigations, the active hold,
2899
+ * `drainSeq` — which belongs to whoever is mid-flight when a second
2900
+ * consumer arrives.
2901
+ */ function resetWindows() {
2902
+ history = [];
2903
+ waterfallLog = [];
2904
+ holdLog = [];
2905
+ navigationLog = [];
2906
+ interactionLog = [];
2907
+ reportedCycles.clear();
2908
+ relays.clear();
2909
+ immutableReported.clear();
2910
+ hotCauses.clear();
2911
+ for (const e of folds) e.reset?.();
2912
+ }
2913
+
2914
+ /** The live tracking state — reset only when the engine is (un)installed. */ function resetTracking() {
2915
+ frames.length = 0;
2916
+ activeHold = null;
2917
+ openFlush = null;
2918
+ stagedFallbacks.delete(DRAIN);
2919
+ trackingGen++;
2920
+ openNavs.clear();
2921
+ landedGraphs.length = 0;
2922
+ openInteractions.clear();
2923
+ routeCounts.clear();
2924
+ visitRoute = undefined;
2925
+ drainSeq = 0;
2926
+ originFrames.length = 0;
2927
+ effectStack.length = 0;
2928
+ interactionStack.length = 0;
2929
+ currentInteraction = null;
2930
+ }
2931
+
2932
+ /**
2933
+ * One hold's request in full: the defaults for what it left unsaid (an
2934
+ * explicit `undefined` is unsaid), and `checks: false` folding its five cost
2935
+ * checks off before anyone else's request is considered.
2936
+ */ function resolveHold(e) {
2937
+ const n = {
2938
+ ...defaultOptions
2939
+ };
2940
+ if (e !== undefined) {
2941
+ for (const t of Object.keys(e)) {
2942
+ const o = e[t];
2943
+ if (o !== undefined) n[t] = o;
2944
+ }
2945
+ }
2946
+ if (!n.checks) {
2947
+ n.hotRuns = false;
2948
+ n.hotTime = false;
2949
+ n.wideDeps = false;
2950
+ n.unstableMemos = false;
2951
+ n.fanOut = false;
2952
+ n.wastedRecompute = false;
2953
+ }
2954
+ return n;
2955
+ }
2956
+
2957
+ /** `values` levels by how much they carry: the merge takes the lowest. */ const VALUES_RANK = {
2958
+ none: 0,
2959
+ labels: 1,
2960
+ full: 2
2961
+ };
2962
+
2963
+ /**
2964
+ * The more demanding of two settings for one key: `true` over `false`, the
2965
+ * larger `historyLimit`, a threshold config over `false`, and between two
2966
+ * configs the field values that fire sooner — the lower count, budget or
2967
+ * millisecond bound, the longer `windowMs`. For `values` "more demanding"
2968
+ * is LESS data: the level that carries the least wins, so a holder that
2969
+ * must not see user data is never overruled by one that wants it.
2970
+ */ function demanding(e, n, t) {
2971
+ if (e === "values") return VALUES_RANK[n] <= VALUES_RANK[t] ? n : t;
2972
+ if (typeof n === "boolean") return n || t;
2973
+ if (e === "historyLimit") return Math.max(n, t);
2974
+ if (n === false) return t;
2975
+ if (t === false) return n;
2976
+ if (typeof n === "number") return Math.min(n, t);
2977
+ const o = {};
2978
+ for (const e in n) {
2979
+ const i = n[e];
2980
+ const s = t[e];
2981
+ o[e] = e === "windowMs" ? Math.max(i, s) : Math.min(i, s);
2982
+ }
2983
+ return o;
2984
+ }
2985
+
2986
+ /**
2987
+ * Recompute the options in effect: each live hold's request resolved, then
2988
+ * combined per key by the most demanding value — the one that has the
2989
+ * engine observe more, report more or keep more. A hold says what it wants
2990
+ * and can only add to what another hold asked for, never take it away, so
2991
+ * the result does not depend on the order the holds were taken: the console
2992
+ * log prints while any holder wants it, a check runs while any holder wants
2993
+ * it and at the most sensitive threshold anyone asked for, the ring buffer
2994
+ * is the largest requested, and records carry the least user data any
2995
+ * holder allows (`values`). With no hold outstanding the defaults stand.
2996
+ */ function applyOptions() {
2997
+ if (holds.length === 0) {
2998
+ options = {
2999
+ ...defaultOptions
3000
+ };
3001
+ return;
3002
+ }
3003
+ const e = resolveHold(holds[0].opts);
3004
+ for (let n = 1; n < holds.length; n++) {
3005
+ const t = resolveHold(holds[n].opts);
3006
+ for (const n in e) e[n] = demanding(n, e[n], t[n]);
3007
+ }
3008
+ options = e;
3009
+ }
3010
+
3011
+ /** The last hold is gone (or `disable()` was called): uninstall and clear everything. */ function uninstall() {
3012
+ holds.length = 0;
3013
+ applyOptions();
3014
+ attributionActive = false;
3015
+ resetWindows();
3016
+ resetTracking();
3017
+ setAttributionHooks(null);
3018
+ }
3019
+
3020
+ const attribution = {
3021
+ enable(e) {
3022
+ const n = {
3023
+ opts: e
3024
+ };
3025
+ holds.push(n);
3026
+ applyOptions();
3027
+ resetWindows();
3028
+ if (!attributionActive) {
3029
+ attributionActive = true;
3030
+ resetTracking();
3031
+ setAttributionHooks(engineHooks);
3032
+ }
3033
+ return () => {
3034
+ const e = holds.indexOf(n);
3035
+ if (e === -1) return;
3036
+ holds.splice(e, 1);
3037
+ if (holds.length === 0) uninstall(); else applyOptions();
3038
+ };
3039
+ },
3040
+ disable() {
3041
+ uninstall();
3042
+ },
3043
+ history(e) {
3044
+ let n;
3045
+ switch (e) {
3046
+ case "rerun":
3047
+ n = history;
3048
+ break;
3049
+
3050
+ case "waterfall":
3051
+ n = waterfallLog;
3052
+ break;
3053
+
3054
+ case "hold":
3055
+ n = holdLog;
3056
+ break;
3057
+
3058
+ case "navigation":
3059
+ n = navigationLog;
3060
+ break;
3061
+
3062
+ case "interaction":
3063
+ n = interactionLog;
3064
+ break;
3065
+
3066
+ default:
3067
+ throw new Error(`attribution.history: unknown record type "${String(e)}"`);
3068
+ }
3069
+ // The buffers are typed by their variable; the switch is the proof.
3070
+ return n;
3071
+ },
3072
+ markFlight(e, n) {
3073
+ // Earliest wins (the slot's rule): re-marking (a cache re-serving the
3074
+ // same promise) must not move the origin later.
3075
+ OBSERVE.attribution.markFlight(e, n);
3076
+ }
3077
+ };
3078
+
3079
+ export { FALLBACK_FLASH_MS, attribution, formatOrigin, formatRerun, graphSize, nodeIdOf, nodeName, now, registerFold, rootsOf };