@solidjs/signals 2.0.0-rc.7 → 2.0.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/dev-shared.js +5243 -0
  2. package/dist/dev.attribution.js +2333 -0
  3. package/dist/dev.js +1229 -6834
  4. package/dist/observe/affects.js +127 -0
  5. package/dist/observe/attribution.js +1 -0
  6. package/dist/observe/boundaries.js +586 -0
  7. package/dist/observe/core/action.js +167 -0
  8. package/dist/observe/core/async.js +736 -0
  9. package/dist/observe/core/attribution-hooks.js +60 -0
  10. package/dist/observe/core/attribution.js +2318 -0
  11. package/dist/{types-cjs/core/constants.d.cts → observe/core/constants.js} +129 -73
  12. package/dist/{types-cjs/core/context.d.cts → observe/core/context.js} +37 -12
  13. package/dist/observe/core/core.js +1289 -0
  14. package/dist/observe/core/dev.js +232 -0
  15. package/dist/observe/core/effect.js +179 -0
  16. package/dist/observe/core/error.js +80 -0
  17. package/dist/observe/core/external.js +98 -0
  18. package/dist/observe/core/graph.js +155 -0
  19. package/dist/observe/core/heap.js +147 -0
  20. package/dist/observe/core/invariants.js +43 -0
  21. package/dist/observe/core/lanes.js +168 -0
  22. package/dist/observe/core/optimistic.js +438 -0
  23. package/dist/observe/core/owner.js +308 -0
  24. package/dist/observe/core/scheduler.js +1190 -0
  25. package/dist/observe/core/verdict.js +551 -0
  26. package/dist/observe/index.js +55 -0
  27. package/dist/observe/map.js +482 -0
  28. package/dist/{types-cjs/signals.d.cts → observe/signals.js} +295 -317
  29. package/dist/observe/store/index.js +38 -0
  30. package/dist/observe/store/next/optimistic.js +651 -0
  31. package/dist/observe/store/next/projection.js +242 -0
  32. package/dist/observe/store/next/reconcile.js +373 -0
  33. package/dist/observe/store/next/store.js +2213 -0
  34. package/dist/observe/store/next/target.js +51 -0
  35. package/dist/observe/store/store.js +323 -0
  36. package/dist/observe/store/storePath.js +103 -0
  37. package/dist/observe/store/utils.js +210 -0
  38. package/dist/prod/attribution.js +32 -0
  39. package/dist/prod/core/action.js +52 -32
  40. package/dist/prod/core/async.js +171 -127
  41. package/dist/prod/core/constants.js +24 -1
  42. package/dist/prod/core/context.js +3 -3
  43. package/dist/prod/core/core.js +434 -291
  44. package/dist/prod/core/effect.js +54 -34
  45. package/dist/prod/core/external.js +2 -2
  46. package/dist/prod/core/graph.js +32 -32
  47. package/dist/prod/core/heap.js +49 -44
  48. package/dist/prod/core/lanes.js +45 -38
  49. package/dist/prod/core/optimistic.js +207 -73
  50. package/dist/prod/core/owner.js +43 -39
  51. package/dist/prod/core/scheduler.js +319 -190
  52. package/dist/prod/core/verdict.js +67 -67
  53. package/dist/prod/index.js +9 -3
  54. package/dist/prod/map.js +292 -141
  55. package/dist/prod/signals.js +7 -10
  56. package/dist/prod/store/next/optimistic.js +140 -125
  57. package/dist/prod/store/next/projection.js +3 -3
  58. package/dist/prod/store/next/reconcile.js +87 -84
  59. package/dist/prod/store/next/store.js +524 -280
  60. package/dist/prod/store/next/target.js +32 -10
  61. package/dist/prod/store/store.js +12 -14
  62. package/dist/prod/store/utils.js +36 -27
  63. package/dist/types/attribution.d.ts +15 -0
  64. package/dist/types/attribution.prod.d.ts +13 -0
  65. package/dist/types/core/async.d.ts +1 -1
  66. package/dist/types/core/attribution-hooks.d.ts +119 -6
  67. package/dist/types/core/attribution.d.ts +208 -53
  68. package/dist/types/core/constants.d.ts +23 -0
  69. package/dist/types/core/core.d.ts +7 -0
  70. package/dist/types/core/dev.d.ts +124 -26
  71. package/dist/types/core/effect.d.ts +8 -1
  72. package/dist/types/core/index.d.ts +2 -1
  73. package/dist/types/core/lanes.d.ts +8 -1
  74. package/dist/types/core/scheduler.d.ts +60 -0
  75. package/dist/types/core/types.d.ts +29 -9
  76. package/dist/types/index.d.ts +9 -2
  77. package/dist/types/map.d.ts +2 -0
  78. package/dist/types/store/index.d.ts +1 -0
  79. package/dist/types/store/next/store.d.ts +5 -0
  80. package/dist/types/store/next/target.d.ts +36 -9
  81. package/dist/types/store/utils.d.ts +7 -0
  82. package/package.json +21 -17
  83. package/dist/node.cjs +0 -10541
  84. package/dist/node.dev.cjs +0 -13724
  85. package/dist/types-cjs/affects.d.cts +0 -47
  86. package/dist/types-cjs/boundaries.d.cts +0 -175
  87. package/dist/types-cjs/core/action.d.cts +0 -64
  88. package/dist/types-cjs/core/async.d.cts +0 -23
  89. package/dist/types-cjs/core/attribution-hooks.d.cts +0 -127
  90. package/dist/types-cjs/core/attribution.d.cts +0 -495
  91. package/dist/types-cjs/core/core.d.cts +0 -185
  92. package/dist/types-cjs/core/dev.d.cts +0 -136
  93. package/dist/types-cjs/core/effect.d.cts +0 -30
  94. package/dist/types-cjs/core/error.d.cts +0 -56
  95. package/dist/types-cjs/core/external.d.cts +0 -15
  96. package/dist/types-cjs/core/graph.d.cts +0 -28
  97. package/dist/types-cjs/core/heap.d.cts +0 -24
  98. package/dist/types-cjs/core/index.d.cts +0 -13
  99. package/dist/types-cjs/core/invariants.d.cts +0 -59
  100. package/dist/types-cjs/core/lanes.d.cts +0 -54
  101. package/dist/types-cjs/core/optimistic.d.cts +0 -6
  102. package/dist/types-cjs/core/owner.d.cts +0 -124
  103. package/dist/types-cjs/core/scheduler.d.cts +0 -236
  104. package/dist/types-cjs/core/types.d.cts +0 -204
  105. package/dist/types-cjs/core/verdict.d.cts +0 -2
  106. package/dist/types-cjs/index.d.cts +0 -10
  107. package/dist/types-cjs/map.d.cts +0 -69
  108. package/dist/types-cjs/package.json +0 -3
  109. package/dist/types-cjs/store/index.d.cts +0 -18
  110. package/dist/types-cjs/store/next/optimistic.d.cts +0 -25
  111. package/dist/types-cjs/store/next/projection.d.cts +0 -8
  112. package/dist/types-cjs/store/next/reconcile.d.cts +0 -10
  113. package/dist/types-cjs/store/next/store.d.cts +0 -121
  114. package/dist/types-cjs/store/next/target.d.cts +0 -166
  115. package/dist/types-cjs/store/store.d.cts +0 -143
  116. package/dist/types-cjs/store/storePath.d.cts +0 -58
  117. package/dist/types-cjs/store/utils.d.cts +0 -74
@@ -1,6 +1,7 @@
1
1
  import type { Computed } from "./types.js";
2
2
  /**
3
- * Dev-mode "why did this run" attribution.
3
+ * "Why did this run" attribution — the engine behind
4
+ * `@solidjs/signals/attribution`.
4
5
  *
5
6
  * The runtime already knows the full dependency set of every scope; this
6
7
  * module surfaces it. Every value commit stamps its node with a ChangeRecord
@@ -15,11 +16,13 @@ import type { Computed } from "./types.js";
15
16
  *
16
17
  * This module is the attribution ENGINE: all semantics live here, and it is
17
18
  * decoupled from the core. `enable()` installs it into the core's narrow
18
- * dev-only hook points (attribution-hooks.ts); core's only obligation is to
19
- * call those hooks with true facts. Disabled cost is one null check per hook
20
- * site; prod builds fold the sites out entirely. The same hook surface is the
21
- * intended substrate for external consumers (devtools) one mechanism, two
22
- * front-ends.
19
+ * observe-tier hook points (attribution-hooks.ts); core's only obligation is
20
+ * to call those hooks with true facts. Disabled cost is one null check per
21
+ * hook site; prod builds fold the sites out entirely. Nothing in the core
22
+ * imports this module it is reachable only through the package's
23
+ * `./attribution` entry, so an observe build that never imports it never
24
+ * ships it. The same hook surface is the intended substrate for external
25
+ * consumers (devtools) — one mechanism, two front-ends.
23
26
  */
24
27
  export type ChangeKind = "write" | "derived" | "async" | "refresh";
25
28
  /**
@@ -36,30 +39,32 @@ export type ChangeKind = "write" | "derived" | "async" | "refresh";
36
39
  * name, when it has one). Writes after an `await` (not a `yield`) run in a
37
40
  * bare microtask and stamp `external` — the documented escape.
38
41
  * - `async` — an async landing (`name` = the node whose flight landed).
42
+ * - `navigation` — a router's navigation, declared via `withOrigin` around
43
+ * the location write (`name` = the matched route pattern `/users/:id`;
44
+ * `to`/`from` the concrete paths; `params` what the pattern bound; `at`
45
+ * when it was requested). The router-agnostic seam: any router that wraps
46
+ * its write gets navigations named by route in every hold, re-run and
47
+ * verdict, with no per-router knowledge anywhere in the engine.
39
48
  * - `external` — none of the above: timers, sockets, promise callbacks, setup.
40
49
  *
41
50
  * `interaction` on a non-interaction frame is the user event the frame runs
42
51
  * 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.
52
+ * click's write, a landing whose flight a click started, a navigation a link
53
+ * click performed. It is what lets every downstream cost be keyed by the
54
+ * interaction that paid for it.
45
55
  */
46
56
  export interface ChangeOrigin {
47
- kind: "interaction" | "effect" | "action" | "async" | "external";
57
+ kind: "interaction" | "effect" | "action" | "async" | "navigation" | "external";
48
58
  name?: string;
49
59
  target?: string;
50
60
  at?: number;
51
61
  interaction?: ChangeOrigin;
52
62
  /** `effect` only: the `RerunEvent.run` of the compute run this callback belongs to. */
53
63
  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;
64
+ /** `navigation` only: concrete destination and departure paths, and the bound params. */
65
+ to?: string;
66
+ from?: string;
67
+ params?: Readonly<Record<string, string | undefined>>;
63
68
  }
64
69
  export interface ChangeRecord {
65
70
  /** Global monotonic change sequence — orders causes across the app. */
@@ -81,6 +86,8 @@ export interface ChangeRecord {
81
86
  export interface RerunEvent {
82
87
  /** Global monotonic run sequence. */
83
88
  run: number;
89
+ /** When the run started (`performance.now()` clock). */
90
+ at: number;
84
91
  /** How many times this node has re-run since attribution was enabled. */
85
92
  nodeRuns: number;
86
93
  nodeKind: "effect" | "memo";
@@ -178,14 +185,11 @@ export interface AttributionOptions {
178
185
  /**
179
186
  * Written-fan-out warning: emit a diagnostic when a committed root
180
187
  * invalidation (write, refresh, async landing) reaches a node with at
181
- * least this many subscribers (default 250). Complements the always-on
182
- * HUGE_FAN_OUT graph-size warning, specced against it deliberately:
183
- * HUGE_FAN_OUT fires at LINK time from GRAPH_SIZE_WARN_AT (2000) up
184
- * static structure so large it warns even if never written while this
185
- * fires at WRITE time from a much lower bar, because fan-out only costs
186
- * anything when the node actually changes. Once per node, re-warning only
187
- * on 2x subscriber growth, so the two never spam the same node. `false`
188
- * disables.
188
+ * least this many subscribers (default 250). The lower-bar, opt-in sibling
189
+ * of the always-on HUGE_FAN_OUT graph-size warning, which fires on the
190
+ * same kind of write from GRAPH_SIZE_WARN_AT (2000) up; this one hands
191
+ * over to it there, so a write never carries both. Once per node,
192
+ * re-warning only on 2x subscriber growth. `false` disables.
189
193
  */
190
194
  wideWrites?: number | false;
191
195
  /**
@@ -243,6 +247,19 @@ export interface AttributionOptions {
243
247
  warnMs: number;
244
248
  } | false;
245
249
  }
250
+ /**
251
+ * The records the engine delivers, by `subscribe(type, …)` name. Every one is
252
+ * delivered synchronously at the moment it is complete — a re-run when its
253
+ * recompute ends, an interaction / hold / navigation when it settles — so a
254
+ * consumer never polls the ring buffers to learn that something finished.
255
+ */
256
+ export interface AttributionRecords {
257
+ rerun: RerunEvent;
258
+ interaction: InteractionEvent;
259
+ hold: HoldEvent;
260
+ navigation: NavigationEvent;
261
+ }
262
+ export type AttributionRecordType = keyof AttributionRecords;
246
263
  export interface ScopeCost {
247
264
  name: string;
248
265
  kind: "effect" | "memo";
@@ -266,20 +283,21 @@ export interface WriteCost {
266
283
  /** Summed self-time of every downstream re-run it caused. */
267
284
  downstreamMs: number;
268
285
  }
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"`, … */
286
+ /** `click on button#next "Next →"`, `effect "syncTitle"`, `action "save"`, `navigation to /users/:id`, … */
277
287
  export declare function formatOrigin(origin: ChangeOrigin): string;
278
288
  export declare function formatRerun(event: RerunEvent): string;
279
289
  export interface Attribution {
280
290
  enable(opts?: AttributionOptions): void;
281
291
  disable(): void;
292
+ /**
293
+ * Deliver records as they complete — see `AttributionRecords`. The bare
294
+ * form is `subscribe("rerun", …)`. Records are the same objects the ring
295
+ * buffers hold (`history()`, `interactions()`, `holds()`, `navigations()`),
296
+ * delivered synchronously from the engine, so a listener must not write
297
+ * signals. All subscriptions are dropped by `disable()`.
298
+ */
282
299
  subscribe(listener: (event: RerunEvent) => void): () => void;
300
+ subscribe<K extends AttributionRecordType>(type: K, listener: (record: AttributionRecords[K]) => void): () => void;
283
301
  history(): readonly RerunEvent[];
284
302
  /** Re-run history for one node — pass a memo/effect accessor or raw node. */
285
303
  why(target: unknown): RerunEvent[];
@@ -308,6 +326,26 @@ export interface Attribution {
308
326
  * the thresholded, unacknowledged subset.
309
327
  */
310
328
  holds(): readonly HoldEvent[];
329
+ /**
330
+ * Every navigation a router declared via `withOrigin` since enable()
331
+ * (ring-buffered like history()), settled or not: what route, under which
332
+ * interaction, how many writes, and — once its writes are through — how
333
+ * long that took and how (`committed` in a plain drain, `held` behind
334
+ * route data with the HoldEvent attached, or `superseded` by a later
335
+ * navigation before it landed). Facts for any consumer that wants
336
+ * navigation spans named by route: no router integration needed.
337
+ */
338
+ navigations(): readonly NavigationEvent[];
339
+ /**
340
+ * Every user interaction a runtime declared via `withInteraction` since
341
+ * enable() (ring-buffered like history()), settled or not: what was
342
+ * dispatched, when, what it wrote, the re-runs and creations it caused,
343
+ * the holds its writes waited in and the navigations it performed — and,
344
+ * once all of that is through, how long the person waited (`settledMs`)
345
+ * and how it ended (`idle` / `committed` / `held`). The per-dispatch
346
+ * record `feedback().interactions` folds by name.
347
+ */
348
+ interactions(): readonly InteractionEvent[];
311
349
  /**
312
350
  * What the user waited on, folded from holds() and the interaction on each
313
351
  * re-run: `sources` ranks async sources by the silent time writes spent
@@ -315,10 +353,12 @@ export interface Attribution {
315
353
  * interactions were held); `interactions` ranks user events by the total
316
354
  * time they cost — re-run work caused (long-flush hazard) beside time held
317
355
  * (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.
356
+ * thresholded verdict. Three more tables round out the picture:
357
+ * `navigations` ranks routes by the time spent held navigating to them
358
+ * (folded from navigations()), `flights` counts each async source's
359
+ * flights and how many were abandoned before landing (the re-ask storm),
360
+ * and `fallbacks` measures how long each loading boundary showed its
361
+ * fallback and how often that was a flash.
322
362
  */
323
363
  feedback(): AttributionFeedbackTables;
324
364
  /**
@@ -330,16 +370,9 @@ export interface Attribution {
330
370
  * is then judged against the real start — work already in the air when its
331
371
  * upstream landed is parallel, never a waterfall link. Callable while
332
372
  * attribution is disabled (marks made at navigation time must survive a
333
- * later enable()). Dev-only, like the whole DEV surface.
373
+ * later enable()).
334
374
  */
335
375
  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;
343
376
  format: typeof formatRerun;
344
377
  formatOrigin: typeof formatOrigin;
345
378
  }
@@ -362,10 +395,13 @@ export interface HeldWrite {
362
395
  }
363
396
  export interface HoldEvent {
364
397
  /**
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.
398
+ * When the wait began (`performance.now()` clock): the interaction that
399
+ * performed the held writes when one is known (`interaction.at`) or the
400
+ * first flush that parked them, whichever is earlier. `at + holdMs` is the
401
+ * commit.
368
402
  */
403
+ at: number;
404
+ /** Wall time the user waited: `at` to the commit. */
369
405
  holdMs: number;
370
406
  /**
371
407
  * The quiescent tail: from the LAST held write to join (the user's final
@@ -375,6 +411,13 @@ export interface HoldEvent {
375
411
  tailMs: number;
376
412
  /** The user interaction whose writes were held, when the stamp is known. */
377
413
  interaction?: ChangeOrigin;
414
+ /**
415
+ * The declared unit of work the held writes belong to — the `navigation`
416
+ * a router described via `withOrigin` — when one is known. What names the
417
+ * hold by route (`navigation to /users/:id`) rather than by signal; the
418
+ * same object as `navigations()[].origin`, so the two join by identity.
419
+ */
420
+ origin?: ChangeOrigin;
378
421
  /** Flushes that ended with the hold still open. */
379
422
  flushes: number;
380
423
  /** Root signal writes staged behind the hold (the user's unanswered input). */
@@ -382,11 +425,12 @@ export interface HoldEvent {
382
425
  /** Async nodes the hold waited on (union across its parked flushes). */
383
426
  blockers: string[];
384
427
  /**
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.
428
+ * Feedback the graph provably rendered for this hold an `isPending()`
429
+ * reader, a `latest()` shadow, an optimistic value, an `affects()` mark —
430
+ * one entry per affordance, in the order found. Empty and
431
+ * `paintedDuringHold === 0` is the SILENT_HOLD signature.
388
432
  */
389
- acknowledgedBy: string[];
433
+ acknowledgements: Acknowledgement[];
390
434
  /**
391
435
  * Effect callbacks that ran inside the hold's parked flushes. Mainline
392
436
  * effects are stashed while a hold is open, so these are lane effects —
@@ -398,6 +442,93 @@ export interface HoldEvent {
398
442
  /** The hold was opened (or joined) by an `action()`. */
399
443
  action: boolean;
400
444
  }
445
+ /**
446
+ * One way the screen acknowledged a hold. `reader` is where it was painted —
447
+ * the owner path of the first effect the census found reading the
448
+ * affordance (`["<App>", "<Feed>", "effect"]`), so a consumer can say WHICH
449
+ * screen answered, not only that one did. Absent when the affordance was
450
+ * registered but the census found no reader through the graph (an optimistic
451
+ * store: its readers are proxy traps, not nodes). `feedback()` ranks
452
+ * acknowledgements by `kind:source` (`isPending:posts`).
453
+ */
454
+ export interface Acknowledgement {
455
+ kind: "isPending" | "latest" | "optimistic" | "affects";
456
+ /** The node the affordance hangs on — the async source for `isPending`/`latest`, the optimistic/affected node otherwise. */
457
+ source: string;
458
+ reader?: string[];
459
+ }
460
+ /** A destination a navigation abandoned when a redirect sent it elsewhere. */
461
+ export interface NavigationHop {
462
+ name?: string;
463
+ to?: string;
464
+ params?: Readonly<Record<string, string | undefined>>;
465
+ /** When the redirect away from it was declared (`performance.now()` clock). */
466
+ at: number;
467
+ }
468
+ export interface NavigationEvent {
469
+ /** The matched route pattern the router gave — `/users/:id`. After a redirect, the final one. */
470
+ name?: string;
471
+ to?: string;
472
+ from?: string;
473
+ params?: Readonly<Record<string, string | undefined>>;
474
+ /** When the navigation was requested (`performance.now()` clock). */
475
+ at: number;
476
+ /** The user interaction it ran under, when known — a link click. */
477
+ interaction?: ChangeOrigin;
478
+ /** Root writes the frame performed, redirect hops included. */
479
+ writes: number;
480
+ /** Destinations abandoned along the way, in order — present only when a redirect occurred. */
481
+ redirects?: NavigationHop[];
482
+ /**
483
+ * Wall time from the request to settle: the end of the drain that committed
484
+ * its writes, or the commit of the hold they waited in. `undefined` while
485
+ * unsettled.
486
+ */
487
+ settledMs?: number;
488
+ outcome?: "committed" | "held" | "superseded";
489
+ /** The hold its writes waited in, when hold tracking recorded one. */
490
+ hold?: HoldEvent;
491
+ /**
492
+ * The frame object its writes were stamped with — `ChangeRecord.origin` on
493
+ * each, `HoldEvent.origin` on the hold. Join key, by identity.
494
+ */
495
+ origin: ChangeOrigin;
496
+ }
497
+ export interface InteractionEvent {
498
+ /** Event type — `click`, `keydown`, `input`… */
499
+ name: string;
500
+ /** The element hit, as the runtime described it — `button#next "Next →"`. */
501
+ target?: string;
502
+ /** Dispatch time (`performance.now()` clock). */
503
+ at: number;
504
+ /** Wall time of the handler itself, dispatch to return. */
505
+ handlerMs: number;
506
+ /** Root writes attributed to the frame: the handler's, and those of frames it opened (a navigation). */
507
+ writes: number;
508
+ /** Re-runs traced back to this interaction while the record was open. */
509
+ runs: number;
510
+ /** Computations created in those runs or in the frame's flushes (the "create 1,000 rows" work). */
511
+ created: number;
512
+ /** Summed self-time of `runs` and `created` (ms). Quantized per run; `settledMs` is the wall clock. */
513
+ runMs: number;
514
+ /** Holds its writes waited in, in settle order. */
515
+ holds: HoldEvent[];
516
+ /** Navigations performed under it, in open order. */
517
+ navigations: NavigationEvent[];
518
+ /**
519
+ * Dispatch to settle: the handler's return when it wrote nothing, the end of
520
+ * the drain that committed its writes, or the commit of the last hold they
521
+ * waited in — whichever came last. `undefined` while unsettled.
522
+ */
523
+ settledMs?: number;
524
+ outcome?: "idle" | "committed" | "held";
525
+ /**
526
+ * The frame object every downstream fact carries — `ChangeOrigin.interaction`
527
+ * on writes and frames, `RerunEvent.interaction`, `HoldEvent.interaction`,
528
+ * `NavigationEvent.interaction`. Join key, by identity.
529
+ */
530
+ origin: ChangeOrigin;
531
+ }
401
532
  export interface FeedbackSource {
402
533
  /** The async nodes the holds waited on; empty when an action alone kept them open. */
403
534
  sources: string[];
@@ -484,9 +615,33 @@ export interface FallbackStats {
484
615
  */
485
616
  flashes: number;
486
617
  }
618
+ /**
619
+ * Per route: what navigating to it cost, folded from settled
620
+ * `NavigationEvent`s — the route-level view a router integration used to
621
+ * have to build itself, from the runtime's own facts.
622
+ */
623
+ export interface FeedbackNavigation {
624
+ /** The route pattern (`/users/:id`), or the concrete `to` when the router gave no pattern. */
625
+ name: string;
626
+ navigations: number;
627
+ /** Summed and worst request-to-settle time (ms) across settled navigations. */
628
+ settledMs: number;
629
+ worstMs: number;
630
+ /** Navigations whose writes waited in a hold, and the time they waited. */
631
+ held: number;
632
+ heldMs: number;
633
+ /** Held navigations the screen acknowledged nothing for — the SILENT_HOLD signature. */
634
+ silent: number;
635
+ /** Navigations overwritten by a later one before they landed. */
636
+ superseded: number;
637
+ /** Navigations a redirect sent elsewhere on the way (keyed by where they ended up). */
638
+ redirected: number;
639
+ }
487
640
  export interface AttributionFeedbackTables {
488
641
  sources: FeedbackSource[];
489
642
  interactions: FeedbackInteraction[];
643
+ /** Routes ranked by the time spent held navigating to them, then by total settle time. */
644
+ navigations: FeedbackNavigation[];
490
645
  /** Async sources ranked by abandoned flights, then by flights. */
491
646
  flights: FlightStats[];
492
647
  /** Loading boundaries ranked by flashes, then by time shown. */
@@ -113,6 +113,26 @@ export declare const CONFIG_HELD_TRUTH: number;
113
113
  * unobserved closure, NodeExtension) were the measured create-floor bytes
114
114
  * (warm dbmon profile: store node machinery ~36% + GC ~29%). */
115
115
  export declare const CONFIG_SLOT_NODE: number;
116
+ /** Optimistic node whose own source arrived with a value DIFFERENT from its
117
+ * active override (A18 supersession, #3331). The override survives only as
118
+ * the displayed value — untracked reads and the applied frame keep it until
119
+ * the owning transaction commits — while the graph has already moved to the
120
+ * staged truth in `_pendingValue`: tracked readers see it and the corrected
121
+ * cascade is that transaction's held work. Set by the two own-source write
122
+ * paths (asyncWrite, transition-held recompute); cleared by a fresh optimistic
123
+ * write (a new override re-masks) and by the revert. */
124
+ export declare const CONFIG_OVERRIDE_SUPERSEDED: number;
125
+ /** In-flight async node whose inputs were PUBLISHED while it was pending: a
126
+ * batch or transaction committed with the node still `STATUS_PENDING` (an
127
+ * unobserved flight, #3305), so the inputs are on screen and the node's
128
+ * committed `_value` is stale against them. Governs read()'s reveal
129
+ * carve-out: a stale (render) reader in some OTHER transaction may show a
130
+ * foreign-held pending node's committed value — parallel transactions, no
131
+ * entanglement — only while that value is coherent with the visible frame,
132
+ * i.e. while the flight's inputs are themselves held (unpublished) and not
133
+ * lane-revealed. Set by `commitPendingNodes`; cleared when the node next
134
+ * enters pending fresh (a new flight from a settled state). */
135
+ export declare const CONFIG_INPUTS_PUBLISHED: number;
116
136
  export declare const STATUS_NONE = 0;
117
137
  export declare const STATUS_PENDING: number;
118
138
  export declare const STATUS_ERROR: number;
@@ -121,6 +141,9 @@ export declare const EFFECT_PURE = 0;
121
141
  export declare const EFFECT_RENDER = 1;
122
142
  export declare const EFFECT_USER = 2;
123
143
  export declare const EFFECT_TRACKED = 3;
144
+ /** OR-ed into the `type` a lane passes to its effect runners: lane runs
145
+ * apply ahead of their transaction and are exempt from ownership parking. */
146
+ export declare const LANE_RUN = 4;
124
147
  export declare const NOT_PENDING: {};
125
148
  export declare const NO_SNAPSHOT: {};
126
149
  /**
@@ -60,6 +60,13 @@ export declare function signal<T>(v: T, options?: NodeOptions<T>, firewall?: Com
60
60
  export declare let slotUnobservedHook: (node: Signal<any>) => void;
61
61
  /** Install the shared slot-node unobserved handler (store module, once). */
62
62
  export declare function setSlotUnobserved(fn: (node: Signal<any>) => void): void;
63
+ /** Release a firewall child the store no longer addresses (unobserved sweep
64
+ * dropped it from its target's cache): unlink it from the chain so the
65
+ * projection stops retaining it and its last value. The node keeps its own
66
+ * `_nextChild` so a walk that is mid-chain on it still terminates. Nodes in
67
+ * `_companionChildren` stay there — companions are permanent by contract
68
+ * and snap through that set, not the chain. */
69
+ export declare function unlinkFirewallChild(node: Signal<any>): void;
63
70
  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>;
64
71
  export declare function optimisticSignal<T>(v: T, options?: NodeOptions<T>): Signal<T>;
65
72
  export declare function optimisticComputed<T>(fn: (prev?: T) => T | PromiseLike<T> | AsyncIterable<T>, options?: NodeOptions<T>): Computed<T>;
@@ -1,5 +1,5 @@
1
- import { type Attribution } from "./attribution.js";
2
- import type { Computed, Link, Owner, Signal } from "./types.js";
1
+ import { type AttributionHooks, type InteractionRef, type OriginRef } from "./attribution-hooks.js";
2
+ import type { Computed, Owner, Signal } from "./types.js";
3
3
  export interface DevHooks {
4
4
  onOwner?: (owner: Owner) => void;
5
5
  onGraph?: (value: any, owner: Owner | null) => void;
@@ -17,9 +17,9 @@ export type DiagnosticCode = "STRICT_READ_UNTRACKED" | "PENDING_ASYNC_UNTRACKED_
17
17
  export type DiagnosticKind = "strict-read" | "async" | "write" | "lifecycle" | "owner" | "error" | "perf" | "graph"
18
18
  /** Perceived responsiveness: the runtime behaved correctly but the user saw no feedback. */
19
19
  | "responsiveness";
20
- /** First warning when a node's live edge count reaches this size. */
20
+ /** First warning when a change reaches (or a pass tracks) this many edges. */
21
21
  export declare const GRAPH_SIZE_WARN_AT = 2000;
22
- /** Repeat the warning at this interval after the first. */
22
+ /** Re-warn once the count has grown by this much since the last warning. */
23
23
  export declare const GRAPH_SIZE_WARN_EVERY = 500;
24
24
  export interface DiagnosticEvent {
25
25
  sequence: number;
@@ -51,27 +51,109 @@ export interface Diagnostics {
51
51
  subscribe(listener: DiagnosticListener): () => void;
52
52
  capture(): DiagnosticCapture;
53
53
  /**
54
- * Registers a console footer appended to the first console report of
55
- * each diagnostic code a discovery pointer to deeper guidance (e.g.
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
60
- * resets the once-per-code memory.
54
+ * Records an event on the channel from outside the reactive core a host
55
+ * runtime reporting its own findings (hydration mismatches, server render
56
+ * faults) so consumers see one stream. `subject` locates it like the
57
+ * internal sites do; a host whose owners are not signals' owners passes
58
+ * `ownerPath` on the event instead and it is used as-is.
61
59
  */
62
- setConsoleFooter(footer: ((event: DiagnosticEvent) => string | undefined) | undefined): void;
60
+ emit(event: Omit<DiagnosticEvent, "sequence">, subject?: DiagnosticSubject | null): DiagnosticEvent;
61
+ }
62
+ /**
63
+ * The core's side of attribution: the hook slot an engine installs into, and
64
+ * the interaction frame the rendering runtime opens around event dispatch.
65
+ * The engine itself — "why did this run", costs, holds, feedback — is
66
+ * `@solidjs/signals/attribution`, a separate entry so an observe build pays
67
+ * for it only when something imports it.
68
+ */
69
+ export interface AttributionSlot {
70
+ /**
71
+ * Installs `hooks` as the engine the core reports facts to (`null`
72
+ * uninstalls). One engine at a time; the built-in engine's `enable()` calls
73
+ * this, and an external consumer (devtools) may install its own instead.
74
+ */
75
+ install(hooks: AttributionHooks | null): void;
76
+ /** The installed engine's hooks, or `null` when none is installed. */
77
+ readonly installed: AttributionHooks | null;
78
+ /**
79
+ * Run `fn` as a user interaction's handler: root writes inside stamp it as
80
+ * their origin, and actions/effects/flights it causes carry it. The web
81
+ * runtime wraps every event dispatch in this; custom renderers and test
82
+ * harnesses call it themselves. `fn()` when no engine is installed.
83
+ */
84
+ withInteraction<T>(ref: InteractionRef, fn: () => T): T;
85
+ /**
86
+ * Run `fn` as a declared unit of work — a router's navigation, described
87
+ * by the parametrized route it matched: root writes inside are attributed
88
+ * to it (under the enclosing interaction, if any), so the hold behind the
89
+ * route's data, the re-runs and the verdicts carry the route's name. Any
90
+ * router calls this around its location write; nothing else is
91
+ * router-specific. `fn()` when no engine is installed.
92
+ */
93
+ withOrigin<T>(ref: OriginRef, fn: () => T): T;
94
+ }
95
+ /**
96
+ * The observe tier: the structured channel and the attribution wiring —
97
+ * everything a production observability consumer needs, and nothing that
98
+ * assumes a developer at a console. Present in dev and observe builds
99
+ * (`__OBSERVE__`); `undefined` in prod.
100
+ */
101
+ export interface Observe {
102
+ diagnostics: Diagnostics;
103
+ /** The attribution hook slot and interaction frame — see `AttributionSlot`. */
104
+ attribution: AttributionSlot;
105
+ /**
106
+ * The live node an emitted event was about, when the emitter knew it.
107
+ * Events are serializable records and never carry the node; consumers that
108
+ * run in-process (devtools, the console reporter) look it up here.
109
+ */
110
+ subjectOf(event: DiagnosticEvent): DiagnosticSubject | undefined;
111
+ /**
112
+ * Marks `owner`'s subtree as the observer's own. A consumer that renders
113
+ * inside the app it watches — an APM adapter's panel, devtools — would
114
+ * otherwise see its own effects, stores and holds reported as findings about
115
+ * the app. Under an excluded owner: diagnostics whose subject sits in the
116
+ * subtree are neither delivered nor reported (the entry is still built, so
117
+ * a site that throws its message still throws), and the attribution engine
118
+ * records no runs for its computations. Mark the root as it is created
119
+ * (`createRoot(() => { OBSERVE.exclude(getOwner()!); … })`) and perform
120
+ * writes from outside the graph under it (`runWithOwner`), so the writer's
121
+ * context is excluded too. Irrevocable for the owner's lifetime.
122
+ */
123
+ exclude(owner: Owner): void;
124
+ /** Whether `subject` sits under an excluded owner (itself included). */
125
+ isExcluded(subject: DiagnosticSubject | null | undefined): boolean;
63
126
  }
127
+ /**
128
+ * The dev tier: devtools hooks, graph traversal, and the console face of the
129
+ * diagnostics channel. Present only in dev builds (`__DEV__`).
130
+ */
64
131
  export interface Dev {
65
132
  hooks: DevHooks;
66
- diagnostics: Diagnostics;
67
- /** "Why did this run" re-run attribution — see attribution.ts. */
68
- attribution: Attribution;
69
133
  getChildren: typeof getChildren;
70
134
  getSignals: typeof getSignals;
71
135
  getParent: typeof getParent;
72
136
  getSources: typeof getSources;
73
137
  getObservers: typeof getObservers;
138
+ /** Console face of an emitted event — see `reportDiagnostic`. */
139
+ report(entry: DiagnosticEvent): void;
140
+ /**
141
+ * Registers a console footer appended to the first console report of
142
+ * each diagnostic code — a discovery pointer to deeper guidance (e.g.
143
+ * solid-js registers its shipped repair skill). Reported events carry
144
+ * it as trailing lines of the same console entry; events that surface as
145
+ * a thrown error instead get it as a follow-up line. Returning undefined
146
+ * for an event suppresses the footer. Passing undefined unregisters and
147
+ * resets the once-per-code memory.
148
+ */
149
+ setConsoleFooter(footer: ((event: DiagnosticEvent) => string | undefined) | undefined): void;
74
150
  }
151
+ export declare const OBSERVE: Observe;
152
+ export declare function isExcluded(subject: DiagnosticSubject | null | undefined): boolean;
153
+ /** For engines that cache the verdict per node: is anything excluded at all? */
154
+ export declare function anyExcluded(): boolean;
155
+ /** Was `entry` built for an excluded subject? Once-per-key reporters must not spend their slot on it. */
156
+ export declare function isSuppressed(entry: DiagnosticEvent): boolean;
75
157
  export declare const DEV: Dev;
76
158
  /**
77
159
  * Dev-mode internal consistency check. A failure means the reactive system
@@ -96,9 +178,11 @@ export declare function ownerPath(subject: DiagnosticSubject | null | undefined)
96
178
  * default (right for the synchronous rule checks — they fire inside the
97
179
  * scope that misbehaved); pass the node for scheduler-time findings whose
98
180
  * ambient context is the flush, or `null` for events that have no location
99
- * by nature. Console output is a separate step see `reportDiagnostic`.
181
+ * by nature. An `ownerPath` already on the event wins over the subject walk
182
+ * (hosts whose owners are not signals' owners compute their own). Console
183
+ * output is a separate, dev-tier step — see `reportDiagnostic`.
100
184
  */
101
- export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence" | "ownerPath">, subject?: DiagnosticSubject | null | undefined): DiagnosticEvent;
185
+ export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence">, subject?: DiagnosticSubject | null | undefined): DiagnosticEvent;
102
186
  /**
103
187
  * The console face of a diagnostic — ONE entry per finding: the message, the
104
188
  * owner path (`in <App> › <TodoRow> › effect`) so a human can locate it, the
@@ -107,7 +191,9 @@ export declare function emitDiagnostic(event: Omit<DiagnosticEvent, "sequence" |
107
191
  * second console argument (hover highlights it, click jumps to Elements).
108
192
  * Severity picks the console method. Call sites report the entry
109
193
  * `emitDiagnostic` returned so the structured and console channels never
110
- * disagree.
194
+ * disagree. Dev-tier: in an observe build this is a no-op, so wiring paths
195
+ * that both emit and report (graph-size warnings) reach the channel only —
196
+ * production observability never writes to the console.
111
197
  */
112
198
  export declare function reportDiagnostic(entry: DiagnosticEvent): void;
113
199
  /**
@@ -117,6 +203,11 @@ export declare function reportDiagnostic(entry: DiagnosticEvent): void;
117
203
  */
118
204
  export declare function throwPendingUntrackedRead(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "data" | "ownerPath">>): never;
119
205
  export declare function warnStrictReadUntracked(strictReadLabel: string, fields?: Partial<Omit<DiagnosticEvent, "sequence" | "ownerPath">>): void;
206
+ /**
207
+ * Observe-tier: stamp a signal with its creating owner so `ownerPath` can
208
+ * locate signal subjects. The per-owner `_signals` list and the devtools
209
+ * `onGraph` hook are dev-tier — the observe build pays one property write.
210
+ */
120
211
  export declare function registerGraph(value: any, owner: Owner | null): void;
121
212
  export declare function clearSignals(node: Owner): void;
122
213
  export declare function getChildren(owner: Owner): Owner[];
@@ -125,12 +216,19 @@ export declare function getParent(owner: Owner): Owner | null;
125
216
  export declare function getSources(computation: Computed<any>): (Signal<any> | Computed<any>)[];
126
217
  export declare function getObservers(node: Signal<any> | Computed<any>): Computed<any>[];
127
218
  /**
128
- * DEV-only: bump live edge counts after a new graph link and warn when a
129
- * node grows an unusually large fan-out (many subscribers on one source) or
130
- * fan-in (many sources on one computation). Repeat-reads that `link()`
131
- * dedupes never reach here. Always-on in dev unlike the opt-in attribution
132
- * engine, a graph-size pathology should surface without asking.
219
+ * Observe-tier: a committed change on `node` is about to re-run `count`
220
+ * subscribers (the notify walk in `insertSubs` counted them as it went
221
+ * fan-out costs exactly one local increment in a loop that already visits
222
+ * every edge, and nothing at link time). Fires from GRAPH_SIZE_WARN_AT up,
223
+ * on the write rather than the link: a fan-out that is never written costs
224
+ * nothing, and one that is re-runs every subscriber this flush. Always-on
225
+ * wherever the channel exists — unlike the opt-in attribution engine, a
226
+ * graph-size pathology should surface without asking.
227
+ */
228
+ export declare function noteFanOut(node: Signal<any> | Computed<any>, count: number): void;
229
+ /**
230
+ * Observe-tier: a recompute pass of `node` tracked `count` distinct sources
231
+ * (its trimmed dep list, walked once at the end of the pass — see recompute;
232
+ * no per-link work, no pass bracket). Fires from GRAPH_SIZE_WARN_AT up.
133
233
  */
134
- export declare function noteGraphLink(dep: Signal<any> | Computed<any>, sub: Computed<any>): void;
135
- /** DEV-only: drop live edge counts when a link is removed. */
136
- export declare function unnoteGraphLink(link: Link): void;
234
+ export declare function noteFanIn(node: Computed<any>, count: number): void;