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