@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
@@ -1,6 +1,7 @@
1
- import type { Computed } from "./types.js";
1
+ import type { Computed, Signal } from "./types.js";
2
2
  /**
3
- * Dev-mode "why did this run" attribution.
3
+ * "Why did this run" attribution — the engine behind
4
+ * `@solidjs/signals/attribution`.
4
5
  *
5
6
  * The runtime already knows the full dependency set of every scope; this
6
7
  * module surfaces it. Every value commit stamps its node with a ChangeRecord
@@ -15,34 +16,111 @@ import type { Computed } from "./types.js";
15
16
  *
16
17
  * This module is the attribution ENGINE: all semantics live here, and it is
17
18
  * decoupled from the core. `enable()` installs it into the core's narrow
18
- * dev-only hook points (attribution-hooks.ts); core's only obligation is to
19
- * call those hooks with true facts. Disabled cost is one null check per hook
20
- * site; prod builds fold the sites out entirely. The same hook surface is the
21
- * intended substrate for external consumers (devtools) — one mechanism, two
22
- * front-ends.
19
+ * observe-tier hook points (attribution-hooks.ts); core's only obligation is
20
+ * to call those hooks with true facts. Disabled cost is one null check per
21
+ * hook site; prod builds fold the sites out entirely. Nothing in the core
22
+ * imports this module — it is reachable only through the package's
23
+ * `./attribution` entry, so an observe build that never imports it never
24
+ * ships it. The same hook surface is the intended substrate for external
25
+ * consumers (devtools) — one mechanism, two front-ends.
23
26
  */
24
27
  export type ChangeKind = "write" | "derived" | "async" | "refresh";
28
+ /**
29
+ * Provenance of a root change: the imperative frame that performed it.
30
+ *
31
+ * - `interaction` — a user event handler (the web runtime marks dispatch via
32
+ * `withInteraction`). `name` is the event type, `target` the element hit
33
+ * (`button#next "Next →"` — the quoted text as `AttributionOptions.values`
34
+ * allows), `at` the dispatch time on the `performance.now()` clock — the
35
+ * base every feedback-latency number is measured from.
36
+ * - `effect` — an effect callback (`name` = the effect's name; `run` = the
37
+ * compute run whose effect phase performed the write, when that run was
38
+ * recorded — so a write can be joined to the re-run that produced it).
39
+ * - `action` — a step of an `action()` generator (`name` = the generator's
40
+ * name, when it has one). Writes after an `await` (not a `yield`) run in a
41
+ * bare microtask and stamp `external` — the documented escape.
42
+ * - `async` — an async landing (`name` = the node whose flight landed).
43
+ * - `navigation` — a router's navigation, declared via `withOrigin` around
44
+ * the location write (`name` = the matched route pattern `/users/:id`;
45
+ * `to`/`from` the concrete paths; `params` what the pattern bound; `at`
46
+ * when it was requested). The router-agnostic seam: any router that wraps
47
+ * its write gets navigations named by route in every hold, re-run and
48
+ * verdict, with no per-router knowledge anywhere in the engine.
49
+ * - `external` — none of the above: timers, sockets, promise callbacks, setup.
50
+ *
51
+ * `interaction` on a non-interaction frame is the user event the frame runs
52
+ * under — an action started by a click, an effect whose run was caused by a
53
+ * click's write, a landing whose flight a click started, a navigation a link
54
+ * click performed. It is what lets every downstream cost be keyed by the
55
+ * interaction that paid for it.
56
+ */
57
+ export interface ChangeOrigin {
58
+ kind: "interaction" | "effect" | "action" | "async" | "navigation" | "external";
59
+ name?: string;
60
+ target?: string;
61
+ /**
62
+ * When the frame opened (`performance.now()` clock). Always set for
63
+ * `interaction` and `navigation`; set on an `effect` frame only while an
64
+ * `effect` record listener exists (the callback's timed start).
65
+ */
66
+ at?: number;
67
+ interaction?: ChangeOrigin;
68
+ /** `effect` only: the `RerunEvent.run` of the compute run this callback belongs to. */
69
+ run?: number;
70
+ /** `navigation` only: concrete destination and departure paths, and the bound params. */
71
+ to?: string;
72
+ from?: string;
73
+ params?: Readonly<Record<string, string | undefined>>;
74
+ }
25
75
  export interface ChangeRecord {
26
76
  /** Global monotonic change sequence — orders causes across the app. */
27
77
  seq: number;
28
78
  kind: ChangeKind;
29
79
  name: string;
30
- /** Short previews of the value transition (writes only). */
80
+ /**
81
+ * Identity of the node that changed — the signal written, the memo whose
82
+ * value changed — in the same id space as `RerunEvent.nodeId`, so a
83
+ * derived cause joins the run that produced it and repeated writes to
84
+ * one signal join each other where `name` alone would merge every
85
+ * unnamed `signal`. Stamped on every record the engine makes; optional
86
+ * for a record built elsewhere (a `HeldWrite`, a deserialized artifact).
87
+ */
88
+ nodeId?: number;
89
+ /**
90
+ * Short previews of the value transition (writes only) — present under
91
+ * `AttributionOptions.values: "full"`, never carried otherwise.
92
+ */
31
93
  prev?: string;
32
94
  value?: string;
33
95
  /** First user frames of the triggering write's stack (opt-in). */
34
96
  stack?: string[];
35
97
  /** For derived changes: the upstream changes that produced this one. */
36
98
  causes?: ChangeRecord[];
99
+ /** Root changes only: who performed the write. */
100
+ origin?: ChangeOrigin;
101
+ /** Root changes only: when the write was stamped (`performance.now()` clock). */
102
+ at?: number;
37
103
  }
38
104
  export interface RerunEvent {
39
105
  /** Global monotonic run sequence. */
40
106
  run: number;
107
+ /** When the run started (`performance.now()` clock). */
108
+ at: number;
41
109
  /** How many times this node has re-run since attribution was enabled. */
42
110
  nodeRuns: number;
43
111
  nodeKind: "effect" | "memo";
44
112
  nodeName: string;
45
- node: Computed<any>;
113
+ /**
114
+ * Identity of the scope that ran, stable for the node's lifetime within
115
+ * the process: every run of one memo/effect carries the same `nodeId`, so
116
+ * runs join to a scope after the record has left the process (where
117
+ * `nodeName` alone would merge every unnamed `effect`). The engine's own
118
+ * per-node id, also what `ChangeOrigin.run` and the cycle/relay checks
119
+ * key on; not meaningful across processes or sessions. In-process
120
+ * consumers get the live node as the `live` argument beside the record
121
+ * (`OBSERVE.records.subscribe("rerun", (event, node) => …)`).
122
+ */
123
+ nodeId: number;
46
124
  /**
47
125
  * The deps that changed since this node's previous run. Empty means the
48
126
  * re-run was not triggered by a tracked value change (creation-adjacent
@@ -60,35 +138,246 @@ export interface RerunEvent {
60
138
  /** Wall time of this run including nested recomputes (ms). */
61
139
  totalMs: number;
62
140
  /**
63
- * Whether the run committed a changed value. A PLAIN memo run with
141
+ * Whether the run produced a changed value. A PLAIN memo run with
64
142
  * `changed: false` was pure waste — the equality cutoff stopped it from
65
- * notifying anyone; an effect run with `changed: false` computed without
66
- * firing its effect phase. Summed as `wastedMs` in costs() (plain,
67
- * non-held runs only — see `phase`).
143
+ * notifying anyone. Effects run with `_equals: false` in core (their
144
+ * effect phase re-fires on every recompute), so the engine derives this
145
+ * fact itself: an effect run whose compute output is identical to the
146
+ * previous run's reports `changed: false` — the phase re-fired with the
147
+ * same input, pure waste. Side-effect-only computes (`undefined` output —
148
+ * effects, projections, memos that work by writing) report `true`:
149
+ * identity of `undefined` proves nothing about their work.
150
+ * Summed as `wastedMs` in costs() (plain, non-held runs only — see
151
+ * `phase`).
68
152
  */
69
153
  changed: boolean;
70
154
  /**
71
155
  * Which posture this run executed under. "optimistic" = under an
72
- * optimistic lane (overlay recompute); "transition" = a transition was
73
- * active or owns the node (the run may be replayed/settled later);
74
- * "plain" = an ordinary committed run. Overlay runs are real work (they
75
- * count toward time budgets) but are never blamed as waste, and costs()
76
- * reports their time separately as `overlayMs`.
156
+ * optimistic lane (overlay recompute); "held" = a hold was open or owns
157
+ * the node (the run may be replayed/settled later); "plain" = an ordinary
158
+ * committed run. Overlay runs are real work (they count toward time
159
+ * budgets) but are never blamed as waste, and costs() reports their time
160
+ * separately as `overlayMs`.
77
161
  */
78
- phase: "plain" | "transition" | "optimistic";
162
+ phase: "plain" | "held" | "optimistic";
79
163
  /**
80
- * The changed value was held in `_pendingValue` (a transition hold) rather
81
- * than committed directly; its reveal happens on the transition's own
82
- * schedule. Held runs are excluded from waste accounting.
164
+ * The changed value was parked in `_pendingValue` (held) rather than
165
+ * committed directly; its reveal happens on the hold's own schedule. Held
166
+ * runs are excluded from waste accounting.
83
167
  */
84
168
  held: boolean;
169
+ /** The user interaction this run traces back to through its causes, if any. */
170
+ interaction?: ChangeOrigin;
171
+ }
172
+ /**
173
+ * A computation's creation run — the first run, the one with no causes to
174
+ * explain (a `RerunEvent` is every run after it). The same measurements as a
175
+ * re-run, less the causal fields a first run cannot have, and `interaction`
176
+ * inherited from whatever built the node: the enclosing recompute (a
177
+ * parent's fn creating children) or the handler/effect frame at the top of
178
+ * the stack. Creation time is already charged to the interaction record's
179
+ * `created`; this is the per-node face of that sum.
180
+ */
181
+ export interface CreateEvent {
182
+ /** When the run started (`performance.now()` clock). */
183
+ at: number;
184
+ nodeKind: "effect" | "memo";
185
+ nodeName: string;
186
+ /** Same id space as `RerunEvent.nodeId` — the node's later re-runs join here. */
187
+ nodeId: number;
188
+ /** Dependency count after this run. */
189
+ depCount: number;
190
+ /** Wall time of this run excluding nested recomputes (ms) — children created inside report their own. */
191
+ selfMs: number;
192
+ /** Wall time of this run including nested recomputes (ms). */
193
+ totalMs: number;
194
+ /** Posture the run executed under — see `RerunEvent.phase`. */
195
+ phase: "plain" | "held" | "optimistic";
196
+ /** The value was parked in `_pendingValue` rather than committed — see `RerunEvent.held`. */
197
+ held: boolean;
198
+ /** The interaction whose handler or flush built this node, if any. */
199
+ interaction?: ChangeOrigin;
200
+ }
201
+ /**
202
+ * One run of an effect's imperative half — the callback that touches the DOM
203
+ * or the outside world — timed from entry to exit, cleanup included, whether
204
+ * or not it threw. The compute half is the `RerunEvent`/`CreateEvent` with
205
+ * the same `nodeId` that preceded it in the flush; `run` joins the two for a
206
+ * re-run and is absent for a creation's first callback.
207
+ */
208
+ export interface EffectRunEvent {
209
+ /** When the callback started (`performance.now()` clock). */
210
+ at: number;
211
+ /** Wall time of the callback, nested runs included (ms). */
212
+ durationMs: number;
213
+ nodeId: number;
214
+ nodeName: string;
215
+ /** `RerunEvent.run` of the compute run whose effect phase this is; absent for a creation. */
216
+ run?: number;
217
+ /** The interaction the preceding compute run traced to, if any. */
218
+ interaction?: ChangeOrigin;
219
+ }
220
+ /**
221
+ * One `flush()` drain: from the scheduler picking up scheduled work until
222
+ * every batch it processed has committed (effects ran) or been parked in a
223
+ * held transition. The unit React's "Scheduler" track paints; the engine's
224
+ * `flushEnd` settles interactions and navigations on the same instant.
225
+ * Counts cover the runs the engine recorded inside the drain (excluded
226
+ * scopes not counted).
227
+ */
228
+ export interface FlushEvent {
229
+ /** When the drain started (`performance.now()` clock). */
230
+ at: number;
231
+ /** Wall time of the drain (ms). */
232
+ durationMs: number;
233
+ /** Re-runs recorded during the drain. */
234
+ runs: number;
235
+ /** Creation runs during the drain. */
236
+ created: number;
237
+ /** A transition was judged incomplete during the drain — some of its writes stayed staged. */
238
+ held: boolean;
239
+ /**
240
+ * The interaction every recorded run of the drain traced to, when there
241
+ * was exactly one; absent when none did, or when runs for several
242
+ * interactions shared the drain.
243
+ */
244
+ interaction?: ChangeOrigin;
245
+ }
246
+ /**
247
+ * One async flight — a promise or async iterable an async computation
248
+ * registered — from its origin (the earliest the engine knows: a
249
+ * `markFlight` preload mark, or first sight at registration) to the moment
250
+ * it landed or was superseded by the node's next flight (`abandoned` — the
251
+ * answer will be discarded). A flight whose node is disposed mid-air
252
+ * produces no record.
253
+ */
254
+ export interface FlightEvent {
255
+ nodeId: number;
256
+ nodeName: string;
257
+ /** Owner-chain labels of the async node, root first, when the node is owned. */
258
+ ownerPath?: string[];
259
+ /** When the flight started (`performance.now()` clock). */
260
+ at: number;
261
+ /** Wall time in the air (ms). */
262
+ durationMs: number;
263
+ outcome: "landed" | "abandoned";
264
+ /** The interaction whose write started the flight, if any. */
265
+ interaction?: ChangeOrigin;
85
266
  }
267
+ /**
268
+ * One showing of a loading boundary's fallback, delivered when it stops
269
+ * showing. `ownerPath` names the boundary by its subtree's owner chain
270
+ * (`["<App>", "<Feed>"]`); absent when the subtree never reported a path.
271
+ * The fold in `feedback().fallbacks` is this record summed per boundary,
272
+ * with the flash verdict.
273
+ */
274
+ export interface FallbackEvent {
275
+ ownerPath?: string[];
276
+ /** When the fallback appeared (`performance.now()` clock). */
277
+ at: number;
278
+ /** How long it stayed (ms). */
279
+ shownMs: number;
280
+ /** The interaction whose work the boundary was waiting on, if the engine could tell. */
281
+ interaction?: ChangeOrigin;
282
+ }
283
+ /**
284
+ * The live graph's size at a navigation's settle — the moment an app has
285
+ * finished moving between two screens, so a count that climbs visit after
286
+ * visit is a root or a subscription the previous screen left behind.
287
+ * Delivered on `OBSERVE.records.subscribe("graph", …)` per settled
288
+ * navigation; a walk of the owner tree from the registered top-level roots,
289
+ * made only when something listens or `graphGrowth` is on.
290
+ */
291
+ export interface GraphEvent extends GraphSize {
292
+ /** When the navigation settled (`performance.now()` clock). */
293
+ at: number;
294
+ /** The route pattern the navigation matched (`name`), else its `to`. */
295
+ route?: string;
296
+ /** The navigation this count belongs to. */
297
+ navigation: NavigationEvent;
298
+ }
299
+ /**
300
+ * The live reactive graph's size, as `graphSize()` counts it: the owner tree
301
+ * from the registered top-level roots, then every computation and signal
302
+ * reachable from it through dependencies and subscriptions — which is how
303
+ * an ownerless effect (created with no owner, kept alive by its sources)
304
+ * is found — and the edges between them.
305
+ */
306
+ export interface GraphSize {
307
+ /** Top-level roots alive (`render()`'s, module-scope `createRoot()`s, panels). */
308
+ roots: number;
309
+ /** Owners in the tree: roots, component owners, owned computations. */
310
+ owners: number;
311
+ /** Computations (memos, effects, boundaries), owned or reached through a subscription. */
312
+ computations: number;
313
+ /** Signals reached through a computation's dependencies. */
314
+ signals: number;
315
+ /** Dependency links — each computation's sources, counted once. */
316
+ edges: number;
317
+ }
318
+ /**
319
+ * What user data the engine's records carry — see `AttributionOptions.values`.
320
+ * Ordered: `"full"` carries the most, `"none"` the least.
321
+ */
322
+ export type AttributionValues = "full" | "labels" | "none";
86
323
  export interface AttributionOptions {
87
324
  /** Pretty-print each re-run to the console (default true). */
88
325
  log?: boolean;
326
+ /**
327
+ * What user data records carry. Default per build tier: `"full"` in dev
328
+ * builds, `"none"` in observe builds — a production observability
329
+ * artifact carries no user data unless a holder asks for it. Records name things —
330
+ * owner paths, `name` options, store paths, route patterns — and are
331
+ * otherwise numbers, kinds and outcomes; this option governs the fields
332
+ * that quote application data: the value previews on a root write
333
+ * (`ChangeRecord.prev`/`value`, and the `HeldWrite.prev`/`value` copied
334
+ * from them), the text of the element an interaction hit
335
+ * (`ChangeOrigin.target` / `InteractionEvent.target` — the `"Next →"` in
336
+ * `button#next "Next →"`), and every sentence built from those (the
337
+ * console log, `formatRerun`, `formatOrigin`, the SILENT_HOLD / LONG_HOLD
338
+ * / STACKED_HOLDS verdicts naming the interaction, OPTIMISTIC_REVERTED's
339
+ * shown and settled values). Applied where the record is BUILT, so
340
+ * nothing downstream — a ring buffer, a fold, a listener, an exporter —
341
+ * ever holds what the level excludes.
342
+ *
343
+ * - `"full"` — previews of the written values (strings quoted and cut at
344
+ * 40 characters, numbers and booleans verbatim, everything else a type
345
+ * tag such as `Array(12)`) and the element's text: what makes dev
346
+ * output readable.
347
+ * - `"labels"` — no value previews; element text kept only on a `button`
348
+ * or an `a` (the control's caption — what the person pressed is the
349
+ * point of an interaction record) and dropped for anything else (the
350
+ * text of a `div` or a `td` is content).
351
+ * - `"none"` — no value previews, no element text: names, numbers, kinds
352
+ * and outcomes only. What an observe-tier holder that carries records
353
+ * out of the process (an APM adapter) should pass.
354
+ *
355
+ * Across holds the LEAST permissive level wins — the one key that
356
+ * combines the other way from the rest, where a hold can only ask for
357
+ * more: a holder that must not see user data is not overruled by one
358
+ * that wants it. A holder that names no level asks for the tier's
359
+ * default, so in an observe build it tightens to `"none"` beside anyone;
360
+ * a single holder passing `"full"` there gets `"full"`, and an explicit
361
+ * `"full"` never loosens what another holder demanded. Governs what is
362
+ * built from the moment the level is in effect; records already in a
363
+ * ring buffer keep what they carried. A
364
+ * navigation's concrete paths and params (`ChangeOrigin.to`/`from`/
365
+ * `params`) are the router's description and are not governed here.
366
+ */
367
+ values?: AttributionValues;
368
+ /**
369
+ * Run the cost checks — the thresholded findings over the engine's own
370
+ * accounting: `hotRuns`, `hotTime`, `wideDeps`, `unstableMemos`,
371
+ * `fanOut`, `wastedRecompute` (default true). `false` turns all six off at once, whatever
372
+ * their individual settings, so a consumer that wants records only (an
373
+ * exporter, a profiler track) pays for none of their per-node bookkeeping.
374
+ * Hold, long-hold and waterfall tracking are records with verdicts on top,
375
+ * not checks, and are unaffected; disable those through their own options.
376
+ */
377
+ checks?: boolean;
89
378
  /** Capture the user stack frame of each write — slow (default false). */
90
379
  stacks?: boolean;
91
- /** Ring-buffer size for `history()` (default 200). */
380
+ /** Ring-buffer size for each `history(type)` buffer (default 200). */
92
381
  historyLimit?: number;
93
382
  /**
94
383
  * Hot-scope warning: emit a diagnostic when one scope re-runs `count`
@@ -102,8 +391,11 @@ export interface AttributionOptions {
102
391
  } | false;
103
392
  /**
104
393
  * Wide-scope warning: emit a diagnostic when a scope's dependency count
105
- * reaches this (default 30) — the coarse-read / helper-leak signature.
106
- * Re-warns only if the count then grows by another 50%. `false` disables.
394
+ * reaches this (default 200) — the coarse-read / helper-leak signature.
395
+ * Re-warns only if the count then grows by another 50%. HMR plumbing is
396
+ * not counted, and in dev builds the renderer's insert child-resolution
397
+ * pass (which must track every row's resolved child) is not judged;
398
+ * HUGE_FAN_IN (2000, always on) still covers both. `false` disables.
107
399
  */
108
400
  wideDeps?: number | false;
109
401
  /**
@@ -126,61 +418,499 @@ export interface AttributionOptions {
126
418
  */
127
419
  unstableMemos?: number | false;
128
420
  /**
129
- * Written-fan-out warning: emit a diagnostic when a committed root
130
- * invalidation (write, refresh, async landing) reaches a node with at
131
- * least this many subscribers (default 250). Complements the always-on
132
- * HUGE_FAN_OUT graph-size warning, specced against it deliberately:
133
- * HUGE_FAN_OUT fires at LINK time from GRAPH_SIZE_WARN_AT (2000) up —
134
- * static structure so large it warns even if never written — while this
135
- * fires at WRITE time from a much lower bar, because fan-out only costs
136
- * anything when the node actually changes. Once per node, re-warning only
137
- * on 2x subscriber growth, so the two never spam the same node. `false`
421
+ * Wasted-recompute warning: emit WASTED_RECOMPUTE when a scope re-ran at
422
+ * least `minRuns` times within `windowMs` and `ratio` or more of those
423
+ * runs produced an unchanged value while costing `budgetMs` or more of
424
+ * compute in all (default 5 runs / 80% / 2ms / 1000ms). The equality gate
425
+ * closed every time: the scope's inputs changed without changing its
426
+ * result, so the runs were pure cost — the profiler-shaped fact
427
+ * `costs().wastedMs` sums, as a finding. Held and overlay runs are not
428
+ * counted (they may be replayed). Once per window per scope. `false`
138
429
  * disables.
139
430
  */
140
- wideWrites?: number | false;
141
- }
142
- export interface ScopeCost {
143
- name: string;
144
- kind: "effect" | "memo";
145
- runs: number;
146
- selfMs: number;
431
+ wastedRecompute?: {
432
+ minRuns: number;
433
+ ratio: number;
434
+ budgetMs: number;
435
+ windowMs: number;
436
+ } | false;
437
+ /**
438
+ * HUGE_FAN_OUT threshold while the engine is enabled: emit the finding
439
+ * when a committed root invalidation (write, refresh, async landing)
440
+ * reaches a node with at least this many subscribers (default 250). The
441
+ * same code the always-on core check emits from GRAPH_SIZE_WARN_AT (2000)
442
+ * up, with `data.write` naming the invalidation; the engine hands over to
443
+ * the core there, so one change never carries two findings. Once per
444
+ * node, re-warning once the count has grown by another 500. HMR plumbing
445
+ * subscribers are not counted. `false` leaves only the always-on threshold.
446
+ */
447
+ fanOut?: number | false;
448
+ /**
449
+ * Async-waterfall warning: emit a diagnostic when an async flight that
450
+ * could only start after an upstream flight resolved (its recompute's
451
+ * cause chain reaches the upstream's async landing, and its origin
452
+ * post-dates that landing) forms a sequential chain of 2+ flights, each
453
+ * of which took at least `minFlightMs` (default 50ms). The duration gate
454
+ * is one safety valve for what the graph cannot see: a settled
455
+ * preload/cache hit resolves fast and never warns. In-flight preloads are
456
+ * absolved by origin: `markFlight()` stamps (and first-seen identity)
457
+ * prove work predated the upstream landing — parallel, not sequential.
458
+ * Chains of 2 emit at `info` severity, structured channel only (a
459
+ * dependent fetch is sometimes intrinsic, and unmarked external preloads
460
+ * are invisible); 3+ escalate to `warn` with console output. `false`
461
+ * disables.
462
+ */
463
+ waterfalls?: {
464
+ minFlightMs: number;
465
+ } | false;
466
+ /**
467
+ * Silent-hold warning: emit a diagnostic when a user's writes were held
468
+ * behind async work for at least `infoMs` (default 100ms — RAIL's "feels
469
+ * instant" ceiling) and the screen never acknowledged the wait — no
470
+ * `isPending()`/`latest()` reader downstream of the held writes or their
471
+ * blockers, no optimistic value, no `affects()` mark, and no lane effect
472
+ * painted while held. Below `warnMs`
473
+ * (default 200ms — the INP "good" ceiling) the event is advisory
474
+ * (structured channel only); at or above it the console gets the finding.
475
+ * The engine measures to the commit, not the paint, so every number is a
476
+ * floor on what the user saw; the thresholds sit at the strict end of the
477
+ * band on purpose. Holds that staged no root write (initial loads, bare
478
+ * `refresh()`) are never judged: nothing the user did went unanswered.
479
+ * `false` disables hold tracking altogether (`longHolds` included).
480
+ */
481
+ holds?: {
482
+ infoMs: number;
483
+ warnMs: number;
484
+ } | false;
485
+ /**
486
+ * Long-hold warning: emit a diagnostic when a hold's quiescent tail — the
487
+ * time from the LAST write to join it until it committed — reached
488
+ * `infoMs` (default 500ms), `warn` from `warnMs` (default 1000ms, where
489
+ * RAIL says the user loses the thread). Measured from the last join so a
490
+ * hold that keeps taking input (typing) is judged by each wait, not by its
491
+ * lifetime. A hold this long is past what a stale screen should carry,
492
+ * acknowledged or not: the honest UI is a fallback, which a `Loading`
493
+ * boundary gives only when it has not revealed yet or its `on` prop
494
+ * changed. Reported as LONG_HOLD when the hold was acknowledged; a silent
495
+ * long hold stays one SILENT_HOLD with the boundary repair appended.
496
+ * `false` disables.
497
+ */
498
+ longHolds?: {
499
+ infoMs: number;
500
+ warnMs: number;
501
+ } | false;
502
+ /**
503
+ * Graph-growth warning: emit GRAPH_GROWTH when the live owner count at the
504
+ * settle of the same route has climbed on `visits` consecutive visits
505
+ * (default 3) to `ratio` or more of the first (default 1.25), and so has
506
+ * the part of it that outlived each of those navigations — a root or a
507
+ * subscription each visit leaves behind, the leak class a heap snapshot
508
+ * finds, not content a visit owns and leaving disposes. The count is a walk
509
+ * when a navigation opens and at its settle, never per node. Navigations
510
+ * that stay on a route pattern continue its visit and are not compared.
511
+ * `false` disables.
512
+ */
513
+ graphGrowth?: {
514
+ visits: number;
515
+ ratio: number;
516
+ } | false;
147
517
  /**
148
- * Self-time of PLAIN, non-held runs that produced an unchanged value —
149
- * the recoverable number. Overlay runs (optimistic/transition) are never
150
- * counted here: an optimistic recompute landing back on the committed
151
- * value is the mechanism working, not waste.
518
+ * Abandoned-flights warning: emit ABANDONED_FLIGHTS when one async source
519
+ * abandons `count` flights within `windowMs` — each superseded by the
520
+ * next before it landed (default 3 / 1000ms: the request-per-keystroke
521
+ * signature, every input asking again and the answers discarded). Once
522
+ * per window per source. In dev builds a framework node that supersedes
523
+ * its own flights by design (a frames slot arg re-read by each re-shipped
524
+ * record) is not judged. `false` disables.
152
525
  */
153
- wastedMs: number;
154
- /** Self-time spent in optimistic/transition (overlay) runs. */
155
- overlayMs: number;
526
+ abandonedFlights?: {
527
+ count: number;
528
+ windowMs: number;
529
+ } | false;
530
+ /**
531
+ * Fallback-flash finding: emit FALLBACK_FLASH (`info`) when a `Loading`
532
+ * boundary's fallback shows for less than `FALLBACK_FLASH_MS` (150ms) — a
533
+ * spinner that appeared and vanished, too much feedback for too little
534
+ * wait (default true). `false` disables.
535
+ */
536
+ fallbackFlashes?: boolean;
537
+ /**
538
+ * Stacked-holds warning: emit STACKED_HOLDS when `count` or more
539
+ * interactions are waiting in one hold when it commits (default 3) — the
540
+ * person kept clicking or typing while the first answer was in the air,
541
+ * and every one of them waited on the same source. `false` disables.
542
+ */
543
+ stackedHolds?: {
544
+ count: number;
545
+ } | false;
546
+ /**
547
+ * Optimistic-revert finding: emit OPTIMISTIC_REVERTED (`info`) when an
548
+ * optimistic value the screen showed is replaced by a different one —
549
+ * reverted at settle, or superseded by the truth (default true). The
550
+ * runtime's own optimistic nodes — `isPending`/`latest` companions and
551
+ * derived overrides — are never judged: they are the acknowledgement
552
+ * machinery, not a guess the person saw. `false` disables.
553
+ */
554
+ optimisticReverts?: boolean;
156
555
  }
157
- export interface WriteCost {
158
- /** Root cause name (a signal write, async landing, or refresh target). */
159
- name: string;
160
- /** Number of downstream re-runs this root triggered. */
161
- runs: number;
162
- /** Summed self-time of every downstream re-run it caused. */
163
- downstreamMs: number;
556
+ /** A fallback shown for less than this is a flash: feedback for a wait too short to need it. */
557
+ export declare const FALLBACK_FLASH_MS = 150;
558
+ /**
559
+ * The ring buffers `attribution.history(type)` reads, by type. Four of the
560
+ * five are the channel's records kept since the window opened; `waterfall`
561
+ * is a fact the engine keeps but never emits — a graph-provable sequential
562
+ * flight chain (`WaterfallRecord`), of which the ASYNC_WATERFALL finding is
563
+ * the thresholded view.
564
+ */
565
+ export interface HistoryRecords {
566
+ rerun: RerunEvent;
567
+ waterfall: WaterfallRecord;
568
+ hold: HoldEvent;
569
+ navigation: NavigationEvent;
570
+ interaction: InteractionEvent;
164
571
  }
572
+ export type HistoryType = keyof HistoryRecords;
573
+ /** @internal The engine's clock: `performance.now()` where it exists. */
574
+ export declare const now: () => number;
575
+ /**
576
+ * @internal The fold tables' seam. The engine emits records; the tables that
577
+ * fold them — `costs()` (scopes and writes), `feedback()` (sources,
578
+ * interactions, navigations, flights, fallbacks) — live in their own modules
579
+ * and register here when imported, so a consumer that only subscribes to
580
+ * records never ships them. Each hook fires at the moment the engine has the
581
+ * fact; `reset` on `enable()`/`disable()`. With nothing registered every
582
+ * site is one empty loop.
583
+ */
584
+ export interface FoldHooks {
585
+ rerun?(el: Computed<any>, event: RerunEvent): void;
586
+ hold?(event: HoldEvent): void;
587
+ navigation?(event: NavigationEvent): void;
588
+ /** A flight started; `abandoned` when it superseded one still in the air. */
589
+ flightStart?(el: Computed<any>, abandoned: boolean): void;
590
+ flightLanded?(el: Computed<any>, ms: number): void;
591
+ fallback?(boundary: object, tree: Computed<any> | undefined, shown: boolean): void;
592
+ reset?(): void;
593
+ }
594
+ /** @internal */
595
+ export declare function registerFold(hooks: FoldHooks): void;
596
+ /** @internal Root cause names of a cause chain — the writes/landings/refreshes the chain bottoms out in. */
597
+ export declare function rootsOf(causes: ChangeRecord[], out: Set<string>): void;
598
+ export declare function nodeName(node: Signal<any> | Computed<any>): string;
599
+ /** `click on button#next "Next →"`, `effect "syncTitle"`, `action "save"`, `navigation to /users/:id`, … */
600
+ export declare function formatOrigin(origin: ChangeOrigin): string;
165
601
  export declare function formatRerun(event: RerunEvent): string;
602
+ /**
603
+ * The engine: turn it on and read its ring buffers. Its records are
604
+ * delivered on the core's channel — `OBSERVE.records.subscribe("rerun" |
605
+ * "hold" | "interaction" | "navigation" | "create" | "effect" | "flush" |
606
+ * "flight" | "fallback" | "graph", (event, live) => …)` — the same place
607
+ * the runtimes' records arrive, so a consumer has one subscribe. The folds
608
+ * over those records — `costs()`, `feedback()` — and the point queries —
609
+ * `why()`, `subscriptions()` — and the formatters — `formatRerun()`,
610
+ * `formatOrigin()` — are named exports of `@solidjs/signals/attribution`
611
+ * rather than methods here, so a consumer that only wants records (a
612
+ * production adapter) does not ship the tables a console or an agent reads;
613
+ * importing a fold is what turns its accounting on.
614
+ */
166
615
  export interface Attribution {
167
- enable(opts?: AttributionOptions): void;
616
+ /**
617
+ * Install the engine, or take one more hold on it, and return the release
618
+ * for that hold. The engine is shared by every consumer in the page — a
619
+ * profiler track, an APM adapter, a diagnostics capture — so each
620
+ * `enable()` is a hold: the first installs the hooks and resets
621
+ * everything; one taken while already enabled opens a fresh window over
622
+ * the ring buffers and folds (`history(type)`, `costs()`, `feedback()` …
623
+ * read from here on) without disturbing the live tracking state or
624
+ * anyone's subscriptions, so a capture begun beside a running consumer
625
+ * still measures only its own scenario.
626
+ *
627
+ * Options combine across holds by the most demanding value per key: a
628
+ * hold's `opts` say what it wants (the defaults fill what it leaves
629
+ * unsaid), and the engine does whatever any holder asked for — the log
630
+ * prints while any holder wants it, a check runs while any holder wants
631
+ * it and at the most sensitive threshold requested, `historyLimit` is the
632
+ * largest. A hold can add to what another asked for, never take it away,
633
+ * so the result does not depend on the order holds were taken; releasing
634
+ * a hold withdraws its requests — a track enabled with `log: false` beside
635
+ * a console session never silences it, and a capture with tight
636
+ * thresholds beside a records-only adapter runs the checks for its own
637
+ * duration. One key runs the other way: `values` combines to the LEAST
638
+ * permissive level any holder asked for, so an adapter that must not see
639
+ * user data (`values: "none"`) is honoured beside a console session that
640
+ * wants everything. The release is idempotent; the last release uninstalls the
641
+ * hooks and clears every ring buffer. A consumer that re-`enable()`s to
642
+ * reopen its window must release both holds (or `disable()`).
643
+ *
644
+ * Subscriptions are not the engine's: its records arrive on
645
+ * `OBSERVE.records`, whose listeners outlive any hold — subscribe before
646
+ * or after `enable()`, and unsubscribe with the function `subscribe`
647
+ * returned.
648
+ */
649
+ enable(opts?: AttributionOptions): () => void;
650
+ /**
651
+ * Tear the engine down whatever holds are outstanding: drops every hold,
652
+ * uninstalls the hooks and clears every ring buffer. The console's and a
653
+ * test harness's reset — a consumer sharing the page with others releases
654
+ * its own hold with the function `enable()` returned instead. Idempotent;
655
+ * a `disable()` with nothing enabled is a no-op reset. Listeners on
656
+ * `OBSERVE.records` are untouched: they are the channel's.
657
+ */
168
658
  disable(): void;
169
- subscribe(listener: (event: RerunEvent) => void): () => void;
170
- history(): readonly RerunEvent[];
171
- /** Re-run history for one node — pass a memo/effect accessor or raw node. */
172
- why(target: unknown): RerunEvent[];
173
- /** Current dependency names of one scope — the devtools subscription view. */
174
- subscriptions(target: unknown): string[];
175
- /**
176
- * Aggregated cost tables since enable(): `scopes` ranked by self-time
177
- * (with `wastedMs` = time spent on unchanged-value runs), `writes` ranked
178
- * by total downstream re-run time each root write caused.
179
- */
180
- costs(): {
181
- scopes: ScopeCost[];
182
- writes: WriteCost[];
183
- };
184
- format: typeof formatRerun;
659
+ /**
660
+ * The ring buffer of `type` since `enable()` — the last `historyLimit`
661
+ * records (default 200), oldest first; the same objects the channel
662
+ * delivered. Facts, not verdicts: each is recorded regardless of the
663
+ * thresholds the findings apply to it.
664
+ *
665
+ * - `"rerun"` — every re-run recorded (see `RerunEvent`). Kept only while
666
+ * a record had an audience — a `rerun` listener, an imported fold
667
+ * (`costs`/`feedback`), or the console log; a records-only consumer
668
+ * that wants none of those pays for none, and reads an empty buffer.
669
+ * - `"waterfall"` — every graph-provable sequential flight chain, warned
670
+ * or not: the ASYNC_WATERFALL finding is the duration-gated view.
671
+ * - `"hold"` — every settled transition hold that staged at least one root
672
+ * write, acknowledged or not: SILENT_HOLD / LONG_HOLD are the
673
+ * thresholded verdicts (`HoldEvent.silent` / `.long` carry them).
674
+ * - `"navigation"` — every navigation a router declared via `withOrigin`,
675
+ * settled or not: what route, under which interaction, how many writes,
676
+ * and — once its writes are through — how long that took and how
677
+ * (`committed`, `held` with the `HoldEvent` attached, `superseded`).
678
+ * - `"interaction"` — every user interaction a runtime declared via
679
+ * `withInteraction`, settled or not: what was dispatched, when, what it
680
+ * wrote, the re-runs and creations it caused, the holds its writes
681
+ * waited in and the navigations it performed — and, once through, how
682
+ * long the person waited (`settledMs`) and how it ended. The
683
+ * per-dispatch record `feedback().interactions` folds by name.
684
+ */
685
+ history<K extends HistoryType>(type: K): readonly HistoryRecords[K][];
686
+ /**
687
+ * Cooperative preload declaration: stamp a flight object (promise or async
688
+ * iterable) with its true kickoff time BEFORE the reactive graph sees it.
689
+ * A route preloader or query cache calls this on the promise it hands out
690
+ * (on the WRAPPER it mints, with the original kickoff time — wrapping
691
+ * defeats identity tracking otherwise); any dependent that later awaits it
692
+ * is then judged against the real start — work already in the air when its
693
+ * upstream landed is parallel, never a waterfall link. Callable while
694
+ * attribution is disabled (marks made at navigation time must survive a
695
+ * later enable()).
696
+ */
697
+ markFlight(flight: object, startedAt?: number): void;
698
+ }
699
+ /** @internal The id the engine's records name `node` by, if it has one yet — read without assigning. */
700
+ export declare function nodeIdOf(node: object): number | undefined;
701
+ /** One landed flight: its node name, wall duration, and upstream chain. */
702
+ export interface FlightLink {
703
+ name: string;
704
+ ms: number;
705
+ }
706
+ export interface WaterfallRecord {
707
+ /** Sequential flights, oldest first, ending at the flight that landed. */
708
+ chain: FlightLink[];
709
+ /** Summed wall time of the chain — the serialized cost. */
710
+ sequentialMs: number;
711
+ }
712
+ export interface HeldWrite {
713
+ name: string;
714
+ prev?: string;
715
+ value?: string;
716
+ origin?: ChangeOrigin;
717
+ }
718
+ export interface HoldEvent {
719
+ /**
720
+ * When the wait began (`performance.now()` clock): the interaction that
721
+ * performed the held writes when one is known (`interaction.at`) or the
722
+ * first flush that parked them, whichever is earlier. `at + holdMs` is the
723
+ * commit.
724
+ */
725
+ at: number;
726
+ /** Wall time the user waited: `at` to the commit. */
727
+ holdMs: number;
728
+ /**
729
+ * The quiescent tail: from the LAST held write to join (the user's final
730
+ * input) to the commit. Equal to `holdMs` for a single write; shorter when
731
+ * the hold kept taking input. The LONG_HOLD measure.
732
+ */
733
+ tailMs: number;
734
+ /** The user interaction whose writes were held, when the stamp is known. */
735
+ interaction?: ChangeOrigin;
736
+ /**
737
+ * The declared unit of work the held writes belong to — the `navigation`
738
+ * a router described via `withOrigin` — when one is known. What names the
739
+ * hold by route (`navigation to /users/:id`) rather than by signal; the
740
+ * same object as `NavigationEvent.origin`, so the two join by identity.
741
+ */
742
+ origin?: ChangeOrigin;
743
+ /** Flushes that ended with the hold still open. */
744
+ flushes: number;
745
+ /** Root signal writes staged behind the hold (the user's unanswered input). */
746
+ heldWrites: HeldWrite[];
747
+ /** Async nodes the hold waited on (union across its parked flushes). */
748
+ blockers: string[];
749
+ /**
750
+ * Feedback the graph provably rendered for this hold — an `isPending()`
751
+ * reader, a `latest()` shadow, an optimistic value, an `affects()` mark —
752
+ * one entry per affordance, in the order found. Empty and
753
+ * `paintedDuringHold === 0` is the SILENT_HOLD signature.
754
+ */
755
+ acknowledgements: Acknowledgement[];
756
+ /**
757
+ * Effect callbacks that ran inside the hold's parked flushes. Mainline
758
+ * effects are stashed while a hold is open, so these are lane effects —
759
+ * readers of optimistic values and of `isPending()`/`latest()` companions,
760
+ * i.e. the screen changing in response to the hold. An unrelated effect
761
+ * cannot land here: it waits with everything else.
762
+ */
763
+ paintedDuringHold: number;
764
+ /** The hold was opened (or joined) by an `action()`. */
765
+ action: boolean;
766
+ /**
767
+ * The engine's silent-hold verdict, stamped at settle: no affordance
768
+ * acknowledged the wait (`acknowledgements` empty) and nothing painted
769
+ * while it was open (`paintedDuringHold === 0`). Duration-free — the
770
+ * SILENT_HOLD finding is this above `holds.infoMs` — so a consumer can
771
+ * flag every silent wait or apply its own floor, and a record that left
772
+ * the process still carries the verdict.
773
+ */
774
+ silent: boolean;
775
+ /**
776
+ * The engine's long-hold verdict, stamped at settle: the quiescent tail
777
+ * (`tailMs`) reached `longHolds.infoMs` under the options in effect;
778
+ * `false` when long-hold tracking is off. The LONG_HOLD finding is this
779
+ * verdict, tiered by `warnMs`.
780
+ */
781
+ long: boolean;
782
+ }
783
+ /**
784
+ * One way the screen acknowledged a hold. `reader` is where it was painted —
785
+ * the owner path of the first effect the census found reading the
786
+ * affordance (`["<App>", "<Feed>", "effect"]`), so a consumer can say WHICH
787
+ * screen answered, not only that one did. Absent when the affordance was
788
+ * registered but the census found no reader through the graph (an optimistic
789
+ * store: its readers are proxy traps, not nodes). `feedback()` ranks
790
+ * acknowledgements by `kind:source` (`isPending:posts`).
791
+ */
792
+ export interface Acknowledgement {
793
+ kind: "isPending" | "latest" | "optimistic" | "affects";
794
+ /** The node the affordance hangs on — the async source for `isPending`/`latest`, the optimistic/affected node otherwise. */
795
+ source: string;
796
+ reader?: string[];
797
+ }
798
+ /**
799
+ * The live graph's size — a walk, not a counter: nothing is charged at node
800
+ * creation, disposal, write or re-run; the engine asks at a navigation's
801
+ * settle. Two passes. The owner tree from the registered top-level roots
802
+ * gives `owners` and seeds the computations. Then each computation's
803
+ * dependency links give `edges` and the `signals` and computations they
804
+ * reach, and each newly met source's subscriber list gives the computations
805
+ * that read it — including one created with no owner, which no chain holds
806
+ * and only its sources keep alive: the subscription leak a heap snapshot
807
+ * finds. Measured at 5–10 ns per link; a 50k-owner graph walks in under a
808
+ * millisecond. Dormant nodes are spliced out of their chain and are not
809
+ * counted unless a subscription still reaches them.
810
+ */
811
+ export declare function graphSize(): GraphSize;
812
+ /** A destination a navigation abandoned when a redirect sent it elsewhere. */
813
+ export interface NavigationHop {
814
+ name?: string;
815
+ to?: string;
816
+ params?: Readonly<Record<string, string | undefined>>;
817
+ /** When the redirect away from it was declared (`performance.now()` clock). */
818
+ at: number;
819
+ }
820
+ export interface NavigationEvent {
821
+ /** The matched route pattern the router gave — `/users/:id`. After a redirect, the final one. */
822
+ name?: string;
823
+ to?: string;
824
+ from?: string;
825
+ params?: Readonly<Record<string, string | undefined>>;
826
+ /** When the navigation was requested (`performance.now()` clock). */
827
+ at: number;
828
+ /**
829
+ * The route the document arrived on, declared by the router at its initial
830
+ * match (`NavigationRef.initial`): `at` is the document's navigation start
831
+ * (`0` unless the router passed its own), there is no `from`, `writes` is
832
+ * `0`, and it settles `committed` when the router's frame closes — the
833
+ * record names the route the page loaded as; its timing is Navigation
834
+ * Timing's, not the runtime's.
835
+ */
836
+ initial?: true;
837
+ /** The user interaction it ran under, when known — a link click. */
838
+ interaction?: ChangeOrigin;
839
+ /** Root writes the frame performed, redirect hops included. */
840
+ writes: number;
841
+ /** Destinations abandoned along the way, in order — present only when a redirect occurred. */
842
+ redirects?: NavigationHop[];
843
+ /**
844
+ * Wall time from the request to settle: the end of the drain that committed
845
+ * its writes, or the commit of the hold they waited in. `undefined` while
846
+ * unsettled.
847
+ */
848
+ settledMs?: number;
849
+ outcome?: "committed" | "held" | "superseded";
850
+ /** The hold its writes waited in, when hold tracking recorded one. */
851
+ hold?: HoldEvent;
852
+ /**
853
+ * The frame object its writes were stamped with — `ChangeRecord.origin` on
854
+ * each, `HoldEvent.origin` on the hold. Join key, by identity.
855
+ */
856
+ origin: ChangeOrigin;
857
+ }
858
+ export interface InteractionEvent {
859
+ /** Event type — `click`, `keydown`, `input`… */
860
+ name: string;
861
+ /** The element hit, as the runtime described it — `button#next "Next →"`. */
862
+ target?: string;
863
+ /**
864
+ * When the interaction began (`performance.now()` clock): the browser event's
865
+ * own timestamp when the runtime supplied it, else the moment the handler
866
+ * frame opened. Joins `PerformanceEventTiming.startTime` for the same event.
867
+ */
868
+ at: number;
869
+ /**
870
+ * Browser event creation to handler entry (ms) — the queueing the browser's
871
+ * INP counts as input delay. Present only when `at` predates the frame.
872
+ */
873
+ inputDelayMs?: number;
874
+ /**
875
+ * Wall time of the handler itself, entry to return — for the frames of one
876
+ * event (`InteractionRef.event`), the first listener's entry to the last
877
+ * one's return, as Event Timing measures processing.
878
+ */
879
+ handlerMs: number;
880
+ /** Root writes attributed to the frame: the handler's, and those of frames it opened (a navigation). */
881
+ writes: number;
882
+ /** Re-runs traced back to this interaction while the record was open. */
883
+ runs: number;
884
+ /** Computations created in those runs or in the frame's flushes (the "create 1,000 rows" work). */
885
+ created: number;
886
+ /** Summed self-time of `runs` and `created` (ms). Quantized per run; `settledMs` is the wall clock. */
887
+ runMs: number;
888
+ /** Holds its writes waited in, in settle order. */
889
+ holds: HoldEvent[];
890
+ /** Navigations performed under it, in open order. */
891
+ navigations: NavigationEvent[];
892
+ /**
893
+ * Dispatch to settle: the handler's return when it wrote nothing, the end of
894
+ * the drain that committed its writes, or the commit of the last hold they
895
+ * waited in — whichever came last. `undefined` while unsettled.
896
+ */
897
+ settledMs?: number;
898
+ outcome?: "idle" | "committed" | "held";
899
+ /**
900
+ * The handler returned a thenable (`async () => { await save(); … }`) and
901
+ * the record waited for it: handler return → the promise settling (the last
902
+ * one, when several listeners of one event returned one), in
903
+ * milliseconds, capped at `ASYNC_HANDLER_CAP_MS`. The continuation runs
904
+ * with no frame on the stack, so writes it makes are not attributed to
905
+ * this interaction — only its duration is. Absent when the handler
906
+ * returned synchronously.
907
+ */
908
+ continuationMs?: number;
909
+ /**
910
+ * The frame object every downstream fact carries — `ChangeOrigin.interaction`
911
+ * on writes and frames, `RerunEvent.interaction`, `HoldEvent.interaction`,
912
+ * `NavigationEvent.interaction`. Join key, by identity.
913
+ */
914
+ origin: ChangeOrigin;
185
915
  }
186
916
  export declare const attribution: Attribution;