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

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 (90) hide show
  1. package/dist/dev-shared.js +1399 -285
  2. package/dist/dev.attribution.js +459 -307
  3. package/dist/dev.js +1864 -435
  4. package/dist/observe/affects.js +3 -1
  5. package/dist/observe/attribution.js +7 -1
  6. package/dist/observe/boundaries.js +209 -154
  7. package/dist/observe/core/action.js +18 -8
  8. package/dist/observe/core/async.js +220 -110
  9. package/dist/observe/core/attribution-costs.js +66 -0
  10. package/dist/observe/core/attribution-feedback.js +282 -0
  11. package/dist/observe/core/attribution-hooks.js +23 -1
  12. package/dist/observe/core/attribution-queries.js +28 -0
  13. package/dist/observe/core/attribution.js +262 -485
  14. package/dist/observe/core/constants.js +34 -1
  15. package/dist/observe/core/context.js +3 -3
  16. package/dist/observe/core/core.js +867 -367
  17. package/dist/observe/core/dev.js +78 -17
  18. package/dist/observe/core/effect.js +67 -51
  19. package/dist/observe/core/error-hooks.js +71 -0
  20. package/dist/observe/core/external.js +4 -4
  21. package/dist/observe/core/graph.js +37 -37
  22. package/dist/observe/core/heap.js +45 -45
  23. package/dist/observe/core/invariants.js +2 -0
  24. package/dist/observe/core/lanes.js +86 -49
  25. package/dist/observe/core/optimistic.js +242 -95
  26. package/dist/observe/core/owner.js +98 -84
  27. package/dist/observe/core/scheduler.js +543 -305
  28. package/dist/observe/core/verdict.js +247 -129
  29. package/dist/observe/index.js +7 -3
  30. package/dist/observe/map.js +126 -124
  31. package/dist/observe/signals.js +33 -17
  32. package/dist/observe/store/index.js +2 -0
  33. package/dist/observe/store/next/optimistic.js +141 -132
  34. package/dist/observe/store/next/projection.js +34 -21
  35. package/dist/observe/store/next/reconcile.js +58 -56
  36. package/dist/observe/store/next/store.js +260 -146
  37. package/dist/observe/store/store.js +5 -3
  38. package/dist/observe/store/utils.js +948 -135
  39. package/dist/prod/attribution.js +26 -17
  40. package/dist/prod/boundaries.js +128 -76
  41. package/dist/prod/core/action.js +16 -8
  42. package/dist/prod/core/async.js +251 -143
  43. package/dist/prod/core/constants.js +34 -1
  44. package/dist/prod/core/context.js +3 -3
  45. package/dist/prod/core/core.js +843 -347
  46. package/dist/prod/core/dev.js +17 -1
  47. package/dist/prod/core/effect.js +48 -34
  48. package/dist/prod/core/error-hooks.js +71 -0
  49. package/dist/prod/core/external.js +4 -4
  50. package/dist/prod/core/graph.js +37 -37
  51. package/dist/prod/core/heap.js +45 -45
  52. package/dist/prod/core/lanes.js +90 -53
  53. package/dist/prod/core/optimistic.js +247 -100
  54. package/dist/prod/core/owner.js +57 -45
  55. package/dist/prod/core/scheduler.js +543 -305
  56. package/dist/prod/core/verdict.js +245 -127
  57. package/dist/prod/index.js +7 -3
  58. package/dist/prod/map.js +112 -112
  59. package/dist/prod/signals.js +28 -12
  60. package/dist/prod/store/next/optimistic.js +135 -128
  61. package/dist/prod/store/next/projection.js +31 -20
  62. package/dist/prod/store/next/store.js +325 -252
  63. package/dist/prod/store/store.js +2 -2
  64. package/dist/prod/store/utils.js +946 -135
  65. package/dist/types/attribution.d.ts +7 -2
  66. package/dist/types/attribution.prod.d.ts +10 -1
  67. package/dist/types/boundaries.d.ts +10 -1
  68. package/dist/types/core/action.d.ts +12 -5
  69. package/dist/types/core/attribution-costs.d.ts +35 -0
  70. package/dist/types/core/attribution-feedback.d.ts +133 -0
  71. package/dist/types/core/attribution-hooks.d.ts +28 -3
  72. package/dist/types/core/attribution-queries.d.ts +10 -0
  73. package/dist/types/core/attribution.d.ts +51 -172
  74. package/dist/types/core/constants.d.ts +33 -0
  75. package/dist/types/core/core.d.ts +186 -5
  76. package/dist/types/core/dev.d.ts +129 -9
  77. package/dist/types/core/error-hooks.d.ts +71 -0
  78. package/dist/types/core/index.d.ts +3 -1
  79. package/dist/types/core/invariants.d.ts +4 -0
  80. package/dist/types/core/lanes.d.ts +31 -4
  81. package/dist/types/core/scheduler.d.ts +95 -2
  82. package/dist/types/core/types.d.ts +3 -0
  83. package/dist/types/index.d.ts +2 -2
  84. package/dist/types/signals.d.ts +8 -0
  85. package/dist/types/store/index.d.ts +2 -1
  86. package/dist/types/store/next/optimistic.d.ts +1 -1
  87. package/dist/types/store/next/store.d.ts +6 -5
  88. package/dist/types/store/next/target.d.ts +1 -1
  89. package/dist/types/store/utils.d.ts +177 -6
  90. package/package.json +1 -1
@@ -1,14 +1,17 @@
1
1
  import {
2
- aF as $REFRESH,
3
- bp as setAttributionHooks,
2
+ bK as setAttributionHooks,
4
3
  b as NOT_PENDING,
5
- bq as ownerPath,
6
- al as reportDiagnostic,
7
- am as emitDiagnostic,
8
- br as isSuppressed,
9
- bs as anyExcluded,
10
- bt as isExcluded,
11
- bu as GRAPH_SIZE_WARN_AT
4
+ ax as reportDiagnostic,
5
+ ay as emitDiagnostic,
6
+ bL as isSuppressed,
7
+ bM as recordSubject,
8
+ bN as anyExcluded,
9
+ bO as isExcluded,
10
+ q as CONFIG_DERIVED_OVERRIDE,
11
+ bP as GRAPH_SIZE_WARN_AT,
12
+ bF as ownerPath,
13
+ bQ as subjectOf,
14
+ aT as $REFRESH
12
15
  } from "./dev-shared.js";
13
16
 
14
17
  /**
@@ -51,42 +54,21 @@ function emitRecord(type, record) {
51
54
  function clearListeners() {
52
55
  for (const type in recordListeners) recordListeners[type].clear();
53
56
  }
57
+ /** @internal The engine's clock: `performance.now()` where it exists. */
54
58
  const now = typeof performance !== "undefined" ? () => performance.now() : () => Date.now();
55
59
  const frames = [];
56
- const scopeCosts = new Map();
57
- const writeCosts = new Map();
60
+ const folds = [];
61
+ /** @internal */
62
+ function registerFold(hooks) {
63
+ folds.push(hooks);
64
+ }
65
+ /** @internal Root cause names of a cause chain — the writes/landings/refreshes the chain bottoms out in. */
58
66
  function rootsOf(causes, out) {
59
67
  for (const c of causes) {
60
68
  if (c.kind === "derived" && c.causes && c.causes.length > 0) rootsOf(c.causes, out);
61
69
  else out.add(c.name);
62
70
  }
63
71
  }
64
- function recordCosts(event) {
65
- let scope = scopeCosts.get(event.node);
66
- if (scope === undefined) {
67
- scope = {
68
- name: event.nodeName,
69
- kind: event.nodeKind,
70
- runs: 0,
71
- selfMs: 0,
72
- wastedMs: 0,
73
- overlayMs: 0
74
- };
75
- scopeCosts.set(event.node, scope);
76
- }
77
- scope.runs++;
78
- scope.selfMs += event.selfMs;
79
- if (event.phase !== "plain") scope.overlayMs += event.selfMs;
80
- else if (!event.changed && !event.held) scope.wastedMs += event.selfMs;
81
- const roots = new Set();
82
- rootsOf(event.causes, roots);
83
- for (const name of roots) {
84
- let write = writeCosts.get(name);
85
- if (write === undefined) writeCosts.set(name, (write = { name, runs: 0, downstreamMs: 0 }));
86
- write.runs++;
87
- write.downstreamMs += event.selfMs;
88
- }
89
- }
90
72
  function nodeName(node) {
91
73
  return node._name ?? "anonymous";
92
74
  }
@@ -179,6 +161,41 @@ function currentOrigin() {
179
161
  if (frame !== undefined) return frame;
180
162
  return currentInteraction ?? EXTERNAL_ORIGIN;
181
163
  }
164
+ /** The root origin a cause list traces back to — the stamp of the nearest root write, derived links walked. */
165
+ function originIn(causes) {
166
+ for (const c of causes) {
167
+ const found =
168
+ c.kind === "derived" ? (c.causes !== undefined ? originIn(c.causes) : undefined) : c.origin;
169
+ if (found !== undefined && found.kind !== "external") return found;
170
+ }
171
+ return undefined;
172
+ }
173
+ /**
174
+ * The `currentOrigin` hook: what a runtime recording its own fact right now
175
+ * (a server-function call) should stamp it with. Inside a recompute the
176
+ * fact belongs to the change that caused the run — a `createAsync` calling
177
+ * the server on a navigation's write is the navigation's, and through it the
178
+ * click's — walked down past create runs the way `trackFlightStart` does,
179
+ * since a node born inside a parent's run inherits the parent's causality.
180
+ * Outside one — or when the causes were themselves external (a memo a
181
+ * handler pulls, stale from a timer's write) — it is the write's answer
182
+ * (`currentOrigin`), minus the external sentinel: "none known" is
183
+ * `undefined` on a record, as `HoldEvent.origin` has it. The objects
184
+ * returned are the engine's own frames, so the caller's record joins
185
+ * `InteractionEvent.origin` / `NavigationEvent.origin` by identity.
186
+ */
187
+ function ambientOrigin() {
188
+ for (let i = frames.length - 1; i >= 0; i--) {
189
+ const causes = frames[i].causes;
190
+ if (causes !== null) {
191
+ const cause = originIn(causes);
192
+ if (cause !== undefined) return cause;
193
+ break;
194
+ }
195
+ }
196
+ const origin = currentOrigin();
197
+ return origin === EXTERNAL_ORIGIN ? undefined : origin;
198
+ }
182
199
  const effectFrames = new WeakMap();
183
200
  /** The interaction the innermost open frame runs under, else the ambient one. */
184
201
  function enclosingInteraction() {
@@ -324,7 +341,7 @@ function stampWrite(node, kind, prev = NO_VALUES, value = NO_VALUES) {
324
341
  const prior = node._devChange;
325
342
  node._devChange = record;
326
343
  noteNavigationWrite(prior, record);
327
- noteInteractionWrite(record.origin);
344
+ noteInteractionWrite(record.origin, excludedNode(node));
328
345
  if (kind === "write") trackEffectWrite(node, record, value);
329
346
  // stampWrite is the single funnel for committed root invalidations (sync
330
347
  // writes, refresh(), async landings), which makes it the one place the
@@ -361,10 +378,20 @@ function collectCauses(el) {
361
378
  function markSeen(el) {
362
379
  el._devSeenSeq = changeSeq;
363
380
  }
364
- /** Snapshot the node's current dep identities (call before a run replaces them). */
381
+ /**
382
+ * Snapshot the dep identities of the node's last pass (call before a run
383
+ * replaces them, or after it to read the fresh set). The validated prefix
384
+ * [`_deps`..`_depsTail`] is that pass's set; links past the tail are a
385
+ * previous pass's, kept linked while the frame they fed is still the
386
+ * committed one (A30 — a staged memo pass, or an effect pass whose run is
387
+ * still owed) and not part of the subscription diff.
388
+ */
365
389
  function captureDeps(el) {
366
390
  const deps = [];
367
- for (let l = el._deps; l !== null; l = l._nextDep) deps.push(l._dep);
391
+ for (let l = el._deps; l !== null; l = l._nextDep) {
392
+ deps.push(l._dep);
393
+ if (l === el._depsTail) break;
394
+ }
368
395
  return deps;
369
396
  }
370
397
  /**
@@ -381,6 +408,7 @@ function checkDepWidth(el) {
381
408
  for (let l = el._deps; l !== null; l = l._nextDep) {
382
409
  count++;
383
410
  if (names.length < 12) names.push(nodeName(l._dep));
411
+ if (l === el._depsTail) break; // the validated prefix, as captureDeps
384
412
  }
385
413
  const node = el;
386
414
  if (count < limit || count < (node._devWideWarnedAt ?? 0) * 1.5) return;
@@ -558,7 +586,7 @@ function recordRerun(el, frame, timing, changed, phase, held) {
558
586
  nodeRuns: (node._devRunCount = (node._devRunCount ?? 0) + 1),
559
587
  nodeKind: el._type ? "effect" : "memo",
560
588
  nodeName: nodeName(el),
561
- node: el,
589
+ nodeId: devId(el),
562
590
  causes,
563
591
  depCount: newDeps.length,
564
592
  depsAdded,
@@ -577,10 +605,13 @@ function recordRerun(el, frame, timing, changed, phase, held) {
577
605
  node._devRunInteraction = interaction;
578
606
  node._devRunSeq = event.run;
579
607
  node._devRunCauses = causes;
608
+ // The record is serializable and never carries the node; keep the node
609
+ // beside it for `OBSERVE.subjectOf` and the engine's own joins (effect
610
+ // cycles, relay tears, `why()`), for as long as anyone holds the record.
611
+ recordSubject(event, el);
580
612
  history.push(event);
581
613
  if (history.length > options.historyLimit) history.shift();
582
- recordCosts(event);
583
- recordFeedbackRun(event);
614
+ for (const f of folds) f.rerun?.(el, event);
584
615
  noteInteractionRun(interaction, timing.selfMs, false);
585
616
  if (event.nodeKind === "effect") checkEffectCycle(el, causes);
586
617
  checkRelayTear(el, causes, prevCauses);
@@ -1015,7 +1046,7 @@ function checkRelayTear(victim, causes, prevCauses) {
1015
1046
  // new data (reconcile's job — and UNSTABLE_LIST_IDENTITY's, downstream). Once
1016
1047
  // per store path.
1017
1048
  const immutableReported = new Set();
1018
- function checkImmutableUpdate(path, isArray, total, same, prevTotal) {
1049
+ function checkImmutableUpdate(path, isArray, total, same, prevTotal, owner) {
1019
1050
  if (immutableReported.has(path) || total < 2) return;
1020
1051
  // Push/filter/splice copies change the length by a little; a wholesale
1021
1052
  // resize is a different operation even if some items survive.
@@ -1035,16 +1066,23 @@ function checkImmutableUpdate(path, isArray, total, same, prevTotal) {
1035
1066
  `tracks ${isArray ? "items" : "leaves"}; a new container makes every reader of "${path}" ` +
1036
1067
  `re-run for the ${changed === 1 ? "one that" : "few that"} moved. Instead, ${repair}. For ` +
1037
1068
  `data arriving from outside (a fetch result), merge it with reconcile(data, key)(${path}).`;
1038
- const entry = emitDiagnostic({
1039
- code: "IMMUTABLE_UPDATE_IN_STORE",
1040
- kind: "perf",
1041
- severity: "warn",
1042
- message,
1043
- nodeName: path,
1044
- data: { path, shape, total, unchanged: same, changed }
1045
- });
1046
- // Paths are not unique across stores: a write under an excluded owner
1047
- // (an adapter's own "store.list") must not spend the app's once-per-path slot.
1069
+ // The subject is the store's own owner, not the writer's context: the
1070
+ // finding is about the store, and its writes legitimately arrive from
1071
+ // outside the graph (an event handler, an adapter). Falls back to the
1072
+ // ambient context (emitDiagnostic's default) when the store recorded none.
1073
+ const entry = emitDiagnostic(
1074
+ {
1075
+ code: "IMMUTABLE_UPDATE_IN_STORE",
1076
+ kind: "perf",
1077
+ severity: "warn",
1078
+ message,
1079
+ nodeName: path,
1080
+ data: { path, shape, total, unchanged: same, changed }
1081
+ },
1082
+ owner
1083
+ );
1084
+ // Paths are not unique across stores: an excluded owner's store (an
1085
+ // adapter's own "store.list") must not spend the app's once-per-path slot.
1048
1086
  if (isSuppressed(entry)) return;
1049
1087
  immutableReported.add(path);
1050
1088
  reportDiagnostic(entry);
@@ -1160,9 +1198,7 @@ function trackFlightStart(el, flight) {
1160
1198
  if (origin === at) flightOrigins.set(flight, at);
1161
1199
  // Census: a flight still in the air when the node starts another was
1162
1200
  // superseded — its answer will be discarded.
1163
- const stats = flightBucket(el);
1164
- stats.flights++;
1165
- if (liveFlights.has(el)) stats.abandoned++;
1201
+ for (const f of folds) f.flightStart?.(el, liveFlights.has(el));
1166
1202
  // Nearest enclosing frame with causes: create runs carry null (a node born
1167
1203
  // inside a parent's recompute inherits the parent's causality — the
1168
1204
  // boundary-reveal case, and the lazy sibling whose first pull is gated
@@ -1200,10 +1236,7 @@ function finalizeFlight(el) {
1200
1236
  liveFlights.delete(el);
1201
1237
  const landedAt = now();
1202
1238
  const ms = landedAt - flight.origin;
1203
- const stats = flightBucket(el);
1204
- stats.landed++;
1205
- stats.landedMs += ms;
1206
- if (ms > stats.worstMs) stats.worstMs = ms;
1239
+ for (const f of folds) f.flightLanded?.(el, ms);
1207
1240
  const record = el._devChange;
1208
1241
  // Only a stamp this landing produced may carry the measurement — a stale
1209
1242
  // async record from a previous landing must not be re-labeled.
@@ -1266,9 +1299,14 @@ function checkWaterfall(el, chain, ms) {
1266
1299
  const holdStates = new WeakMap();
1267
1300
  let activeHold = null;
1268
1301
  let holdLog = [];
1269
- /** Companions are optimistic nodes too; `_parentSource` marks them. */
1302
+ /** Companions are optimistic nodes too; `_parentSource` marks them. So is a
1303
+ * memo carrying a DERIVED override (lanes stage, #3479) — a lane pass's
1304
+ * result, not a write anyone made: neither is an acknowledgement. */
1270
1305
  function isCompanion(node) {
1271
- return !!node._x && node._x._parentSource !== undefined;
1306
+ return (
1307
+ (!!node._x && node._x._parentSource !== undefined) ||
1308
+ (node._config & CONFIG_DERIVED_OVERRIDE) !== 0
1309
+ );
1272
1310
  }
1273
1311
  const HOLD_CENSUS_CAP = 10_000;
1274
1312
  function acknowledge(state, kind, source, reader) {
@@ -1461,11 +1499,12 @@ function trackHoldSettled(t) {
1461
1499
  emitRecord("hold", event);
1462
1500
  settleNavigations(t, event);
1463
1501
  settleInteractionsHeld(t, event);
1464
- recordFeedbackHold(event);
1502
+ for (const f of folds) f.hold?.(event);
1465
1503
  if (isSilentHold(event)) checkSilentHold(event, subject);
1466
1504
  else checkLongHold(event, subject);
1467
1505
  }
1468
1506
  /** `isLongHold` — the tail outlasted `longHolds.infoMs`. */
1507
+ /** @internal */
1469
1508
  function isLongHold(event) {
1470
1509
  const cfg = options.longHolds;
1471
1510
  return cfg !== false && cfg !== undefined && event.tailMs >= cfg.infoMs;
@@ -1735,7 +1774,7 @@ function settleNavigation(state, outcome, hold) {
1735
1774
  event.settledMs = now() - event.at;
1736
1775
  event.outcome = outcome;
1737
1776
  if (hold !== undefined) event.hold = hold;
1738
- recordFeedbackNavigation(event);
1777
+ for (const f of folds) f.navigation?.(event);
1739
1778
  emitRecord("navigation", event);
1740
1779
  // The interaction that performed it may have been waiting only on this.
1741
1780
  const under = openInteractionOf(event.interaction);
@@ -1764,7 +1803,8 @@ function openInteraction(frame) {
1764
1803
  open: true,
1765
1804
  writeDrain: drainSeq,
1766
1805
  heldIn: new Set(),
1767
- held: false
1806
+ held: false,
1807
+ excludedWrites: 0
1768
1808
  };
1769
1809
  interactionStates.set(frame, state);
1770
1810
  openInteractions.add(state);
@@ -1788,9 +1828,13 @@ function openInteractionOf(origin) {
1788
1828
  return state !== undefined && openInteractions.has(state) ? state : undefined;
1789
1829
  }
1790
1830
  /** stampWrite: a root write stamped `origin`. */
1791
- function noteInteractionWrite(origin) {
1831
+ function noteInteractionWrite(origin, excluded) {
1792
1832
  const state = openInteractionOf(origin);
1793
1833
  if (state === undefined) return;
1834
+ if (excluded) {
1835
+ state.excludedWrites++;
1836
+ return;
1837
+ }
1794
1838
  state.event.writes++;
1795
1839
  state.writeDrain = drainSeq;
1796
1840
  }
@@ -1839,6 +1883,14 @@ function maybeSettleInteraction(state, end = now()) {
1839
1883
  if (event.writes > 0 && drainSeq <= state.writeDrain) return;
1840
1884
  for (const nav of event.navigations) if (nav.outcome === undefined) return;
1841
1885
  openInteractions.delete(state);
1886
+ // Every write went to an excluded subject and nothing of the app's ran: the
1887
+ // click was on the observer's own UI (a devtools panel's button). Not a
1888
+ // fact about the app — forget it rather than report a dead interaction.
1889
+ if (event.writes === 0 && state.excludedWrites > 0 && event.runs === 0 && event.created === 0) {
1890
+ const i = interactionLog.indexOf(event);
1891
+ if (i !== -1) interactionLog.splice(i, 1);
1892
+ return;
1893
+ }
1842
1894
  event.settledMs = end - event.at;
1843
1895
  event.outcome = event.writes === 0 ? "idle" : state.held ? "held" : "committed";
1844
1896
  emitRecord("interaction", event);
@@ -1852,209 +1904,10 @@ function navigationData(origin) {
1852
1904
  if (origin.params !== undefined) data.params = origin.params;
1853
1905
  return data;
1854
1906
  }
1855
- const feedbackSources = new Map();
1856
- const feedbackInteractions = new Map();
1857
- const feedbackNavigations = new Map();
1858
- const flightStats = new Map();
1859
- const fallbackStats = new Map();
1860
- const FALLBACK_FLASH_MS = 150;
1861
- function flightBucket(el) {
1862
- let row = flightStats.get(el);
1863
- if (row === undefined) {
1864
- row = { source: nodeName(el), flights: 0, landed: 0, abandoned: 0, landedMs: 0, worstMs: 0 };
1865
- flightStats.set(el, row);
1866
- }
1867
- return row;
1868
- }
1869
- function trackFallback(boundary, tree, shown) {
1870
- let bucket = fallbackStats.get(boundary);
1871
- if (bucket === undefined) {
1872
- bucket = {
1873
- row: { boundary: "boundary", shows: 0, shownMs: 0, worstMs: 0, flashes: 0 },
1874
- shownAt: null
1875
- };
1876
- fallbackStats.set(boundary, bucket);
1877
- }
1878
- // The first show can fire before the subtree exists; name on first sight.
1879
- if (bucket.row.boundary === "boundary" && tree !== undefined) {
1880
- const path = ownerPath(tree);
1881
- if (path !== undefined) bucket.row.boundary = path.join(" › ");
1882
- }
1883
- if (shown) {
1884
- if (bucket.shownAt === null) {
1885
- bucket.shownAt = now();
1886
- bucket.row.shows++;
1887
- }
1888
- return;
1889
- }
1890
- if (bucket.shownAt === null) return;
1891
- const ms = now() - bucket.shownAt;
1892
- bucket.shownAt = null;
1893
- bucket.row.shownMs += ms;
1894
- if (ms > bucket.row.worstMs) bucket.row.worstMs = ms;
1895
- if (ms < FALLBACK_FLASH_MS) bucket.row.flashes++;
1896
- }
1897
- /** No affordance answered and nothing painted while held. */
1907
+ /** @internal No affordance answered and nothing painted while held. */
1898
1908
  function isSilentHold(event) {
1899
1909
  return event.paintedDuringHold === 0 && event.acknowledgements.length === 0;
1900
1910
  }
1901
- function interactionBucket(interaction) {
1902
- const key = formatOrigin(interaction);
1903
- let bucket = feedbackInteractions.get(key);
1904
- if (bucket === undefined) {
1905
- bucket = {
1906
- row: {
1907
- interaction: key,
1908
- dispatches: 0,
1909
- runs: 0,
1910
- selfMs: 0,
1911
- worstDispatchMs: 0,
1912
- holds: 0,
1913
- heldMs: 0,
1914
- silentMs: 0,
1915
- worstHoldMs: 0
1916
- },
1917
- dispatches: new Map()
1918
- };
1919
- feedbackInteractions.set(key, bucket);
1920
- }
1921
- const at = interaction.at ?? 0;
1922
- if (!bucket.dispatches.has(at)) {
1923
- bucket.dispatches.set(at, 0);
1924
- bucket.row.dispatches++;
1925
- }
1926
- return bucket;
1927
- }
1928
- function recordFeedbackRun(event) {
1929
- if (event.interaction === undefined) return;
1930
- const bucket = interactionBucket(event.interaction);
1931
- bucket.row.runs++;
1932
- bucket.row.selfMs += event.selfMs;
1933
- const at = event.interaction.at ?? 0;
1934
- const dispatchMs = bucket.dispatches.get(at) + event.selfMs;
1935
- bucket.dispatches.set(at, dispatchMs);
1936
- if (dispatchMs > bucket.row.worstDispatchMs) bucket.row.worstDispatchMs = dispatchMs;
1937
- }
1938
- function recordFeedbackHold(event) {
1939
- const sources = [...event.blockers].sort();
1940
- const key = sources.join("\u0000");
1941
- let bucket = feedbackSources.get(key);
1942
- if (bucket === undefined) {
1943
- bucket = {
1944
- row: {
1945
- sources,
1946
- holds: 0,
1947
- heldMs: 0,
1948
- worstMs: 0,
1949
- silent: 0,
1950
- silentMs: 0,
1951
- latestOnly: 0,
1952
- long: 0,
1953
- longMs: 0,
1954
- acknowledgedBy: [],
1955
- interactions: [],
1956
- writes: [],
1957
- actions: 0
1958
- },
1959
- acks: new Map(),
1960
- interactions: new Map(),
1961
- writes: new Set()
1962
- };
1963
- feedbackSources.set(key, bucket);
1964
- }
1965
- const row = bucket.row;
1966
- const silent = isSilentHold(event);
1967
- row.holds++;
1968
- row.heldMs += event.holdMs;
1969
- if (event.holdMs > row.worstMs) row.worstMs = event.holdMs;
1970
- if (silent) {
1971
- row.silent++;
1972
- row.silentMs += event.holdMs;
1973
- } else if (
1974
- event.acknowledgements.length > 0 &&
1975
- event.acknowledgements.every(a => a.kind === "latest")
1976
- )
1977
- row.latestOnly++;
1978
- if (isLongHold(event)) {
1979
- row.long++;
1980
- row.longMs += event.tailMs;
1981
- }
1982
- if (event.action) row.actions++;
1983
- for (const a of event.acknowledgements) {
1984
- const by = `${a.kind}:${a.source}`;
1985
- bucket.acks.set(by, (bucket.acks.get(by) ?? 0) + 1);
1986
- }
1987
- for (const w of event.heldWrites) bucket.writes.add(w.name);
1988
- if (event.interaction !== undefined) {
1989
- const key = formatOrigin(event.interaction);
1990
- bucket.interactions.set(key, (bucket.interactions.get(key) ?? 0) + 1);
1991
- const ib = interactionBucket(event.interaction);
1992
- ib.row.holds++;
1993
- ib.row.heldMs += event.holdMs;
1994
- if (silent) ib.row.silentMs += event.holdMs;
1995
- if (event.holdMs > ib.row.worstHoldMs) ib.row.worstHoldMs = event.holdMs;
1996
- }
1997
- }
1998
- function recordFeedbackNavigation(event) {
1999
- const name = event.name ?? event.to;
2000
- if (name === undefined) return;
2001
- let row = feedbackNavigations.get(name);
2002
- if (row === undefined) {
2003
- row = {
2004
- name,
2005
- navigations: 0,
2006
- settledMs: 0,
2007
- worstMs: 0,
2008
- held: 0,
2009
- heldMs: 0,
2010
- silent: 0,
2011
- superseded: 0,
2012
- redirected: 0
2013
- };
2014
- feedbackNavigations.set(name, row);
2015
- }
2016
- row.navigations++;
2017
- if (event.redirects !== undefined) row.redirected++;
2018
- if (event.outcome === "superseded") {
2019
- row.superseded++;
2020
- return;
2021
- }
2022
- const ms = event.settledMs;
2023
- row.settledMs += ms;
2024
- if (ms > row.worstMs) row.worstMs = ms;
2025
- if (event.outcome === "held") {
2026
- row.held++;
2027
- row.heldMs += event.hold?.holdMs ?? ms;
2028
- if (event.hold !== undefined && isSilentHold(event.hold)) row.silent++;
2029
- }
2030
- }
2031
- function rankedCounts(counts, key) {
2032
- return [...counts].sort((a, b) => b[1] - a[1]).map(([name, holds]) => ({ [key]: name, holds }));
2033
- }
2034
- function feedbackTables() {
2035
- const navigations = [...feedbackNavigations.values()]
2036
- .map(row => ({ ...row }))
2037
- .sort((a, b) => b.heldMs - a.heldMs || b.settledMs - a.settledMs);
2038
- const flights = [...flightStats.values()]
2039
- .map(row => ({ ...row }))
2040
- .sort((a, b) => b.abandoned - a.abandoned || b.flights - a.flights);
2041
- const fallbacks = [...fallbackStats.values()]
2042
- .map(bucket => ({ ...bucket.row }))
2043
- .sort((a, b) => b.flashes - a.flashes || b.shownMs - a.shownMs);
2044
- const sources = [...feedbackSources.values()]
2045
- .map(bucket => ({
2046
- ...bucket.row,
2047
- acknowledgedBy: rankedCounts(bucket.acks, "by"),
2048
- interactions: rankedCounts(bucket.interactions, "interaction"),
2049
- writes: [...bucket.writes]
2050
- }))
2051
- .sort((a, b) => b.silentMs - a.silentMs || b.heldMs - a.heldMs);
2052
- // Ranked by the total time the user spent on it: held plus synchronous work.
2053
- const interactions = [...feedbackInteractions.values()]
2054
- .map(bucket => ({ ...bucket.row }))
2055
- .sort((a, b) => b.heldMs + b.selfMs - (a.heldMs + a.selfMs));
2056
- return { sources, interactions, navigations, flights, fallbacks };
2057
- }
2058
1911
  // The engine's implementation of the core's dev hook points. Installed by
2059
1912
  // enable(), uninstalled by disable() — while uninstalled the core pays one
2060
1913
  // null check per site and nothing else.
@@ -2213,28 +2066,26 @@ const engineHooks = {
2213
2066
  transitionMerged(target, outgoing) {
2214
2067
  trackHoldMerge(target, outgoing);
2215
2068
  },
2216
- storeReplaced(path, isArray, total, unchanged, prevTotal) {
2217
- checkImmutableUpdate(path, isArray, total, unchanged, prevTotal);
2069
+ storeReplaced(path, isArray, total, unchanged, prevTotal, owner) {
2070
+ checkImmutableUpdate(path, isArray, total, unchanged, prevTotal, owner);
2218
2071
  },
2219
2072
  listChurn(el, removed, created, newLen, keyed) {
2220
2073
  checkListIdentity(el, removed, created, newLen, keyed);
2221
2074
  },
2222
2075
  boundaryFallback(boundary, tree, shown) {
2223
- trackFallback(boundary, tree, shown);
2076
+ for (const f of folds) f.fallback?.(boundary, tree, shown);
2077
+ },
2078
+ currentOrigin() {
2079
+ return ambientOrigin();
2224
2080
  }
2225
2081
  };
2226
2082
  const attribution = {
2227
2083
  enable(opts) {
2228
2084
  options = { ...defaultOptions, ...opts };
2229
2085
  frames.length = 0;
2230
- scopeCosts.clear();
2231
- writeCosts.clear();
2232
2086
  waterfallLog = [];
2233
2087
  holdLog = [];
2234
2088
  activeHold = null;
2235
- feedbackSources.clear();
2236
- feedbackInteractions.clear();
2237
- feedbackNavigations.clear();
2238
2089
  navigationLog = [];
2239
2090
  openNavs.clear();
2240
2091
  interactionLog = [];
@@ -2243,26 +2094,20 @@ const attribution = {
2243
2094
  reportedCycles.clear();
2244
2095
  relays.clear();
2245
2096
  immutableReported.clear();
2246
- flightStats.clear();
2247
- fallbackStats.clear();
2248
2097
  originFrames.length = 0;
2249
2098
  interactionStack.length = 0;
2250
2099
  currentInteraction = null;
2251
2100
  hotCauses.clear();
2101
+ for (const f of folds) f.reset?.();
2252
2102
  setAttributionHooks(engineHooks);
2253
2103
  },
2254
2104
  disable() {
2255
2105
  clearListeners();
2256
2106
  history = [];
2257
2107
  frames.length = 0;
2258
- scopeCosts.clear();
2259
- writeCosts.clear();
2260
2108
  waterfallLog = [];
2261
2109
  holdLog = [];
2262
2110
  activeHold = null;
2263
- feedbackSources.clear();
2264
- feedbackInteractions.clear();
2265
- feedbackNavigations.clear();
2266
2111
  navigationLog = [];
2267
2112
  openNavs.clear();
2268
2113
  interactionLog = [];
@@ -2271,12 +2116,11 @@ const attribution = {
2271
2116
  reportedCycles.clear();
2272
2117
  relays.clear();
2273
2118
  immutableReported.clear();
2274
- flightStats.clear();
2275
- fallbackStats.clear();
2276
2119
  originFrames.length = 0;
2277
2120
  interactionStack.length = 0;
2278
2121
  currentInteraction = null;
2279
2122
  hotCauses.clear();
2123
+ for (const f of folds) f.reset?.();
2280
2124
  setAttributionHooks(null);
2281
2125
  },
2282
2126
  subscribe(typeOrListener, listener) {
@@ -2289,22 +2133,6 @@ const attribution = {
2289
2133
  history() {
2290
2134
  return history;
2291
2135
  },
2292
- why(target) {
2293
- const node = target?.[$REFRESH] ?? target;
2294
- return history.filter(event => event.node === node);
2295
- },
2296
- subscriptions(target) {
2297
- const node = target?.[$REFRESH] ?? target;
2298
- const names = [];
2299
- for (let l = node?._deps ?? null; l !== null; l = l._nextDep) names.push(nodeName(l._dep));
2300
- return names;
2301
- },
2302
- costs() {
2303
- return {
2304
- scopes: [...scopeCosts.values()].sort((a, b) => b.selfMs - a.selfMs),
2305
- writes: [...writeCosts.values()].sort((a, b) => b.downstreamMs - a.downstreamMs)
2306
- };
2307
- },
2308
2136
  waterfalls() {
2309
2137
  return waterfallLog;
2310
2138
  },
@@ -2317,17 +2145,341 @@ const attribution = {
2317
2145
  interactions() {
2318
2146
  return interactionLog;
2319
2147
  },
2320
- feedback() {
2321
- return feedbackTables();
2322
- },
2323
2148
  markFlight(flight, startedAt = now()) {
2324
2149
  // Earliest wins: re-marking (a cache re-serving the same promise) must
2325
2150
  // not move the origin later.
2326
2151
  const existing = flightOrigins.get(flight);
2327
2152
  if (existing === undefined || startedAt < existing) flightOrigins.set(flight, startedAt);
2328
- },
2329
- format: formatRerun,
2330
- formatOrigin
2153
+ }
2331
2154
  };
2332
2155
 
2333
- export { attribution };
2156
+ /**
2157
+ * `costs()` — the cost tables: scopes ranked by the self-time they burn,
2158
+ * root writes ranked by the downstream re-run time they cause. Folded from
2159
+ * every `RerunEvent` as the engine records it.
2160
+ *
2161
+ * Its own module on purpose: the tables register with the engine's fold seam
2162
+ * when this module is evaluated, so an observe-tier consumer that only
2163
+ * subscribes to records (an APM adapter) never ships the accumulation or the
2164
+ * tables; a consumer that imports `costs` gets both. Reset with the engine on
2165
+ * `enable()`/`disable()`.
2166
+ */
2167
+ const scopeCosts = new Map();
2168
+ const writeCosts = new Map();
2169
+ function recordCosts(el, event) {
2170
+ let scope = scopeCosts.get(el);
2171
+ if (scope === undefined) {
2172
+ scope = {
2173
+ name: event.nodeName,
2174
+ kind: event.nodeKind,
2175
+ runs: 0,
2176
+ selfMs: 0,
2177
+ wastedMs: 0,
2178
+ overlayMs: 0
2179
+ };
2180
+ scopeCosts.set(el, scope);
2181
+ }
2182
+ scope.runs++;
2183
+ scope.selfMs += event.selfMs;
2184
+ if (event.phase !== "plain") scope.overlayMs += event.selfMs;
2185
+ else if (!event.changed && !event.held) scope.wastedMs += event.selfMs;
2186
+ const roots = new Set();
2187
+ rootsOf(event.causes, roots);
2188
+ for (const name of roots) {
2189
+ let write = writeCosts.get(name);
2190
+ if (write === undefined) writeCosts.set(name, (write = { name, runs: 0, downstreamMs: 0 }));
2191
+ write.runs++;
2192
+ write.downstreamMs += event.selfMs;
2193
+ }
2194
+ }
2195
+ registerFold({
2196
+ rerun: recordCosts,
2197
+ reset() {
2198
+ scopeCosts.clear();
2199
+ writeCosts.clear();
2200
+ }
2201
+ });
2202
+ /**
2203
+ * Aggregated cost tables since `enable()`: `scopes` ranked by self-time
2204
+ * (with `wastedMs` = time spent on unchanged-value runs), `writes` ranked by
2205
+ * total downstream re-run time each root write caused.
2206
+ */
2207
+ function costs() {
2208
+ return {
2209
+ scopes: [...scopeCosts.values()].sort((a, b) => b.selfMs - a.selfMs),
2210
+ writes: [...writeCosts.values()].sort((a, b) => b.downstreamMs - a.downstreamMs)
2211
+ };
2212
+ }
2213
+
2214
+ /**
2215
+ * `feedback()` — what the user waited on, as ranked tables. Folded from the
2216
+ * records the engine already keeps (holds, the interaction on each re-run,
2217
+ * navigations) and two censuses the engine reports as they happen (flights
2218
+ * started/landed/abandoned per async source; fallback shows per boundary).
2219
+ *
2220
+ * Its own module on purpose: the tables register with the engine's fold seam
2221
+ * when this module is evaluated, so an observe-tier consumer that only
2222
+ * subscribes to records never ships them. Reset with the engine on
2223
+ * `enable()`/`disable()`.
2224
+ */
2225
+ const feedbackSources = new Map();
2226
+ const feedbackInteractions = new Map();
2227
+ const feedbackNavigations = new Map();
2228
+ const flightStats = new Map();
2229
+ const fallbackStats = new Map();
2230
+ const FALLBACK_FLASH_MS = 150;
2231
+ function flightBucket(el) {
2232
+ let row = flightStats.get(el);
2233
+ if (row === undefined) {
2234
+ row = { source: nodeName(el), flights: 0, landed: 0, abandoned: 0, landedMs: 0, worstMs: 0 };
2235
+ flightStats.set(el, row);
2236
+ }
2237
+ return row;
2238
+ }
2239
+ function trackFallback(boundary, tree, shown) {
2240
+ let bucket = fallbackStats.get(boundary);
2241
+ if (bucket === undefined) {
2242
+ bucket = {
2243
+ row: { boundary: "boundary", shows: 0, shownMs: 0, worstMs: 0, flashes: 0 },
2244
+ shownAt: null
2245
+ };
2246
+ fallbackStats.set(boundary, bucket);
2247
+ }
2248
+ // The first show can fire before the subtree exists; name on first sight.
2249
+ if (bucket.row.boundary === "boundary" && tree !== undefined) {
2250
+ const path = ownerPath(tree);
2251
+ if (path !== undefined) bucket.row.boundary = path.join(" › ");
2252
+ }
2253
+ if (shown) {
2254
+ if (bucket.shownAt === null) {
2255
+ bucket.shownAt = now();
2256
+ bucket.row.shows++;
2257
+ }
2258
+ return;
2259
+ }
2260
+ if (bucket.shownAt === null) return;
2261
+ const ms = now() - bucket.shownAt;
2262
+ bucket.shownAt = null;
2263
+ bucket.row.shownMs += ms;
2264
+ if (ms > bucket.row.worstMs) bucket.row.worstMs = ms;
2265
+ if (ms < FALLBACK_FLASH_MS) bucket.row.flashes++;
2266
+ }
2267
+ function interactionBucket(interaction) {
2268
+ const key = formatOrigin(interaction);
2269
+ let bucket = feedbackInteractions.get(key);
2270
+ if (bucket === undefined) {
2271
+ bucket = {
2272
+ row: {
2273
+ interaction: key,
2274
+ dispatches: 0,
2275
+ runs: 0,
2276
+ selfMs: 0,
2277
+ worstDispatchMs: 0,
2278
+ holds: 0,
2279
+ heldMs: 0,
2280
+ silentMs: 0,
2281
+ worstHoldMs: 0
2282
+ },
2283
+ dispatches: new Map()
2284
+ };
2285
+ feedbackInteractions.set(key, bucket);
2286
+ }
2287
+ const at = interaction.at ?? 0;
2288
+ if (!bucket.dispatches.has(at)) {
2289
+ bucket.dispatches.set(at, 0);
2290
+ bucket.row.dispatches++;
2291
+ }
2292
+ return bucket;
2293
+ }
2294
+ function recordFeedbackRun(event) {
2295
+ if (event.interaction === undefined) return;
2296
+ const bucket = interactionBucket(event.interaction);
2297
+ bucket.row.runs++;
2298
+ bucket.row.selfMs += event.selfMs;
2299
+ const at = event.interaction.at ?? 0;
2300
+ const dispatchMs = bucket.dispatches.get(at) + event.selfMs;
2301
+ bucket.dispatches.set(at, dispatchMs);
2302
+ if (dispatchMs > bucket.row.worstDispatchMs) bucket.row.worstDispatchMs = dispatchMs;
2303
+ }
2304
+ function recordFeedbackHold(event) {
2305
+ const sources = [...event.blockers].sort();
2306
+ const key = sources.join("\u0000");
2307
+ let bucket = feedbackSources.get(key);
2308
+ if (bucket === undefined) {
2309
+ bucket = {
2310
+ row: {
2311
+ sources,
2312
+ holds: 0,
2313
+ heldMs: 0,
2314
+ worstMs: 0,
2315
+ silent: 0,
2316
+ silentMs: 0,
2317
+ latestOnly: 0,
2318
+ long: 0,
2319
+ longMs: 0,
2320
+ acknowledgedBy: [],
2321
+ interactions: [],
2322
+ writes: [],
2323
+ actions: 0
2324
+ },
2325
+ acks: new Map(),
2326
+ interactions: new Map(),
2327
+ writes: new Set()
2328
+ };
2329
+ feedbackSources.set(key, bucket);
2330
+ }
2331
+ const row = bucket.row;
2332
+ const silent = isSilentHold(event);
2333
+ row.holds++;
2334
+ row.heldMs += event.holdMs;
2335
+ if (event.holdMs > row.worstMs) row.worstMs = event.holdMs;
2336
+ if (silent) {
2337
+ row.silent++;
2338
+ row.silentMs += event.holdMs;
2339
+ } else if (
2340
+ event.acknowledgements.length > 0 &&
2341
+ event.acknowledgements.every(a => a.kind === "latest")
2342
+ )
2343
+ row.latestOnly++;
2344
+ if (isLongHold(event)) {
2345
+ row.long++;
2346
+ row.longMs += event.tailMs;
2347
+ }
2348
+ if (event.action) row.actions++;
2349
+ for (const a of event.acknowledgements) {
2350
+ const by = `${a.kind}:${a.source}`;
2351
+ bucket.acks.set(by, (bucket.acks.get(by) ?? 0) + 1);
2352
+ }
2353
+ for (const w of event.heldWrites) bucket.writes.add(w.name);
2354
+ if (event.interaction !== undefined) {
2355
+ const key = formatOrigin(event.interaction);
2356
+ bucket.interactions.set(key, (bucket.interactions.get(key) ?? 0) + 1);
2357
+ const ib = interactionBucket(event.interaction);
2358
+ ib.row.holds++;
2359
+ ib.row.heldMs += event.holdMs;
2360
+ if (silent) ib.row.silentMs += event.holdMs;
2361
+ if (event.holdMs > ib.row.worstHoldMs) ib.row.worstHoldMs = event.holdMs;
2362
+ }
2363
+ }
2364
+ function recordFeedbackNavigation(event) {
2365
+ const name = event.name ?? event.to;
2366
+ if (name === undefined) return;
2367
+ let row = feedbackNavigations.get(name);
2368
+ if (row === undefined) {
2369
+ row = {
2370
+ name,
2371
+ navigations: 0,
2372
+ settledMs: 0,
2373
+ worstMs: 0,
2374
+ held: 0,
2375
+ heldMs: 0,
2376
+ silent: 0,
2377
+ superseded: 0,
2378
+ redirected: 0
2379
+ };
2380
+ feedbackNavigations.set(name, row);
2381
+ }
2382
+ row.navigations++;
2383
+ if (event.redirects !== undefined) row.redirected++;
2384
+ if (event.outcome === "superseded") {
2385
+ row.superseded++;
2386
+ return;
2387
+ }
2388
+ const ms = event.settledMs;
2389
+ row.settledMs += ms;
2390
+ if (ms > row.worstMs) row.worstMs = ms;
2391
+ if (event.outcome === "held") {
2392
+ row.held++;
2393
+ row.heldMs += event.hold?.holdMs ?? ms;
2394
+ if (event.hold !== undefined && isSilentHold(event.hold)) row.silent++;
2395
+ }
2396
+ }
2397
+ function rankedCounts(counts, key) {
2398
+ return [...counts].sort((a, b) => b[1] - a[1]).map(([name, holds]) => ({ [key]: name, holds }));
2399
+ }
2400
+ registerFold({
2401
+ rerun: (_el, event) => recordFeedbackRun(event),
2402
+ hold: recordFeedbackHold,
2403
+ navigation: recordFeedbackNavigation,
2404
+ flightStart(el, abandoned) {
2405
+ const stats = flightBucket(el);
2406
+ stats.flights++;
2407
+ if (abandoned) stats.abandoned++;
2408
+ },
2409
+ flightLanded(el, ms) {
2410
+ const stats = flightBucket(el);
2411
+ stats.landed++;
2412
+ stats.landedMs += ms;
2413
+ if (ms > stats.worstMs) stats.worstMs = ms;
2414
+ },
2415
+ fallback: trackFallback,
2416
+ reset() {
2417
+ feedbackSources.clear();
2418
+ feedbackInteractions.clear();
2419
+ feedbackNavigations.clear();
2420
+ flightStats.clear();
2421
+ fallbackStats.clear();
2422
+ }
2423
+ });
2424
+ /**
2425
+ * What the user waited on, folded from holds and the interaction on each
2426
+ * re-run: `sources` ranks async sources by the silent time writes spent held
2427
+ * behind them (with which affordances answered, how often, and which
2428
+ * interactions were held); `interactions` ranks user events by the total time
2429
+ * they cost — re-run work caused (long-flush hazard) beside time held
2430
+ * (silent-hold hazard). Facts at every duration; SILENT_HOLD is the
2431
+ * thresholded verdict. Three more tables round out the picture: `navigations`
2432
+ * ranks routes by the time spent held navigating to them (folded from settled
2433
+ * navigations), `flights` counts each async source's flights and how many
2434
+ * were abandoned before landing (the re-ask storm), and `fallbacks` measures
2435
+ * how long each loading boundary showed its fallback and how often that was a
2436
+ * flash.
2437
+ */
2438
+ function feedback() {
2439
+ const navigations = [...feedbackNavigations.values()]
2440
+ .map(row => ({ ...row }))
2441
+ .sort((a, b) => b.heldMs - a.heldMs || b.settledMs - a.settledMs);
2442
+ const flights = [...flightStats.values()]
2443
+ .map(row => ({ ...row }))
2444
+ .sort((a, b) => b.abandoned - a.abandoned || b.flights - a.flights);
2445
+ const fallbacks = [...fallbackStats.values()]
2446
+ .map(bucket => ({ ...bucket.row }))
2447
+ .sort((a, b) => b.flashes - a.flashes || b.shownMs - a.shownMs);
2448
+ const sources = [...feedbackSources.values()]
2449
+ .map(bucket => ({
2450
+ ...bucket.row,
2451
+ acknowledgedBy: rankedCounts(bucket.acks, "by"),
2452
+ interactions: rankedCounts(bucket.interactions, "interaction"),
2453
+ writes: [...bucket.writes]
2454
+ }))
2455
+ .sort((a, b) => b.silentMs - a.silentMs || b.heldMs - a.heldMs);
2456
+ // Ranked by the total time the user spent on it: held plus synchronous work.
2457
+ const interactions = [...feedbackInteractions.values()]
2458
+ .map(bucket => ({ ...bucket.row }))
2459
+ .sort((a, b) => b.heldMs + b.selfMs - (a.heldMs + a.selfMs));
2460
+ return { sources, interactions, navigations, flights, fallbacks };
2461
+ }
2462
+
2463
+ /**
2464
+ * Point queries over the engine's live state — the devtools/console view of
2465
+ * one scope. Their own module so a records-only consumer never ships them;
2466
+ * they read the engine's ring buffer and the graph, and register nothing.
2467
+ */
2468
+ /** The node behind a memo/effect accessor, or the raw node passed through. */
2469
+ function nodeOf(target) {
2470
+ return target?.[$REFRESH] ?? target;
2471
+ }
2472
+ /** Re-run history for one node — pass a memo/effect accessor or raw node. */
2473
+ function why(target) {
2474
+ const node = nodeOf(target);
2475
+ return attribution.history().filter(event => subjectOf(event) === node);
2476
+ }
2477
+ /** Current dependency names of one scope — the devtools subscription view. */
2478
+ function subscriptions(target) {
2479
+ const node = nodeOf(target);
2480
+ const names = [];
2481
+ for (let l = node?._deps ?? null; l !== null; l = l._nextDep) names.push(nodeName(l._dep));
2482
+ return names;
2483
+ }
2484
+
2485
+ export { attribution, costs, feedback, formatOrigin, formatRerun, subscriptions, why };