@solidjs/signals 2.0.0-rc.5 → 2.0.0-rc.7
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.js +2467 -1322
- package/dist/node.cjs +2037 -2428
- package/dist/node.dev.cjs +13724 -0
- package/dist/prod/boundaries.js +3 -1
- package/dist/prod/core/async.js +47 -20
- package/dist/prod/core/attribution-hooks.js +3 -0
- package/dist/prod/core/constants.js +9 -1
- package/dist/prod/core/context.js +10 -16
- package/dist/prod/core/core.js +260 -188
- package/dist/prod/core/dev.js +2 -0
- package/dist/prod/core/effect.js +41 -32
- package/dist/prod/core/external.js +2 -2
- package/dist/prod/core/graph.js +35 -31
- package/dist/prod/core/heap.js +34 -40
- package/dist/prod/core/lanes.js +44 -30
- package/dist/prod/core/optimistic.js +55 -45
- package/dist/prod/core/owner.js +35 -35
- package/dist/prod/core/scheduler.js +176 -177
- package/dist/prod/core/verdict.js +53 -52
- package/dist/prod/index.js +0 -2
- package/dist/prod/map.js +104 -94
- package/dist/prod/signals.js +119 -35
- package/dist/prod/store/next/optimistic.js +217 -163
- package/dist/prod/store/next/projection.js +2 -2
- package/dist/prod/store/next/reconcile.js +140 -288
- package/dist/prod/store/next/store.js +338 -252
- package/dist/prod/store/store.js +2 -2
- package/dist/types/core/attribution-hooks.d.ts +75 -0
- package/dist/types/core/attribution.d.ts +313 -9
- package/dist/types/core/constants.d.ts +8 -0
- package/dist/types/core/core.d.ts +12 -3
- package/dist/types/core/dev.d.ts +56 -9
- package/dist/types/core/heap.d.ts +5 -3
- package/dist/types/core/invariants.d.ts +1 -1
- package/dist/types/core/lanes.d.ts +10 -0
- package/dist/types/core/scheduler.d.ts +14 -4
- package/dist/types/signals.d.ts +10 -11
- package/dist/types/store/index.d.ts +3 -6
- package/dist/types/store/next/optimistic.d.ts +4 -2
- package/dist/types/store/next/reconcile.d.ts +5 -12
- package/dist/types/store/next/store.d.ts +3 -9
- package/dist/types/store/next/target.d.ts +30 -46
- package/dist/types/store/store.d.ts +14 -9
- package/dist/types-cjs/core/attribution-hooks.d.cts +75 -0
- package/dist/types-cjs/core/attribution.d.cts +313 -9
- package/dist/types-cjs/core/constants.d.cts +8 -0
- package/dist/types-cjs/core/core.d.cts +12 -3
- package/dist/types-cjs/core/dev.d.cts +56 -9
- package/dist/types-cjs/core/heap.d.cts +5 -3
- package/dist/types-cjs/core/invariants.d.cts +1 -1
- package/dist/types-cjs/core/lanes.d.cts +10 -0
- package/dist/types-cjs/core/scheduler.d.cts +14 -4
- package/dist/types-cjs/signals.d.cts +10 -11
- package/dist/types-cjs/store/index.d.cts +3 -6
- package/dist/types-cjs/store/next/optimistic.d.cts +4 -2
- package/dist/types-cjs/store/next/reconcile.d.cts +5 -12
- package/dist/types-cjs/store/next/store.d.cts +3 -9
- package/dist/types-cjs/store/next/target.d.cts +30 -46
- package/dist/types-cjs/store/store.d.cts +14 -9
- package/package.json +3 -2
- package/dist/prod/store/next/patch-hooks.js +0 -13
- package/dist/prod/store/next/patch.js +0 -614
- package/dist/types/store/next/patch-hooks.d.ts +0 -41
- package/dist/types/store/next/patch.d.ts +0 -91
- package/dist/types-cjs/store/next/patch-hooks.d.cts +0 -41
- package/dist/types-cjs/store/next/patch.d.cts +0 -91
package/dist/prod/store/store.js
CHANGED
|
@@ -252,7 +252,7 @@ s) {
|
|
|
252
252
|
// Callers guard on `pendingCheckActive`, which only flips inside
|
|
253
253
|
// isPending() — the verdict layer is loaded and its hook installed.
|
|
254
254
|
const o = e[STORE_NODE]?.[$AFFECTS];
|
|
255
|
-
if (o?.o?.t) GlobalQueue.
|
|
255
|
+
if (o?.o?.t) GlobalQueue.zt(o);
|
|
256
256
|
if (affectsScopes.size) {
|
|
257
257
|
// Chained backings (§7b): a wrapper's STORE_VALUE can be another store's
|
|
258
258
|
// proxy — marks cover by identity of the BASE raw, so resolve the chain
|
|
@@ -263,7 +263,7 @@ s) {
|
|
|
263
263
|
let t = r;
|
|
264
264
|
for (;;) {
|
|
265
265
|
if (s.scope.has(t)) {
|
|
266
|
-
GlobalQueue.
|
|
266
|
+
GlobalQueue.zt(e);
|
|
267
267
|
break;
|
|
268
268
|
}
|
|
269
269
|
const o = t?.[$TARGET];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Transition } from "./scheduler.js";
|
|
1
2
|
import type { Computed, Signal } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Dev-only observability hook points for the reactive core.
|
|
@@ -36,6 +37,17 @@ export interface AttributionHooks {
|
|
|
36
37
|
write(el: Signal<any> | Computed<any>, prev: unknown, value: unknown): void;
|
|
37
38
|
/** refresh() invalidated this node (self-invalidation, no dep changed). */
|
|
38
39
|
refreshed(el: Computed<any>): void;
|
|
40
|
+
/**
|
|
41
|
+
* A new async flight entered the system (`_inFlight` was just assigned
|
|
42
|
+
* during a recompute of `el`). Always fired inside the owning recompute —
|
|
43
|
+
* both call paths (core's recompute and the projection self-registration)
|
|
44
|
+
* run within one — so the engine can read the current frame stack to link
|
|
45
|
+
* the flight to the change that caused it (waterfall chaining). `flight`
|
|
46
|
+
* is the registered thenable/iterable itself: the engine keys a first-seen
|
|
47
|
+
* origin registry on its identity, so shared and preloader-marked promises
|
|
48
|
+
* carry their true start time instead of the moment the graph saw them.
|
|
49
|
+
*/
|
|
50
|
+
flightStart(el: Computed<any>, flight: object): void;
|
|
39
51
|
/** An async landing is about to apply its value (before any branch). */
|
|
40
52
|
asyncStart(el: Computed<any>): void;
|
|
41
53
|
/**
|
|
@@ -47,6 +59,69 @@ export interface AttributionHooks {
|
|
|
47
59
|
* from the node's state against its asyncStart snapshot.
|
|
48
60
|
*/
|
|
49
61
|
asyncEnd(el: Computed<any>, prev: unknown, value: unknown, direct: boolean): void;
|
|
62
|
+
/**
|
|
63
|
+
* An effect's imperative half (its effect callback) is about to run /
|
|
64
|
+
* has run. Both fire outside the run's try; `effectRunEnd` fires whether
|
|
65
|
+
* or not the callback threw. Writes between the two are the effect's.
|
|
66
|
+
*/
|
|
67
|
+
effectRunStart(el: Computed<any>): void;
|
|
68
|
+
effectRunEnd(el: Computed<any>): void;
|
|
69
|
+
/**
|
|
70
|
+
* One synchronous step of an `action()` generator is about to run / has
|
|
71
|
+
* run (`it.next()`/`it.throw()` up to the next yield). `it` is the
|
|
72
|
+
* invocation's iterator — stable identity across its steps; `name` the
|
|
73
|
+
* generator function's name. Writes between the two are the action's.
|
|
74
|
+
*/
|
|
75
|
+
actionStepStart(it: object, name: string | undefined): void;
|
|
76
|
+
actionStepEnd(it: object): void;
|
|
77
|
+
/**
|
|
78
|
+
* A flush found `t` incomplete (transitionComplete's false verdict): its
|
|
79
|
+
* writes stay staged and its queues are about to be parked. Fired BEFORE
|
|
80
|
+
* this flush's lane effects (the visible acknowledgers — isPending
|
|
81
|
+
* companions, optimistic values) run; `holdEnd` fires from the root
|
|
82
|
+
* stashQueues call after them, so effect runs between the two are runs that
|
|
83
|
+
* painted *during* the hold.
|
|
84
|
+
*/
|
|
85
|
+
holdStart(t: Transition): void;
|
|
86
|
+
holdEnd(): void;
|
|
87
|
+
/**
|
|
88
|
+
* `t` was judged complete (transitionComplete's true verdict, before `_done`
|
|
89
|
+
* flips). Fired before its held writes commit, so `t._pendingNodes` still
|
|
90
|
+
* lists what was staged.
|
|
91
|
+
*/
|
|
92
|
+
transitionSettled(t: Transition): void;
|
|
93
|
+
/** `outgoing` was folded into `target` (`outgoing._done = target`). */
|
|
94
|
+
transitionMerged(target: Transition, outgoing: Transition): void;
|
|
95
|
+
/**
|
|
96
|
+
* A store setter batch replaced the container at `path` (e.g. `store.user`)
|
|
97
|
+
* with a different one (both non-null, same array-ness, not the same
|
|
98
|
+
* logical slot), and this is the leaf census of the new container against
|
|
99
|
+
* the old: `total` leaves (own keys, or items) in the new one, `unchanged`
|
|
100
|
+
* of which are the same value as before (identity, judged on unwrapped
|
|
101
|
+
* values — object keys compared by key, array items by membership), and
|
|
102
|
+
* `prevTotal` leaves in the old one. Containers above 64 leaves are not
|
|
103
|
+
* announced. Fired per written key from the write channel's notify. The
|
|
104
|
+
* engine decides whether the replacement was a spread-copy worth a
|
|
105
|
+
* diagnostic.
|
|
106
|
+
*/
|
|
107
|
+
storeReplaced(path: string, isArray: boolean, total: number, unchanged: number, prevTotal: number): void;
|
|
108
|
+
/**
|
|
109
|
+
* A `mapArray` update both disposed and created rows: `removed` are the
|
|
110
|
+
* items whose rows were disposed, `created` the items that got new rows,
|
|
111
|
+
* `newLen` the list's new length, `keyed` whether a key function is in use
|
|
112
|
+
* (false = identity or by-index). Fired after commit. The engine judges
|
|
113
|
+
* whether the churn replaced equivalent records (unstable identity).
|
|
114
|
+
*/
|
|
115
|
+
listChurn(el: Computed<any>, removed: unknown[], created: unknown[], newLen: number, keyed: boolean): void;
|
|
116
|
+
/**
|
|
117
|
+
* A loading boundary started (`shown` true) or stopped showing its
|
|
118
|
+
* fallback. `boundary` is the boundary's queue (stable identity); `tree`
|
|
119
|
+
* its bound subtree computed when already constructed — the first show can
|
|
120
|
+
* fire while the subtree is still being built — whose owner chain names
|
|
121
|
+
* the boundary. Fired at the source-set transitions (first pending source
|
|
122
|
+
* registers / last one clears), not per flush.
|
|
123
|
+
*/
|
|
124
|
+
boundaryFallback(boundary: object, tree: Computed<any> | undefined, shown: boolean): void;
|
|
50
125
|
}
|
|
51
126
|
export declare let attrHooks: AttributionHooks | null;
|
|
52
127
|
export declare function setAttributionHooks(hooks: AttributionHooks | null): void;
|
|
@@ -22,6 +22,45 @@ import type { Computed } from "./types.js";
|
|
|
22
22
|
* front-ends.
|
|
23
23
|
*/
|
|
24
24
|
export type ChangeKind = "write" | "derived" | "async" | "refresh";
|
|
25
|
+
/**
|
|
26
|
+
* Provenance of a root change: the imperative frame that performed it.
|
|
27
|
+
*
|
|
28
|
+
* - `interaction` — a user event handler (the web runtime marks dispatch via
|
|
29
|
+
* `withInteraction`). `name` is the event type, `target` the element hit
|
|
30
|
+
* (`button#next "Next →"`), `at` the dispatch time on the `performance.now()`
|
|
31
|
+
* clock — the base every feedback-latency number is measured from.
|
|
32
|
+
* - `effect` — an effect callback (`name` = the effect's name; `run` = the
|
|
33
|
+
* compute run whose effect phase performed the write, when that run was
|
|
34
|
+
* recorded — so a write can be joined to the re-run that produced it).
|
|
35
|
+
* - `action` — a step of an `action()` generator (`name` = the generator's
|
|
36
|
+
* name, when it has one). Writes after an `await` (not a `yield`) run in a
|
|
37
|
+
* bare microtask and stamp `external` — the documented escape.
|
|
38
|
+
* - `async` — an async landing (`name` = the node whose flight landed).
|
|
39
|
+
* - `external` — none of the above: timers, sockets, promise callbacks, setup.
|
|
40
|
+
*
|
|
41
|
+
* `interaction` on a non-interaction frame is the user event the frame runs
|
|
42
|
+
* under — an action started by a click, an effect whose run was caused by a
|
|
43
|
+
* click's write, a landing whose flight a click started. It is what lets
|
|
44
|
+
* every downstream cost be keyed by the interaction that paid for it.
|
|
45
|
+
*/
|
|
46
|
+
export interface ChangeOrigin {
|
|
47
|
+
kind: "interaction" | "effect" | "action" | "async" | "external";
|
|
48
|
+
name?: string;
|
|
49
|
+
target?: string;
|
|
50
|
+
at?: number;
|
|
51
|
+
interaction?: ChangeOrigin;
|
|
52
|
+
/** `effect` only: the `RerunEvent.run` of the compute run this callback belongs to. */
|
|
53
|
+
run?: number;
|
|
54
|
+
}
|
|
55
|
+
/** A user interaction, as the web runtime describes it to `withInteraction`. */
|
|
56
|
+
export interface InteractionRef {
|
|
57
|
+
/** Event type — `click`, `keydown`, `input`… */
|
|
58
|
+
type: string;
|
|
59
|
+
/** The element hit, e.g. `button#next "Next →"`. */
|
|
60
|
+
target?: string;
|
|
61
|
+
/** Dispatch time on the `performance.now()` clock; defaults to now. */
|
|
62
|
+
at?: number;
|
|
63
|
+
}
|
|
25
64
|
export interface ChangeRecord {
|
|
26
65
|
/** Global monotonic change sequence — orders causes across the app. */
|
|
27
66
|
seq: number;
|
|
@@ -34,6 +73,10 @@ export interface ChangeRecord {
|
|
|
34
73
|
stack?: string[];
|
|
35
74
|
/** For derived changes: the upstream changes that produced this one. */
|
|
36
75
|
causes?: ChangeRecord[];
|
|
76
|
+
/** Root changes only: who performed the write. */
|
|
77
|
+
origin?: ChangeOrigin;
|
|
78
|
+
/** Root changes only: when the write was stamped (`performance.now()` clock). */
|
|
79
|
+
at?: number;
|
|
37
80
|
}
|
|
38
81
|
export interface RerunEvent {
|
|
39
82
|
/** Global monotonic run sequence. */
|
|
@@ -74,19 +117,21 @@ export interface RerunEvent {
|
|
|
74
117
|
changed: boolean;
|
|
75
118
|
/**
|
|
76
119
|
* Which posture this run executed under. "optimistic" = under an
|
|
77
|
-
* optimistic lane (overlay recompute); "
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
120
|
+
* optimistic lane (overlay recompute); "held" = a hold was open or owns
|
|
121
|
+
* the node (the run may be replayed/settled later); "plain" = an ordinary
|
|
122
|
+
* committed run. Overlay runs are real work (they count toward time
|
|
123
|
+
* budgets) but are never blamed as waste, and costs() reports their time
|
|
124
|
+
* separately as `overlayMs`.
|
|
82
125
|
*/
|
|
83
|
-
phase: "plain" | "
|
|
126
|
+
phase: "plain" | "held" | "optimistic";
|
|
84
127
|
/**
|
|
85
|
-
* The changed value was
|
|
86
|
-
*
|
|
87
|
-
*
|
|
128
|
+
* The changed value was parked in `_pendingValue` (held) rather than
|
|
129
|
+
* committed directly; its reveal happens on the hold's own schedule. Held
|
|
130
|
+
* runs are excluded from waste accounting.
|
|
88
131
|
*/
|
|
89
132
|
held: boolean;
|
|
133
|
+
/** The user interaction this run traces back to through its causes, if any. */
|
|
134
|
+
interaction?: ChangeOrigin;
|
|
90
135
|
}
|
|
91
136
|
export interface AttributionOptions {
|
|
92
137
|
/** Pretty-print each re-run to the console (default true). */
|
|
@@ -143,6 +188,60 @@ export interface AttributionOptions {
|
|
|
143
188
|
* disables.
|
|
144
189
|
*/
|
|
145
190
|
wideWrites?: number | false;
|
|
191
|
+
/**
|
|
192
|
+
* Async-waterfall warning: emit a diagnostic when an async flight that
|
|
193
|
+
* could only start after an upstream flight resolved (its recompute's
|
|
194
|
+
* cause chain reaches the upstream's async landing, and its origin
|
|
195
|
+
* post-dates that landing) forms a sequential chain of 2+ flights, each
|
|
196
|
+
* of which took at least `minFlightMs` (default 50ms). The duration gate
|
|
197
|
+
* is one safety valve for what the graph cannot see: a settled
|
|
198
|
+
* preload/cache hit resolves fast and never warns. In-flight preloads are
|
|
199
|
+
* absolved by origin: `markFlight()` stamps (and first-seen identity)
|
|
200
|
+
* prove work predated the upstream landing — parallel, not sequential.
|
|
201
|
+
* Chains of 2 emit at `info` severity, structured channel only (a
|
|
202
|
+
* dependent fetch is sometimes intrinsic, and unmarked external preloads
|
|
203
|
+
* are invisible); 3+ escalate to `warn` with console output. `false`
|
|
204
|
+
* disables.
|
|
205
|
+
*/
|
|
206
|
+
waterfalls?: {
|
|
207
|
+
minFlightMs: number;
|
|
208
|
+
} | false;
|
|
209
|
+
/**
|
|
210
|
+
* Silent-hold warning: emit a diagnostic when a user's writes were held
|
|
211
|
+
* behind async work for at least `infoMs` (default 100ms — RAIL's "feels
|
|
212
|
+
* instant" ceiling) and the screen never acknowledged the wait — no
|
|
213
|
+
* `isPending()`/`latest()` reader downstream of the held writes or their
|
|
214
|
+
* blockers, no optimistic value, no `affects()` mark, and no lane effect
|
|
215
|
+
* painted while held. Below `warnMs`
|
|
216
|
+
* (default 200ms — the INP "good" ceiling) the event is advisory
|
|
217
|
+
* (structured channel only); at or above it the console gets the finding.
|
|
218
|
+
* The engine measures to the commit, not the paint, so every number is a
|
|
219
|
+
* floor on what the user saw; the thresholds sit at the strict end of the
|
|
220
|
+
* band on purpose. Holds that staged no root write (initial loads, bare
|
|
221
|
+
* `refresh()`) are never judged: nothing the user did went unanswered.
|
|
222
|
+
* `false` disables hold tracking altogether (`longHolds` included).
|
|
223
|
+
*/
|
|
224
|
+
holds?: {
|
|
225
|
+
infoMs: number;
|
|
226
|
+
warnMs: number;
|
|
227
|
+
} | false;
|
|
228
|
+
/**
|
|
229
|
+
* Long-hold warning: emit a diagnostic when a hold's quiescent tail — the
|
|
230
|
+
* time from the LAST write to join it until it committed — reached
|
|
231
|
+
* `infoMs` (default 500ms), `warn` from `warnMs` (default 1000ms, where
|
|
232
|
+
* RAIL says the user loses the thread). Measured from the last join so a
|
|
233
|
+
* hold that keeps taking input (typing) is judged by each wait, not by its
|
|
234
|
+
* lifetime. A hold this long is past what a stale screen should carry,
|
|
235
|
+
* acknowledged or not: the honest UI is a fallback, which a `Loading`
|
|
236
|
+
* boundary gives only when it has not revealed yet or its `on` prop
|
|
237
|
+
* changed. Reported as LONG_HOLD when the hold was acknowledged; a silent
|
|
238
|
+
* long hold stays one SILENT_HOLD with the boundary repair appended.
|
|
239
|
+
* `false` disables.
|
|
240
|
+
*/
|
|
241
|
+
longHolds?: {
|
|
242
|
+
infoMs: number;
|
|
243
|
+
warnMs: number;
|
|
244
|
+
} | false;
|
|
146
245
|
}
|
|
147
246
|
export interface ScopeCost {
|
|
148
247
|
name: string;
|
|
@@ -167,6 +266,15 @@ export interface WriteCost {
|
|
|
167
266
|
/** Summed self-time of every downstream re-run it caused. */
|
|
168
267
|
downstreamMs: number;
|
|
169
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Run `fn` as the handler of a user interaction: every root write it performs
|
|
271
|
+
* (and every action step or effect the write causes) carries the interaction
|
|
272
|
+
* as provenance. The web runtime wraps event dispatch in this; it is dev-only
|
|
273
|
+
* and safe to call with no engine enabled.
|
|
274
|
+
*/
|
|
275
|
+
export declare function withInteraction<T>(ref: InteractionRef, fn: () => T): T;
|
|
276
|
+
/** `click on button#next "Next →"`, `effect "syncTitle"`, `action "save"`, … */
|
|
277
|
+
export declare function formatOrigin(origin: ChangeOrigin): string;
|
|
170
278
|
export declare function formatRerun(event: RerunEvent): string;
|
|
171
279
|
export interface Attribution {
|
|
172
280
|
enable(opts?: AttributionOptions): void;
|
|
@@ -186,6 +294,202 @@ export interface Attribution {
|
|
|
186
294
|
scopes: ScopeCost[];
|
|
187
295
|
writes: WriteCost[];
|
|
188
296
|
};
|
|
297
|
+
/**
|
|
298
|
+
* Every graph-provable sequential flight chain observed since enable()
|
|
299
|
+
* (ring-buffered like history()). Facts, not verdicts: chains are recorded
|
|
300
|
+
* regardless of the duration gate — the ASYNC_WATERFALL diagnostic is the
|
|
301
|
+
* thresholded view of the same data.
|
|
302
|
+
*/
|
|
303
|
+
waterfalls(): readonly WaterfallRecord[];
|
|
304
|
+
/**
|
|
305
|
+
* Every settled transition hold that staged at least one root write since
|
|
306
|
+
* enable() (ring-buffered like history()). Facts, not verdicts: recorded
|
|
307
|
+
* regardless of duration or acknowledgment — the SILENT_HOLD diagnostic is
|
|
308
|
+
* the thresholded, unacknowledged subset.
|
|
309
|
+
*/
|
|
310
|
+
holds(): readonly HoldEvent[];
|
|
311
|
+
/**
|
|
312
|
+
* What the user waited on, folded from holds() and the interaction on each
|
|
313
|
+
* re-run: `sources` ranks async sources by the silent time writes spent
|
|
314
|
+
* held behind them (with which affordances answered, how often, and which
|
|
315
|
+
* interactions were held); `interactions` ranks user events by the total
|
|
316
|
+
* time they cost — re-run work caused (long-flush hazard) beside time held
|
|
317
|
+
* (silent-hold hazard). Facts at every duration; SILENT_HOLD is the
|
|
318
|
+
* thresholded verdict. Two more tables round out the picture: `flights`
|
|
319
|
+
* counts each async source's flights and how many were abandoned before
|
|
320
|
+
* landing (the re-ask storm), and `fallbacks` measures how long each
|
|
321
|
+
* loading boundary showed its fallback and how often that was a flash.
|
|
322
|
+
*/
|
|
323
|
+
feedback(): AttributionFeedbackTables;
|
|
324
|
+
/**
|
|
325
|
+
* Cooperative preload declaration: stamp a flight object (promise or async
|
|
326
|
+
* iterable) with its true kickoff time BEFORE the reactive graph sees it.
|
|
327
|
+
* A route preloader or query cache calls this on the promise it hands out
|
|
328
|
+
* (on the WRAPPER it mints, with the original kickoff time — wrapping
|
|
329
|
+
* defeats identity tracking otherwise); any dependent that later awaits it
|
|
330
|
+
* is then judged against the real start — work already in the air when its
|
|
331
|
+
* upstream landed is parallel, never a waterfall link. Callable while
|
|
332
|
+
* attribution is disabled (marks made at navigation time must survive a
|
|
333
|
+
* later enable()). Dev-only, like the whole DEV surface.
|
|
334
|
+
*/
|
|
335
|
+
markFlight(flight: object, startedAt?: number): void;
|
|
336
|
+
/**
|
|
337
|
+
* Run `fn` as a user interaction's handler: root writes inside stamp it as
|
|
338
|
+
* their origin, and actions/effects/flights it causes carry it. The web
|
|
339
|
+
* runtime wraps every event dispatch in this; custom renderers and test
|
|
340
|
+
* harnesses call it themselves. Callable while attribution is disabled.
|
|
341
|
+
*/
|
|
342
|
+
withInteraction: typeof withInteraction;
|
|
189
343
|
format: typeof formatRerun;
|
|
344
|
+
formatOrigin: typeof formatOrigin;
|
|
345
|
+
}
|
|
346
|
+
/** One landed flight: its node name, wall duration, and upstream chain. */
|
|
347
|
+
export interface FlightLink {
|
|
348
|
+
name: string;
|
|
349
|
+
ms: number;
|
|
350
|
+
}
|
|
351
|
+
export interface WaterfallRecord {
|
|
352
|
+
/** Sequential flights, oldest first, ending at the flight that landed. */
|
|
353
|
+
chain: FlightLink[];
|
|
354
|
+
/** Summed wall time of the chain — the serialized cost. */
|
|
355
|
+
sequentialMs: number;
|
|
356
|
+
}
|
|
357
|
+
export interface HeldWrite {
|
|
358
|
+
name: string;
|
|
359
|
+
prev?: string;
|
|
360
|
+
value?: string;
|
|
361
|
+
origin?: ChangeOrigin;
|
|
362
|
+
}
|
|
363
|
+
export interface HoldEvent {
|
|
364
|
+
/**
|
|
365
|
+
* Wall time the user waited: from the interaction that performed the held
|
|
366
|
+
* writes when one is known (`interaction.at`) or the first flush that
|
|
367
|
+
* parked them, whichever is earlier, to the commit.
|
|
368
|
+
*/
|
|
369
|
+
holdMs: number;
|
|
370
|
+
/**
|
|
371
|
+
* The quiescent tail: from the LAST held write to join (the user's final
|
|
372
|
+
* input) to the commit. Equal to `holdMs` for a single write; shorter when
|
|
373
|
+
* the hold kept taking input. The LONG_HOLD measure.
|
|
374
|
+
*/
|
|
375
|
+
tailMs: number;
|
|
376
|
+
/** The user interaction whose writes were held, when the stamp is known. */
|
|
377
|
+
interaction?: ChangeOrigin;
|
|
378
|
+
/** Flushes that ended with the hold still open. */
|
|
379
|
+
flushes: number;
|
|
380
|
+
/** Root signal writes staged behind the hold (the user's unanswered input). */
|
|
381
|
+
heldWrites: HeldWrite[];
|
|
382
|
+
/** Async nodes the hold waited on (union across its parked flushes). */
|
|
383
|
+
blockers: string[];
|
|
384
|
+
/**
|
|
385
|
+
* Feedback the graph provably rendered for this hold, as `"<kind>:<node>"`
|
|
386
|
+
* — `isPending:posts`, `latest:page`, `optimistic:todos`, `affects:list`.
|
|
387
|
+
* Empty and `paintedDuringHold === 0` is the SILENT_HOLD signature.
|
|
388
|
+
*/
|
|
389
|
+
acknowledgedBy: string[];
|
|
390
|
+
/**
|
|
391
|
+
* Effect callbacks that ran inside the hold's parked flushes. Mainline
|
|
392
|
+
* effects are stashed while a hold is open, so these are lane effects —
|
|
393
|
+
* readers of optimistic values and of `isPending()`/`latest()` companions,
|
|
394
|
+
* i.e. the screen changing in response to the hold. An unrelated effect
|
|
395
|
+
* cannot land here: it waits with everything else.
|
|
396
|
+
*/
|
|
397
|
+
paintedDuringHold: number;
|
|
398
|
+
/** The hold was opened (or joined) by an `action()`. */
|
|
399
|
+
action: boolean;
|
|
400
|
+
}
|
|
401
|
+
export interface FeedbackSource {
|
|
402
|
+
/** The async nodes the holds waited on; empty when an action alone kept them open. */
|
|
403
|
+
sources: string[];
|
|
404
|
+
holds: number;
|
|
405
|
+
/** Summed wait across the holds (ms). */
|
|
406
|
+
heldMs: number;
|
|
407
|
+
worstMs: number;
|
|
408
|
+
/** Holds with no acknowledgment at all — the SILENT_HOLD signature, at any duration. */
|
|
409
|
+
silent: number;
|
|
410
|
+
silentMs: number;
|
|
411
|
+
/** Holds whose only acknowledgment was a `latest()` shadow: the input showed, nothing said "loading". */
|
|
412
|
+
latestOnly: number;
|
|
413
|
+
/**
|
|
414
|
+
* Holds whose quiescent tail (last write to join → commit) reached
|
|
415
|
+
* `longHolds.infoMs`, acknowledged or not — the LONG_HOLD signature at the
|
|
416
|
+
* table level. The affordance is not the whole answer there: a fallback
|
|
417
|
+
* (`Loading` keyed with `on`), a preload, a cache, or a faster source is.
|
|
418
|
+
* `longMs` sums the tails.
|
|
419
|
+
*/
|
|
420
|
+
long: number;
|
|
421
|
+
longMs: number;
|
|
422
|
+
/** Which affordances answered, and in how many holds — ranked. */
|
|
423
|
+
acknowledgedBy: {
|
|
424
|
+
by: string;
|
|
425
|
+
holds: number;
|
|
426
|
+
}[];
|
|
427
|
+
/** Interactions whose writes were held here, ranked by holds. */
|
|
428
|
+
interactions: {
|
|
429
|
+
interaction: string;
|
|
430
|
+
holds: number;
|
|
431
|
+
}[];
|
|
432
|
+
/** Distinct root writes that were held. */
|
|
433
|
+
writes: string[];
|
|
434
|
+
/** Holds an action opened or joined. */
|
|
435
|
+
actions: number;
|
|
436
|
+
}
|
|
437
|
+
export interface FeedbackInteraction {
|
|
438
|
+
/** `click on button#next "Next →"` — type and target; repeated dispatches fold together. */
|
|
439
|
+
interaction: string;
|
|
440
|
+
/** Distinct dispatches seen (by dispatch time). */
|
|
441
|
+
dispatches: number;
|
|
442
|
+
/** Re-runs traced back to this interaction, and their summed self-time. */
|
|
443
|
+
runs: number;
|
|
444
|
+
selfMs: number;
|
|
445
|
+
/** The most re-run self-time a single dispatch caused — the long-flush hazard. */
|
|
446
|
+
worstDispatchMs: number;
|
|
447
|
+
/** Holds this interaction's writes waited in — the silent-hold hazard. */
|
|
448
|
+
holds: number;
|
|
449
|
+
heldMs: number;
|
|
450
|
+
silentMs: number;
|
|
451
|
+
worstHoldMs: number;
|
|
452
|
+
}
|
|
453
|
+
/** Per async source: how many flights it started, and how many it threw away. */
|
|
454
|
+
export interface FlightStats {
|
|
455
|
+
source: string;
|
|
456
|
+
/** Flights registered (a recompute that produced a new promise/iterable). */
|
|
457
|
+
flights: number;
|
|
458
|
+
/** Flights that landed (whether or not the value changed). */
|
|
459
|
+
landed: number;
|
|
460
|
+
/**
|
|
461
|
+
* Flights superseded by a newer one before landing — the search-as-you-type
|
|
462
|
+
* signature when large: every keystroke asked, most answers were discarded.
|
|
463
|
+
* A debounced/equality-gated derivation between input and fetch is the repair.
|
|
464
|
+
*/
|
|
465
|
+
abandoned: number;
|
|
466
|
+
/** Summed and worst wall time of landed flights (ms). */
|
|
467
|
+
landedMs: number;
|
|
468
|
+
worstMs: number;
|
|
469
|
+
}
|
|
470
|
+
/** Per loading boundary: how long, and how briefly, it showed its fallback. */
|
|
471
|
+
export interface FallbackStats {
|
|
472
|
+
/** The boundary's owner path (`<App> › <Feed>`), or `boundary` when unnamed. */
|
|
473
|
+
boundary: string;
|
|
474
|
+
/** Times the fallback was shown. */
|
|
475
|
+
shows: number;
|
|
476
|
+
/** Summed and worst fallback duration (ms) across completed shows. */
|
|
477
|
+
shownMs: number;
|
|
478
|
+
worstMs: number;
|
|
479
|
+
/**
|
|
480
|
+
* Shows shorter than the flash window (default 150ms): a spinner that
|
|
481
|
+
* appeared and vanished — the other end of the SILENT_HOLD spectrum, too
|
|
482
|
+
* much feedback for too little wait. A preload, a cache, or lifting the
|
|
483
|
+
* fetch above the boundary removes the flash.
|
|
484
|
+
*/
|
|
485
|
+
flashes: number;
|
|
486
|
+
}
|
|
487
|
+
export interface AttributionFeedbackTables {
|
|
488
|
+
sources: FeedbackSource[];
|
|
489
|
+
interactions: FeedbackInteraction[];
|
|
490
|
+
/** Async sources ranked by abandoned flights, then by flights. */
|
|
491
|
+
flights: FlightStats[];
|
|
492
|
+
/** Loading boundaries ranked by flashes, then by time shown. */
|
|
493
|
+
fallbacks: FallbackStats[];
|
|
190
494
|
}
|
|
191
495
|
export declare const attribution: Attribution;
|
|
@@ -105,6 +105,14 @@ export declare const CONFIG_FRESH_READ: number;
|
|
|
105
105
|
* (A17). Cleared at commit (the commit IS the reveal); subscribers masked
|
|
106
106
|
* during the hold are woken by finalizePureQueue's post-revert pass. */
|
|
107
107
|
export declare const CONFIG_HELD_TRUTH: number;
|
|
108
|
+
/** SLOT node (store leaf): created through `slotSignal` with `_host`/`_key`
|
|
109
|
+
* backrefs baked into the literal. The unobserved sweep dispatches these to
|
|
110
|
+
* the ONE shared hook (`setSlotUnobserved`) instead of a per-node closure
|
|
111
|
+
* held in a per-node extension — store mounts materialize one signal per
|
|
112
|
+
* touched leaf, so per-node allocations (options object, equals closure,
|
|
113
|
+
* unobserved closure, NodeExtension) were the measured create-floor bytes
|
|
114
|
+
* (warm dbmon profile: store node machinery ~36% + GC ~29%). */
|
|
115
|
+
export declare const CONFIG_SLOT_NODE: number;
|
|
108
116
|
export declare const STATUS_NONE = 0;
|
|
109
117
|
export declare const STATUS_PENDING: number;
|
|
110
118
|
export declare const STATUS_ERROR: number;
|
|
@@ -54,6 +54,13 @@ export declare function setEffectStatusNotify(fn: NonNullable<typeof effectStatu
|
|
|
54
54
|
export declare function statusNotifierOf(el: any): ((this: any, status?: number, error?: any) => void) | undefined;
|
|
55
55
|
export declare function signal<T>(v: T, options?: NodeOptions<T>): Signal<T>;
|
|
56
56
|
export declare function signal<T>(v: T, options?: NodeOptions<T>, firewall?: Computed<any>): FirewallSignal<T>;
|
|
57
|
+
/** The shared slot-node unobserved handler — a live binding read directly by
|
|
58
|
+
* the sweep sites (no wrapper frame, no null check: a CONFIG_SLOT_NODE node
|
|
59
|
+
* existing implies the store module loaded and registered the hook). */
|
|
60
|
+
export declare let slotUnobservedHook: (node: Signal<any>) => void;
|
|
61
|
+
/** Install the shared slot-node unobserved handler (store module, once). */
|
|
62
|
+
export declare function setSlotUnobserved(fn: (node: Signal<any>) => void): void;
|
|
63
|
+
export declare function slotSignal<T>(v: T, equals: (a: T, b: T) => boolean, host: object, key: PropertyKey, acc: boolean, firewall?: Computed<unknown> | null): Signal<T>;
|
|
57
64
|
export declare function optimisticSignal<T>(v: T, options?: NodeOptions<T>): Signal<T>;
|
|
58
65
|
export declare function optimisticComputed<T>(fn: (prev?: T) => T | PromiseLike<T> | AsyncIterable<T>, options?: NodeOptions<T>): Computed<T>;
|
|
59
66
|
export declare function isEqual<T>(a: T, b: T): boolean;
|
|
@@ -115,9 +122,11 @@ export declare const READ_SLOW: unique symbol;
|
|
|
115
122
|
* installed at first until() call — apps that never use until() shake it.
|
|
116
123
|
*/
|
|
117
124
|
export declare function notifyAuthoritativeObservers(el: Signal<any> | Computed<any>): void;
|
|
118
|
-
/** Installs the
|
|
119
|
-
*
|
|
120
|
-
* optimistic engine
|
|
125
|
+
/** Installs the authoritative-reader wakeup hook. Idempotent; called by every
|
|
126
|
+
* creator of a CONFIG_AUTHORITATIVE_READ computation — until() and refresh() —
|
|
127
|
+
* before its first read (same late-binding contract as the optimistic engine;
|
|
128
|
+
* the gating bit is only ever set by such a read, so the `!` call sites are
|
|
129
|
+
* safe once every setter installs, #3303). */
|
|
121
130
|
export declare function installAuthoritativeRead(): void;
|
|
122
131
|
export declare function readNodeFast<T>(el: Signal<T>): T | typeof READ_SLOW;
|
|
123
132
|
export declare function read<T>(el: Signal<T> | Computed<T>): T;
|
package/dist/types/core/dev.d.ts
CHANGED
|
@@ -6,9 +6,17 @@ export interface DevHooks {
|
|
|
6
6
|
onUpdate?: () => void;
|
|
7
7
|
onStoreNodeUpdate?: (state: any, property: PropertyKey, value: any, prev: any) => void;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* `info` is the advisory tier: a structural fact worth surfacing that is not
|
|
11
|
+
* presumptively a bug (e.g. a 2-deep sequential fetch chain, which may be an
|
|
12
|
+
* intrinsic data dependency). Budget/assertion consumers should treat only
|
|
13
|
+
* `warn`/`error` as failures unless they opt in to `info`.
|
|
14
|
+
*/
|
|
15
|
+
export type DiagnosticSeverity = "info" | "warn" | "error";
|
|
16
|
+
export type DiagnosticCode = "STRICT_READ_UNTRACKED" | "PENDING_ASYNC_UNTRACKED_READ" | "PENDING_ASYNC_FORBIDDEN_SCOPE" | "REACTIVE_WRITE_IN_OWNED_SCOPE" | "ACTION_CALLED_IN_OWNED_SCOPE" | "RUN_WITH_DISPOSED_OWNER" | "NO_OWNER_CLEANUP" | "CLEANUP_IN_FORBIDDEN_SCOPE" | "SETTLED_CLEANUP_UNOWNED" | "SETTLE_WALK_UNINITIALIZED_SOURCE" | "FLUSH_IN_EFFECT_CALLBACK" | "PRIMITIVE_IN_FORBIDDEN_SCOPE" | "NO_OWNER_EFFECT" | "NO_OWNER_BOUNDARY" | "ASYNC_OUTSIDE_LOADING_BOUNDARY" | "INVALID_REFRESH_TARGET" | "INVALID_AFFECTS_TARGET" | "MISSING_EFFECT_FN" | "SYNC_NODE_RECEIVED_ASYNC" | "REACTIVITY_HALTED" | "INVARIANT_VIOLATION" | "HUGE_FAN_OUT" | "HUGE_FAN_IN" | "HOT_SCOPE_RERUNS" | "HOT_SCOPE_TIME" | "WIDE_SCOPE_DEPS" | "UNSTABLE_MEMO_OUTPUT" | "WIDE_WRITE" | "ASYNC_WATERFALL" | "HOT_SCOPE_FANOUT" | "SILENT_HOLD" | "LONG_HOLD" | "EFFECT_WRITES_OWN_SOURCE" | "EFFECT_RELAY_TEAR" | "IMMUTABLE_UPDATE_IN_STORE" | "UNSTABLE_LIST_IDENTITY";
|
|
17
|
+
export type DiagnosticKind = "strict-read" | "async" | "write" | "lifecycle" | "owner" | "error" | "perf" | "graph"
|
|
18
|
+
/** Perceived responsiveness: the runtime behaved correctly but the user saw no feedback. */
|
|
19
|
+
| "responsiveness";
|
|
12
20
|
/** First warning when a node's live edge count reaches this size. */
|
|
13
21
|
export declare const GRAPH_SIZE_WARN_AT = 2000;
|
|
14
22
|
/** Repeat the warning at this interval after the first. */
|
|
@@ -22,6 +30,15 @@ export interface DiagnosticEvent {
|
|
|
22
30
|
ownerId?: string;
|
|
23
31
|
ownerName?: string;
|
|
24
32
|
nodeName?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Root-first chain of named owners enclosing the subject of the event —
|
|
35
|
+
* component roots as `<Name>`, computations by their `name` option (or
|
|
36
|
+
* the `effect`/`computed` default) — e.g. `["<App>", "<TodoRow>", "effect"]`.
|
|
37
|
+
* Unnamed owners (plain roots) are skipped. Absent when the subject has no
|
|
38
|
+
* named owner at all (a top-level scope, or an unowned primitive — which
|
|
39
|
+
* is usually the finding itself).
|
|
40
|
+
*/
|
|
41
|
+
ownerPath?: string[];
|
|
25
42
|
data?: Record<string, unknown>;
|
|
26
43
|
}
|
|
27
44
|
export type DiagnosticListener = (event: DiagnosticEvent) => void;
|
|
@@ -34,10 +51,12 @@ export interface Diagnostics {
|
|
|
34
51
|
subscribe(listener: DiagnosticListener): () => void;
|
|
35
52
|
capture(): DiagnosticCapture;
|
|
36
53
|
/**
|
|
37
|
-
* Registers a console footer
|
|
54
|
+
* Registers a console footer appended to the first console report of
|
|
38
55
|
* each diagnostic code — a discovery pointer to deeper guidance (e.g.
|
|
39
|
-
* solid-js registers its shipped repair skill).
|
|
40
|
-
*
|
|
56
|
+
* solid-js registers its shipped repair skill). Reported events carry
|
|
57
|
+
* it as trailing lines of the same console entry; events that surface as
|
|
58
|
+
* a thrown error instead get it as a follow-up line. Returning undefined
|
|
59
|
+
* for an event suppresses the footer. Passing undefined unregisters and
|
|
41
60
|
* resets the once-per-code memory.
|
|
42
61
|
*/
|
|
43
62
|
setConsoleFooter(footer: ((event: DiagnosticEvent) => string | undefined) | undefined): void;
|
|
@@ -62,14 +81,42 @@ export declare const DEV: Dev;
|
|
|
62
81
|
* dev builds so user apps degrade instead of crashing.
|
|
63
82
|
*/
|
|
64
83
|
export declare function assertInvariant(condition: boolean, name: string, message: string): void;
|
|
65
|
-
|
|
84
|
+
/** Anything a diagnostic can be about: an owner (root, computed, effect) or a signal. */
|
|
85
|
+
export type DiagnosticSubject = Owner | Signal<any> | Computed<any>;
|
|
86
|
+
/**
|
|
87
|
+
* Root-first names of the owners enclosing `subject` (inclusive when the
|
|
88
|
+
* subject is itself a named owner). Signals hop to their registering owner
|
|
89
|
+
* (`_owner`, set by registerGraph). Unnamed owners are skipped so the path
|
|
90
|
+
* reads as the component tree plus the scope: `<App> › <TodoRow> › effect`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function ownerPath(subject: DiagnosticSubject | null | undefined): string[] | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Records a diagnostic on the structured channel (listeners, captures) and
|
|
95
|
+
* returns the entry. `subject` locates it: the current reactive `context` by
|
|
96
|
+
* default (right for the synchronous rule checks — they fire inside the
|
|
97
|
+
* scope that misbehaved); pass the node for scheduler-time findings whose
|
|
98
|
+
* ambient context is the flush, or `null` for events that have no location
|
|
99
|
+
* by nature. Console output is a separate step — see `reportDiagnostic`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence" | "ownerPath">, subject?: DiagnosticSubject | null | undefined): DiagnosticEvent;
|
|
102
|
+
/**
|
|
103
|
+
* The console face of a diagnostic — ONE entry per finding: the message, the
|
|
104
|
+
* owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
|
|
105
|
+
* once-per-code footer as trailing lines, and — when the subject is a
|
|
106
|
+
* binding effect the rendering runtime tagged — the element it writes, as a
|
|
107
|
+
* second console argument (hover highlights it, click jumps to Elements).
|
|
108
|
+
* Severity picks the console method. Call sites report the entry
|
|
109
|
+
* `emitDiagnostic` returned so the structured and console channels never
|
|
110
|
+
* disagree.
|
|
111
|
+
*/
|
|
112
|
+
export declare function reportDiagnostic(entry: DiagnosticEvent): void;
|
|
66
113
|
/**
|
|
67
114
|
* Shared strict-read diagnostics for core read() and the store proxy traps.
|
|
68
115
|
* Single source for the message text — the #2897 safeguard parity between
|
|
69
116
|
* memos and stores is exactly these firing identically from both paths.
|
|
70
117
|
*/
|
|
71
|
-
export declare function throwPendingUntrackedRead(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "data">>): never;
|
|
72
|
-
export declare function warnStrictReadUntracked(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence">>): void;
|
|
118
|
+
export declare function throwPendingUntrackedRead(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "data" | "ownerPath">>): never;
|
|
119
|
+
export declare function warnStrictReadUntracked(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "ownerPath">>): void;
|
|
73
120
|
export declare function registerGraph(value: any, owner: Owner | null): void;
|
|
74
121
|
export declare function clearSignals(node: Owner): void;
|
|
75
122
|
export declare function getChildren(owner: Owner): Owner[];
|
|
@@ -2,9 +2,11 @@ import type { Computed } from "./types.js";
|
|
|
2
2
|
/** The queue a node belongs to, picked from its own zombie flag. */
|
|
3
3
|
export declare function queueFor(n: Computed<any>): Heap;
|
|
4
4
|
/**
|
|
5
|
-
* Schedule one subscriber to re-run on the next flush:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Schedule one subscriber to re-run on the next flush: inserted into its own
|
|
6
|
+
* (zombie-flag-routed) heap with the `_min` cursor pulled down. Tracked
|
|
7
|
+
* effects ride the heap too — the heap visit is their (empty) compute phase,
|
|
8
|
+
* which hands the callback to the user queue once the pass has committed
|
|
9
|
+
* (see GlobalQueue._update, #3291).
|
|
8
10
|
*/
|
|
9
11
|
export declare function enqueueSub(node: Computed<any>): void;
|
|
10
12
|
export interface Heap {
|
|
@@ -2,7 +2,7 @@ import type { OptimisticLane } from "./lanes.js";
|
|
|
2
2
|
import type { Computed, Signal } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Test-mode invariant checks for the async/transition/lane machinery.
|
|
5
|
-
* Catalog and rationale: packages/signals/INTERNALS-ASYNC-STATE.md.
|
|
5
|
+
* Catalog and rationale: packages/signals/docs/INTERNALS-ASYNC-STATE.md.
|
|
6
6
|
*
|
|
7
7
|
* These are implementation self-consistency checks, not semantic rules: a
|
|
8
8
|
* violation means the reactive system contradicted itself.
|