@ogygia/devtools 0.12.0 → 0.13.0

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 (79) hide show
  1. package/dist/app.d.ts +3 -0
  2. package/dist/app.js +9 -0
  3. package/dist/devtools/BoundaryOverlay.svelte +10 -6
  4. package/dist/devtools/Devtools.svelte +24 -18
  5. package/dist/devtools/IslandDetail.svelte +7 -5
  6. package/dist/devtools/PageTab.svelte +4 -2
  7. package/dist/devtools/ProfilerTab.svelte +8 -2
  8. package/dist/devtools/SessionTab.svelte +21 -8
  9. package/dist/devtools/highlight.js +14 -7
  10. package/dist/devtools/ledger-dom.js +6 -9
  11. package/dist/devtools/names.js +18 -17
  12. package/dist/devtools/page.d.ts +2 -0
  13. package/dist/devtools/page.js +37 -17
  14. package/dist/devtools/profile-store.d.ts +4 -9
  15. package/dist/devtools/profile-store.js +26 -21
  16. package/dist/devtools/session.d.ts +2 -0
  17. package/dist/devtools/session.js +48 -42
  18. package/dist/devtools/testing-engine.js +10 -1
  19. package/dist/devtools/ui-app.d.ts +3 -1
  20. package/dist/devtools/ui-app.js +9 -3
  21. package/dist/devtools/ui.js +83 -40
  22. package/dist/messages/build.d.ts +21 -0
  23. package/dist/messages/build.js +56 -0
  24. package/dist/messages/index.d.ts +3 -0
  25. package/dist/messages/index.js +3 -0
  26. package/dist/messages/runtime.d.ts +63 -0
  27. package/dist/messages/runtime.js +122 -0
  28. package/dist/profiler/async-io.d.ts +12 -7
  29. package/dist/profiler/async-io.js +94 -82
  30. package/dist/profiler/beacon-standalone.js +15 -6
  31. package/dist/profiler/busy.d.ts +13 -0
  32. package/dist/profiler/busy.js +45 -0
  33. package/dist/profiler/client.js +5 -24
  34. package/dist/profiler/embedded-maps.js +9 -5
  35. package/dist/profiler/gc.d.ts +5 -0
  36. package/dist/profiler/gc.js +75 -11
  37. package/dist/profiler/index.d.ts +23 -2
  38. package/dist/profiler/index.js +648 -373
  39. package/dist/profiler/net.d.ts +14 -6
  40. package/dist/profiler/net.js +61 -45
  41. package/dist/profiler/patterns.js +29 -1
  42. package/dist/profiler/source-scan.js +24 -12
  43. package/dist/profiler/storage/index.d.ts +14 -3
  44. package/dist/profiler/storage/index.js +41 -41
  45. package/dist/profiler/storage/postgres.d.ts +18 -5
  46. package/dist/profiler/storage/postgres.js +124 -85
  47. package/dist/profiler/storage/redis.d.ts +18 -5
  48. package/dist/profiler/storage/redis.js +160 -131
  49. package/dist/profiler/storage/sqlite.d.ts +15 -2
  50. package/dist/profiler/storage/sqlite.js +54 -48
  51. package/dist/profiler/ui/ComponentsTable.svelte +2 -2
  52. package/dist/profiler/ui/Dashboard.svelte +2 -3
  53. package/dist/profiler/ui/DownloadHtml.svelte +1 -0
  54. package/dist/profiler/ui/ExportButton.svelte +1 -0
  55. package/dist/profiler/ui/Flame.svelte +10 -4
  56. package/dist/profiler/ui/Flame.svelte.d.ts +0 -5
  57. package/dist/profiler/ui/FrameDetails.svelte +13 -2
  58. package/dist/profiler/ui/FrameDetails.svelte.d.ts +0 -6
  59. package/dist/profiler/ui/FunctionsTable.svelte +2 -2
  60. package/dist/profiler/ui/IslandsTable.svelte +2 -2
  61. package/dist/profiler/ui/OnThisPage.svelte +12 -4
  62. package/dist/profiler/ui/RunView.svelte +31 -27
  63. package/dist/profiler/ui/ShareLink.svelte +6 -1
  64. package/dist/profiler/ui/Sidebar.svelte +14 -9
  65. package/dist/profiler/ui/Treemap.svelte +11 -4
  66. package/dist/profiler/ui/Treemap.svelte.d.ts +0 -6
  67. package/dist/profiler/ui/WaitingTable.svelte +2 -2
  68. package/dist/profiler/ui/fake-progress.svelte.d.ts +7 -12
  69. package/dist/profiler/ui/fake-progress.svelte.js +57 -36
  70. package/dist/profiler/ui/press.js +4 -2
  71. package/dist/profiler/ui/row-anchor.svelte.d.ts +0 -6
  72. package/dist/profiler/ui/row-anchor.svelte.js +13 -8
  73. package/dist/profiler/ui/sort.svelte.d.ts +9 -6
  74. package/dist/profiler/ui/sort.svelte.js +34 -28
  75. package/dist/profiler/visit.d.ts +0 -1
  76. package/dist/profiler/visit.js +1 -2
  77. package/dist/server.js +27 -10
  78. package/dist/vite.js +3 -2
  79. package/package.json +13 -4
@@ -1,4 +1,5 @@
1
1
  import { register_profiler_file } from "./frames.js";
2
+ import { Scope } from "@ogygia/machine";
2
3
  import { AsyncLocalStorage } from "node:async_hooks";
3
4
  export interface NetCall {
4
5
  /** performance.now() at call start */
@@ -71,7 +72,10 @@ export interface ServerTiming {
71
72
  desc?: string;
72
73
  }
73
74
  export declare function parse_server_timing(header: string | null | undefined): ServerTiming[] | undefined;
74
- export declare function set_stack_capture(on: boolean): void;
75
+ /** Turns `owner`'s ask for call stacks on or off. They are captured while any recording asks, so
76
+ * the first of two overlapping recordings to end (two profilers in one dev process) never turns
77
+ * them off under the other. */
78
+ export declare function set_stack_capture(owner: object, on: boolean): void;
75
79
  /** A caller location as the bundler sees it — resolved to source later, at
76
80
  * report time, through the sourcemap resolver (we have no maps here). */
77
81
  export interface CallerSite {
@@ -100,12 +104,15 @@ export interface NetContext {
100
104
  /** the profiler's what-if render: an answer kept from the timed renders, for a call this request
101
105
  * makes (`GET <url>`), instead of making it — undefined = make the call */
102
106
  answer?(method: string, url: string): Response | undefined;
107
+ /** the request's owner: what the capture reads for a call this request makes (the copy of its
108
+ * body it counts) is let go when the request ends */
109
+ owner?: Scope;
103
110
  }
104
111
  type Emit = (call: NetCall) => void;
105
- /** start (clearing what was kept) or stop keeping answers */
106
- export declare function keep_answers(on: boolean): void;
107
- /** drop every kept answer */
108
- export declare function drop_answers(): void;
112
+ /** `owner` starts (the first recording to start clears what was kept) or stops keeping answers */
113
+ export declare function keep_answers(owner: object, on: boolean): void;
114
+ /** `owner` is done with the kept answers: dropped once no recording reads them */
115
+ export declare function drop_answers(owner: object): void;
109
116
  /**
110
117
  * WHAT CHANGED IN AN ANSWER between the first render and the last: the one stretch that differs
111
118
  * (the bytes before it and after it are the same), and that stretch as text with a little around
@@ -121,7 +128,8 @@ export declare function kept_answer(key: string): Response | undefined;
121
128
  /**
122
129
  * Install the patches. `fallback` receives calls made outside any request
123
130
  * context (startup work, background jobs) so window recordings still see them.
124
- * Idempotent: a second call just re-asserts the fetch patch (see `ensure_fetch_patched`).
131
+ * A second call makes its own storage and fallback the live ones, and re-asserts the fetch patch
132
+ * (see `ensure_fetch_patched`).
125
133
  */
126
134
  export declare function install_net_capture(als: AsyncLocalStorage<NetContext>, fallback: Emit): Promise<void>;
127
135
  /**
@@ -66,8 +66,14 @@ function parse_server_timing(header) {
66
66
  }
67
67
  const NET_STATE = Symbol.for("ogygia.profiler.net-state");
68
68
  const shared = globalThis[NET_STATE] ??= { stacks: false };
69
- function set_stack_capture(on) {
70
- shared.stacks = on;
69
+ /** Turns `owner`'s ask for call stacks on or off. They are captured while any recording asks, so
70
+ * the first of two overlapping recordings to end (two profilers in one dev process) never turns
71
+ * them off under the other. */
72
+ function set_stack_capture(owner, on) {
73
+ const owners = shared.owners ??= /* @__PURE__ */ new Set();
74
+ if (on) owners.add(owner);
75
+ else owners.delete(owner);
76
+ shared.stacks = owners.size > 0;
71
77
  }
72
78
  register_profiler_file();
73
79
  /** The nearest first-party frame that issued an I/O call — skips node internals,
@@ -111,10 +117,10 @@ function nearest_app_frame() {
111
117
  if (!s) return void 0;
112
118
  return `${s.fn} (${base_name(s.file)}:${s.line})`;
113
119
  }
114
- let installed = false;
115
- let net_emit = null;
116
- /** the current request's kept-answer lookup (its context's `answer`), set by install */
117
- let net_answer = null;
120
+ /** A call goes to the live profiler (nowhere before one installed). */
121
+ function emit(call) {
122
+ shared.emit?.(call);
123
+ }
118
124
  const KEEP_ONE = 1048576;
119
125
  const KEEP_ALL = 8388608;
120
126
  let keep_bodies = false;
@@ -122,21 +128,32 @@ let kept_bytes = 0;
122
128
  const kept = /* @__PURE__ */ new Map();
123
129
  /** each GET's FIRST answer in the recording too: against the last, what changed between renders */
124
130
  const kept_first = /* @__PURE__ */ new Map();
125
- /** start (clearing what was kept) or stop keeping answers */
126
- function keep_answers(on) {
127
- keep_bodies = on;
128
- if (on) {
129
- kept.clear();
130
- kept_first.clear();
131
- kept_bytes = 0;
132
- }
133
- }
134
- /** drop every kept answer */
135
- function drop_answers() {
131
+ /** The recordings keeping answers now, and the ones still reading what was kept (a recording stops
132
+ * keeping after its timed renders, and reads them in its keep-the-answers renders after). Each
133
+ * recording turns only its own on and off: one that ends never stops or drops another's. */
134
+ const keepers = /* @__PURE__ */ new Set();
135
+ const readers = /* @__PURE__ */ new Set();
136
+ function clear_kept() {
136
137
  kept.clear();
137
138
  kept_first.clear();
138
139
  kept_bytes = 0;
139
140
  }
141
+ /** `owner` starts (the first recording to start clears what was kept) or stops keeping answers */
142
+ function keep_answers(owner, on) {
143
+ if (on) {
144
+ if (readers.size === 0) clear_kept();
145
+ keepers.add(owner);
146
+ readers.add(owner);
147
+ } else keepers.delete(owner);
148
+ keep_bodies = keepers.size > 0;
149
+ }
150
+ /** `owner` is done with the kept answers: dropped once no recording reads them */
151
+ function drop_answers(owner) {
152
+ keepers.delete(owner);
153
+ readers.delete(owner);
154
+ keep_bodies = keepers.size > 0;
155
+ if (readers.size === 0) clear_kept();
156
+ }
140
157
  /**
141
158
  * WHAT CHANGED IN AN ANSWER between the first render and the last: the one stretch that differs
142
159
  * (the bytes before it and after it are the same), and that stretch as text with a little around
@@ -176,11 +193,13 @@ const OG_FETCH_PATCH = Symbol.for("ogygia.profiler.net.fetch-patch");
176
193
  /**
177
194
  * Install the patches. `fallback` receives calls made outside any request
178
195
  * context (startup work, background jobs) so window recordings still see them.
179
- * Idempotent: a second call just re-asserts the fetch patch (see `ensure_fetch_patched`).
196
+ * A second call makes its own storage and fallback the live ones, and re-asserts the fetch patch
197
+ * (see `ensure_fetch_patched`).
180
198
  */
181
199
  async function install_net_capture(als, fallback) {
182
- net_answer = (method, url) => als.getStore()?.answer?.(method, url);
183
- net_emit = (call) => {
200
+ shared.answer = (method, url) => als.getStore()?.answer?.(method, url);
201
+ shared.owner = () => als.getStore()?.owner;
202
+ shared.emit = (call) => {
184
203
  const ctx = als.getStore();
185
204
  if (ctx) {
186
205
  call.route = ctx.route;
@@ -188,13 +207,10 @@ async function install_net_capture(als, fallback) {
188
207
  ctx.on_net(call);
189
208
  } else fallback(call);
190
209
  };
191
- if (installed) {
192
- ensure_fetch_patched();
193
- return;
194
- }
195
- installed = true;
196
- patch_fetch(net_emit);
197
- await patch_http(net_emit);
210
+ patch_fetch();
211
+ if (shared.http_patched) return;
212
+ shared.http_patched = true;
213
+ await patch_http();
198
214
  }
199
215
  /**
200
216
  * Re-assert the `globalThis.fetch` patch. `globalThis.fetch` can be REPLACED out from under us after
@@ -205,10 +221,7 @@ async function install_net_capture(als, fallback) {
205
221
  * call every request/run.
206
222
  */
207
223
  function ensure_fetch_patched() {
208
- if (!net_emit) return;
209
- const cur = globalThis.fetch;
210
- if (typeof cur === "function" && cur[OG_FETCH_PATCH]) return;
211
- patch_fetch(net_emit);
224
+ if (shared.emit) patch_fetch();
212
225
  }
213
226
  const INTERNAL_HEADER = "x-og-profiler-internal";
214
227
  function is_internal(input, init) {
@@ -226,11 +239,11 @@ function host_of(url) {
226
239
  return "";
227
240
  }
228
241
  }
229
- function patch_fetch(emit) {
242
+ function patch_fetch() {
230
243
  const orig = globalThis.fetch;
231
244
  if (typeof orig !== "function") return;
232
245
  if (orig[OG_FETCH_PATCH]) return;
233
- globalThis.fetch = wrap_fetch(orig, emit);
246
+ globalThis.fetch = wrap_fetch(orig);
234
247
  }
235
248
  /**
236
249
  * KIT'S OWN `event.fetch` never reaches the global one for a same-origin call: it dispatches the
@@ -239,8 +252,8 @@ function patch_fetch(emit) {
239
252
  * the same recorder. Idempotent (the brand), nothing when capture is not installed.
240
253
  */
241
254
  function wrap_event_fetch(f) {
242
- if (!net_emit || typeof f !== "function" || f[OG_FETCH_PATCH]) return f;
243
- return wrap_fetch(f, net_emit);
255
+ if (!shared.emit || typeof f !== "function" || f[OG_FETCH_PATCH]) return f;
256
+ return wrap_fetch(f);
244
257
  }
245
258
  /** the request header that asks, and the response header that answers */
246
259
  const TRACE_HEADER = "x-og-trace";
@@ -281,13 +294,13 @@ function decode_trace(h) {
281
294
  }
282
295
  }
283
296
  /** The recording wrapper around a fetch-shaped function: one NetCall per call, timed to headers
284
- * and body, the caller captured while a recording runs. */
285
- function wrap_fetch(orig, emit) {
297
+ * and body, the caller captured while a recording runs, handed to the live profiler. */
298
+ function wrap_fetch(orig) {
286
299
  const patched = async function fetch(input, init) {
287
300
  if (is_internal(input, init)) return orig(input, init);
288
301
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input?.url ?? "";
289
302
  const method = init?.method ?? (input?.method || "GET");
290
- const canned = net_answer?.(method.toUpperCase(), url);
303
+ const canned = shared.answer?.(method.toUpperCase(), url);
291
304
  if (canned) return canned;
292
305
  const call = {
293
306
  start: performance.now(),
@@ -335,7 +348,7 @@ function wrap_fetch(orig, emit) {
335
348
  call.timings = parse_server_timing(res.headers.get("server-timing"));
336
349
  const trace = decode_trace(res.headers.get(TRACE_HEADER));
337
350
  if (trace) call.trace = trace;
338
- count_decoded(res, call);
351
+ count_decoded(res, call, shared.owner?.());
339
352
  wrap_body(res, call);
340
353
  return res;
341
354
  } catch (e) {
@@ -419,8 +432,10 @@ function capture_req_payload(init, call) {
419
432
  /** a body is fingerprinted over this many bytes at most (with its full length) */
420
433
  const BODY_HASH_MAX = 4194304;
421
434
  /** Count the DECODED response body size by draining a CLONE in the background (the original is left
422
- * untouched). Sets `call.bytes` when done — the render finishes first, so it's ready by report time. */
423
- function count_decoded(res, call) {
435
+ * untouched). Sets `call.bytes` when done — the render finishes first, so it's ready by report time.
436
+ * The clone is the request's (`owner`): when the request ends it is let go, so a body that never
437
+ * ends is not read for good. */
438
+ function count_decoded(res, call, owner) {
424
439
  let clone;
425
440
  try {
426
441
  clone = res.clone();
@@ -434,6 +449,7 @@ function count_decoded(res, call) {
434
449
  (async () => {
435
450
  try {
436
451
  const reader = stream.getReader();
452
+ owner?.defer(() => void reader.cancel().catch(() => {}));
437
453
  let total = 0;
438
454
  let parts = keep ? [] : null;
439
455
  let h = 2166136261;
@@ -481,7 +497,7 @@ function count_decoded(res, call) {
481
497
  } catch {}
482
498
  })();
483
499
  }
484
- async function patch_http(emit) {
500
+ async function patch_http() {
485
501
  for (const proto of ["http", "https"]) {
486
502
  let mod;
487
503
  try {
@@ -498,18 +514,18 @@ async function patch_http(emit) {
498
514
  try {
499
515
  mod.request = function(...args) {
500
516
  const req = orig_request.apply(this, args);
501
- instrument_client_request(req, args, proto, emit);
517
+ instrument_client_request(req, args, proto);
502
518
  return req;
503
519
  };
504
520
  if (typeof orig_get === "function") mod.get = function(...args) {
505
521
  const req = orig_get.apply(this, args);
506
- instrument_client_request(req, args, proto, emit);
522
+ instrument_client_request(req, args, proto);
507
523
  return req;
508
524
  };
509
525
  } catch {}
510
526
  }
511
527
  }
512
- function instrument_client_request(req, args, proto, emit) {
528
+ function instrument_client_request(req, args, proto) {
513
529
  const r = req;
514
530
  if (typeof r?.on !== "function") return;
515
531
  let url = "";
@@ -1334,6 +1334,33 @@ const CHAIN_GAP_MS = 10;
1334
1334
  const CHAIN_MIN = 3;
1335
1335
  const CHAIN_MIN_MS = 20;
1336
1336
  /**
1337
+ * The calls that started together (under 1 ms apart) with another call from the same calling line:
1338
+ * one of a batch run in parallel (a `Promise.all`). Each calling line's calls are sorted by start
1339
+ * once, so the call that started nearest to one is beside it.
1340
+ */
1341
+ function batched_calls(calls) {
1342
+ const by_line = /* @__PURE__ */ new Map();
1343
+ for (const c of calls) {
1344
+ const line = c.outer?.line ?? c.at?.line;
1345
+ if (Number.isNaN(line) || !Number.isFinite(c.start)) continue;
1346
+ const path = c.outer?.path ?? c.at?.path;
1347
+ let lines = by_line.get(path);
1348
+ if (!lines) by_line.set(path, lines = /* @__PURE__ */ new Map());
1349
+ let same = lines.get(line);
1350
+ if (!same) lines.set(line, same = /* @__PURE__ */ new Set());
1351
+ same.add(c);
1352
+ }
1353
+ const out = /* @__PURE__ */ new Set();
1354
+ for (const lines of by_line.values()) for (const same of lines.values()) {
1355
+ const sorted = [...same].sort((a, b) => a.start - b.start);
1356
+ for (let i = 1; i < sorted.length; i++) if (Math.abs(sorted[i].start - sorted[i - 1].start) < 1) {
1357
+ out.add(sorted[i]);
1358
+ out.add(sorted[i - 1]);
1359
+ }
1360
+ }
1361
+ return out;
1362
+ }
1363
+ /**
1337
1364
  * THE WAITS IN A ROW: calls made from ONE line whose waits did not overlap — each started after
1338
1365
  * the one before it ended — so the line waited for them one by one (`for (…) await fetch(…)`).
1339
1366
  * Measured, not guessed: the sum of the waits is what the line cost in wall time, the longest
@@ -1367,7 +1394,8 @@ function find_wait_patterns(input) {
1367
1394
  if (g.length < 2) continue;
1368
1395
  g.sort((a, b) => a.start - b.start);
1369
1396
  const runs = [];
1370
- const batched = (c) => g.some((o) => o !== c && Math.abs(o.start - c.start) < 1 && (o.outer?.line ?? o.at?.line) === (c.outer?.line ?? c.at?.line) && (o.outer?.path ?? o.at?.path) === (c.outer?.path ?? c.at?.path));
1397
+ const in_batch = batched_calls(g);
1398
+ const batched = (c) => in_batch.has(c);
1371
1399
  let run = batched(g[0]) ? [] : [g[0]];
1372
1400
  for (let i = 1; i < g.length; i++) {
1373
1401
  if (batched(g[i])) {
@@ -1049,6 +1049,7 @@ function constant_work(source, spans = false) {
1049
1049
  });
1050
1050
  }
1051
1051
  }
1052
+ const helper_names = new Set(helpers.map((h) => h.name));
1052
1053
  const impure = (t) => {
1053
1054
  for (let k = 0; k < t.length; k++) {
1054
1055
  const x = t[k];
@@ -1056,21 +1057,31 @@ function constant_work(source, spans = false) {
1056
1057
  if (per_call.has(x) || PER_CALL_GLOBALS.has(x)) return true;
1057
1058
  if (x === "Math" && t[k + 2] === "random") return true;
1058
1059
  if (x === "Date" && (t[k + 2] === "now" || t[k - 1] === "new" && t[k + 1] === "(" && t[k + 2] === ")")) return true;
1059
- if (helpers.some((h) => h.name === x) && !module_names.has(x)) return true;
1060
+ if (helper_names.has(x) && !module_names.has(x)) return true;
1060
1061
  }
1061
1062
  return changed_names(t).some((n) => state.has(n));
1062
1063
  };
1063
- for (let dropped = true; dropped;) {
1064
- dropped = false;
1065
- for (const h of helpers) {
1066
- if (!module_names.has(h.name)) continue;
1067
- for (let i = h.from; i <= h.to; i++) if (impure(T[i])) {
1068
- module_names.delete(h.name);
1069
- dropped = true;
1070
- break;
1071
- }
1064
+ const named_by = /* @__PURE__ */ new Map();
1065
+ for (const h of helpers) for (let i = h.from; i <= h.to; i++) {
1066
+ const t = T[i];
1067
+ for (let k = 0; k < t.length; k++) if (helper_names.has(t[k]) && t[k - 1] !== "." && t[k - 1] !== "?.") {
1068
+ const by = named_by.get(t[k]);
1069
+ if (by) by.push(h.name);
1070
+ else named_by.set(t[k], [h.name]);
1072
1071
  }
1073
1072
  }
1073
+ const dropped = [];
1074
+ for (const h of helpers) {
1075
+ if (!module_names.has(h.name)) continue;
1076
+ for (let i = h.from; i <= h.to; i++) if (impure(T[i])) {
1077
+ dropped.push(h.name);
1078
+ break;
1079
+ }
1080
+ }
1081
+ while (dropped.length) {
1082
+ const name = dropped.pop();
1083
+ if (module_names.delete(name)) for (const by of named_by.get(name) ?? []) dropped.push(by);
1084
+ }
1074
1085
  const changed = /* @__PURE__ */ new Set();
1075
1086
  for (let i = body0 + 1; i < body1; i++) for (const n of changed_names(T[i])) changed.add(n);
1076
1087
  const local = /* @__PURE__ */ new Set();
@@ -1163,8 +1174,9 @@ function constant_work(source, spans = false) {
1163
1174
  });
1164
1175
  }
1165
1176
  }
1166
- const mine = stmt_lines;
1167
- for (const o of out) o.calls = o.calls.filter((c) => !T.some((t, i) => i > body0 && i < body1 && !mine.has(i) && t.includes(c)));
1177
+ const elsewhere = /* @__PURE__ */ new Set();
1178
+ for (let i = body0 + 1; i < body1; i++) if (!stmt_lines.has(i)) for (const x of T[i]) elsewhere.add(x);
1179
+ for (const o of out) o.calls = o.calls.filter((c) => !elsewhere.has(c));
1168
1180
  return out;
1169
1181
  }
1170
1182
  /**
@@ -13,8 +13,8 @@
13
13
  *
14
14
  * // src/hooks.server.ts
15
15
  * import { setProfilerStore } from '@ogygia/devtools/profiler/storage';
16
- * import { sqliteStore } from '@ogygia/devtools/profiler/storage/sqlite';
17
- * setProfilerStore(sqliteStore('.ogygia/profiles.db'));
16
+ * import { SqliteStore } from '@ogygia/devtools/profiler/storage/sqlite';
17
+ * setProfilerStore(new SqliteStore('.ogygia/profiles.db'));
18
18
  *
19
19
  * A report is stored as its portable dump (the same JSON the `.ogp` export and the agent view are
20
20
  * built from): `meta` + `analysis` + `extras`, plus a few summary columns for the list. Nothing
@@ -79,7 +79,18 @@ export interface ProfilerStore {
79
79
  /** An in-memory store — a Map with an LRU cap. The profiler without a store keeps reports in its own
80
80
  * memory already, so this adds nothing there; it is for tests and a model for a custom store.
81
81
  * Nothing survives a restart. */
82
- export declare function memoryStore(cap?: number): ProfilerStore;
82
+ export declare class MemoryStore implements ProfilerStore {
83
+ #private;
84
+ readonly kind: string;
85
+ constructor(cap?: number);
86
+ putReport(rec: ProfilerRecord): void;
87
+ getReport(id: string): string | null;
88
+ listReports(limit?: number): ProfilerSummary[];
89
+ deleteReport(id: string): void;
90
+ prune(keep: number): void;
91
+ putVisit(rec: ProfilerVisitRecord): void;
92
+ listVisits(page: string, limit?: number): ProfilerVisitRecord[];
93
+ }
83
94
  /** Pull the summary columns off a record for storing them alongside the dump. */
84
95
  export declare function recordColumns(rec: ProfilerRecord): ProfilerSummary;
85
96
  export declare function setProfilerStore(store: ProfilerStore): void;
@@ -2,45 +2,45 @@
2
2
  /** An in-memory store — a Map with an LRU cap. The profiler without a store keeps reports in its own
3
3
  * memory already, so this adds nothing there; it is for tests and a model for a custom store.
4
4
  * Nothing survives a restart. */
5
- function memoryStore(cap = 50) {
6
- const reports = /* @__PURE__ */ new Map();
7
- const visits = [];
8
- return {
9
- kind: "memory",
10
- putReport(rec) {
11
- reports.delete(rec.id);
12
- reports.set(rec.id, rec);
13
- while (reports.size > cap) {
14
- const oldest = reports.keys().next().value;
15
- if (oldest === void 0) break;
16
- reports.delete(oldest);
17
- }
18
- },
19
- getReport(id) {
20
- return reports.get(id)?.dump ?? null;
21
- },
22
- listReports(limit = 30) {
23
- return [...reports.values()].sort((a, b) => b.created - a.created).slice(0, limit).map(({ dump: _dump, ...s }) => s);
24
- },
25
- deleteReport(id) {
26
- reports.delete(id);
27
- },
28
- prune(keep) {
29
- while (reports.size > keep) {
30
- const oldest = reports.keys().next().value;
31
- if (oldest === void 0) break;
32
- reports.delete(oldest);
33
- }
34
- },
35
- putVisit(rec) {
36
- visits.push(rec);
37
- if (visits.length > 2e3) visits.shift();
38
- },
39
- listVisits(page, limit = 10) {
40
- return visits.filter((v) => v.page === page).sort((a, b) => b.at - a.at).slice(0, limit);
5
+ var MemoryStore = class {
6
+ kind = "memory";
7
+ #cap;
8
+ #reports = /* @__PURE__ */ new Map();
9
+ #visits = [];
10
+ constructor(cap = 50) {
11
+ this.#cap = cap;
12
+ }
13
+ putReport(rec) {
14
+ this.#reports.delete(rec.id);
15
+ this.#reports.set(rec.id, rec);
16
+ this.prune(this.#cap);
17
+ }
18
+ getReport(id) {
19
+ return this.#reports.get(id)?.dump ?? null;
20
+ }
21
+ listReports(limit = 30) {
22
+ return [...this.#reports.values()].sort((a, b) => b.created - a.created).slice(0, limit).map(({ dump: _dump, ...s }) => s);
23
+ }
24
+ deleteReport(id) {
25
+ this.#reports.delete(id);
26
+ }
27
+ prune(keep) {
28
+ const reports = this.#reports;
29
+ while (reports.size > keep) {
30
+ const oldest = reports.keys().next().value;
31
+ if (oldest === void 0) break;
32
+ reports.delete(oldest);
41
33
  }
42
- };
43
- }
34
+ }
35
+ putVisit(rec) {
36
+ const visits = this.#visits;
37
+ visits.push(rec);
38
+ if (visits.length > 2e3) visits.shift();
39
+ }
40
+ listVisits(page, limit = 10) {
41
+ return this.#visits.filter((v) => v.page === page).sort((a, b) => b.at - a.at).slice(0, limit);
42
+ }
43
+ };
44
44
  /** Pull the summary columns off a record for storing them alongside the dump. */
45
45
  function recordColumns(rec) {
46
46
  return {
@@ -59,8 +59,8 @@ function recordColumns(rec) {
59
59
  *
60
60
  * // src/hooks.server.ts
61
61
  * import { setProfilerStore } from '@ogygia/devtools/profiler/storage';
62
- * import { postgresStore } from '@ogygia/devtools/profiler/storage/postgres';
63
- * setProfilerStore(postgresStore(process.env.DATABASE_URL!));
62
+ * import { PostgresStore } from '@ogygia/devtools/profiler/storage/postgres';
63
+ * setProfilerStore(new PostgresStore(process.env.DATABASE_URL!));
64
64
  *
65
65
  * The profiler reads this when it mounts and every finished report is written there. Call it before
66
66
  * the profiler handles its first request; a later call is ignored (the instance already mounted).
@@ -73,4 +73,4 @@ function setProfilerStore(store) {
73
73
  function getProfilerStore() {
74
74
  return globalThis[STORE_SLOT];
75
75
  }
76
- export { getProfilerStore, memoryStore, recordColumns, setProfilerStore };
76
+ export { MemoryStore, getProfilerStore, recordColumns, setProfilerStore };
@@ -1,12 +1,25 @@
1
- import { ProfilerStore } from "./index.js";
1
+ import { ProfilerRecord, ProfilerStore, ProfilerSummary, ProfilerVisitRecord } from "./index.js";
2
2
  interface PgLike {
3
3
  query(text: string, params?: unknown[]): Promise<{
4
4
  rows: Record<string, unknown>[];
5
5
  }>;
6
6
  end?(): Promise<void>;
7
7
  }
8
- export declare function postgresStore(client: PgLike | string, opts?: {
9
- schema?: string;
10
- table?: string;
11
- }): ProfilerStore;
8
+ export declare class PostgresStore implements ProfilerStore {
9
+ #private;
10
+ readonly kind: string;
11
+ constructor(client: PgLike | string, opts?: {
12
+ schema?: string;
13
+ table?: string;
14
+ });
15
+ init(): Promise<void>;
16
+ putReport(rec: ProfilerRecord): Promise<void>;
17
+ getReport(id: string): Promise<string | null>;
18
+ listReports(limit?: number): Promise<ProfilerSummary[]>;
19
+ deleteReport(id: string): Promise<void>;
20
+ prune(keep: number): Promise<void>;
21
+ putVisit(rec: ProfilerVisitRecord): Promise<void>;
22
+ listVisits(page: string, limit?: number): Promise<ProfilerVisitRecord[]>;
23
+ close(): Promise<void>;
24
+ }
12
25
  export {};