@ogygia/devtools 0.11.4 → 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 (81) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +3 -0
  3. package/dist/app.js +9 -0
  4. package/dist/devtools/BoundaryOverlay.svelte +10 -6
  5. package/dist/devtools/Devtools.svelte +24 -18
  6. package/dist/devtools/IslandDetail.svelte +7 -5
  7. package/dist/devtools/PageTab.svelte +4 -2
  8. package/dist/devtools/ProfilerTab.svelte +8 -2
  9. package/dist/devtools/SessionTab.svelte +21 -8
  10. package/dist/devtools/highlight.js +14 -7
  11. package/dist/devtools/ledger-dom.js +6 -9
  12. package/dist/devtools/names.js +18 -17
  13. package/dist/devtools/page.d.ts +2 -0
  14. package/dist/devtools/page.js +37 -17
  15. package/dist/devtools/profile-store.d.ts +4 -9
  16. package/dist/devtools/profile-store.js +26 -21
  17. package/dist/devtools/session.d.ts +2 -0
  18. package/dist/devtools/session.js +48 -42
  19. package/dist/devtools/testing-engine.js +10 -1
  20. package/dist/devtools/ui-app.d.ts +3 -1
  21. package/dist/devtools/ui-app.js +9 -3
  22. package/dist/devtools/ui.js +83 -40
  23. package/dist/messages/build.d.ts +21 -0
  24. package/dist/messages/build.js +56 -0
  25. package/dist/messages/index.d.ts +3 -0
  26. package/dist/messages/index.js +3 -0
  27. package/dist/messages/runtime.d.ts +63 -0
  28. package/dist/messages/runtime.js +122 -0
  29. package/dist/profiler/async-io.d.ts +12 -7
  30. package/dist/profiler/async-io.js +94 -82
  31. package/dist/profiler/beacon-standalone.js +15 -6
  32. package/dist/profiler/busy.d.ts +13 -0
  33. package/dist/profiler/busy.js +45 -0
  34. package/dist/profiler/client.js +5 -24
  35. package/dist/profiler/embedded-maps.js +9 -5
  36. package/dist/profiler/gc.d.ts +5 -0
  37. package/dist/profiler/gc.js +75 -11
  38. package/dist/profiler/index.d.ts +23 -2
  39. package/dist/profiler/index.js +648 -373
  40. package/dist/profiler/net.d.ts +14 -6
  41. package/dist/profiler/net.js +61 -45
  42. package/dist/profiler/patterns.js +29 -1
  43. package/dist/profiler/source-scan.js +24 -12
  44. package/dist/profiler/standalone.js +12 -4
  45. package/dist/profiler/storage/index.d.ts +14 -3
  46. package/dist/profiler/storage/index.js +41 -41
  47. package/dist/profiler/storage/postgres.d.ts +18 -5
  48. package/dist/profiler/storage/postgres.js +124 -85
  49. package/dist/profiler/storage/redis.d.ts +18 -5
  50. package/dist/profiler/storage/redis.js +160 -131
  51. package/dist/profiler/storage/sqlite.d.ts +15 -2
  52. package/dist/profiler/storage/sqlite.js +54 -48
  53. package/dist/profiler/ui/ComponentsTable.svelte +2 -2
  54. package/dist/profiler/ui/Dashboard.svelte +2 -3
  55. package/dist/profiler/ui/DownloadHtml.svelte +1 -0
  56. package/dist/profiler/ui/ExportButton.svelte +1 -0
  57. package/dist/profiler/ui/Flame.svelte +10 -4
  58. package/dist/profiler/ui/Flame.svelte.d.ts +0 -5
  59. package/dist/profiler/ui/FrameDetails.svelte +13 -2
  60. package/dist/profiler/ui/FrameDetails.svelte.d.ts +0 -6
  61. package/dist/profiler/ui/FunctionsTable.svelte +2 -2
  62. package/dist/profiler/ui/IslandsTable.svelte +2 -2
  63. package/dist/profiler/ui/OnThisPage.svelte +12 -4
  64. package/dist/profiler/ui/RunView.svelte +31 -27
  65. package/dist/profiler/ui/ShareLink.svelte +6 -1
  66. package/dist/profiler/ui/Sidebar.svelte +14 -9
  67. package/dist/profiler/ui/Treemap.svelte +11 -4
  68. package/dist/profiler/ui/Treemap.svelte.d.ts +0 -6
  69. package/dist/profiler/ui/WaitingTable.svelte +2 -2
  70. package/dist/profiler/ui/fake-progress.svelte.d.ts +7 -12
  71. package/dist/profiler/ui/fake-progress.svelte.js +57 -36
  72. package/dist/profiler/ui/press.js +4 -2
  73. package/dist/profiler/ui/row-anchor.svelte.d.ts +0 -6
  74. package/dist/profiler/ui/row-anchor.svelte.js +13 -8
  75. package/dist/profiler/ui/sort.svelte.d.ts +9 -6
  76. package/dist/profiler/ui/sort.svelte.js +34 -28
  77. package/dist/profiler/visit.d.ts +0 -1
  78. package/dist/profiler/visit.js +1 -2
  79. package/dist/server.js +27 -10
  80. package/dist/vite.js +3 -2
  81. package/package.json +18 -9
@@ -1,94 +1,133 @@
1
1
  import { MAX_VISITS_PER_KEY } from "./visits.js";
2
+ import { STALE, flights } from "@ogygia/machine";
2
3
  // src/profiler/storage/postgres.ts
3
4
  async function resolve(client) {
4
5
  if (typeof client !== "string") return client;
5
6
  const pg = await import("pg");
6
7
  const Pool = pg.Pool ?? pg.default?.Pool;
7
- if (!Pool) throw new Error("ogygia/profiler: postgresStore needs the 'pg' package.");
8
+ if (!Pool) throw new Error("ogygia/profiler: PostgresStore needs the 'pg' package.");
8
9
  return new Pool({ connectionString: client });
9
10
  }
10
- function postgresStore(client, opts = {}) {
11
- const base = opts.table ?? "ogygia";
12
- const q = (n) => opts.schema ? `"${opts.schema}"."${base}_${n}"` : `"${base}_${n}"`;
13
- const R = q("reports");
14
- const V = q("visits");
15
- let c = null;
16
- let ready = null;
17
- const need = async () => {
18
- c ??= await resolve(client);
19
- ready ??= (async () => {
20
- if (opts.schema) await c.query(`CREATE SCHEMA IF NOT EXISTS "${opts.schema}"`);
21
- await c.query(`CREATE TABLE IF NOT EXISTS ${R} (id TEXT PRIMARY KEY, created BIGINT NOT NULL, page TEXT, trigger TEXT, label TEXT, median DOUBLE PRECISION, dump JSONB NOT NULL)`);
22
- await c.query(`CREATE INDEX IF NOT EXISTS ${base}_reports_created ON ${R} (created DESC)`);
23
- await c.query(`CREATE TABLE IF NOT EXISTS ${V} (key TEXT PRIMARY KEY, page TEXT NOT NULL, at BIGINT NOT NULL, visit JSONB NOT NULL)`);
24
- await c.query(`CREATE INDEX IF NOT EXISTS ${base}_visits_page ON ${V} (page, at DESC)`);
25
- })();
26
- await ready;
11
+ var PostgresStore = class {
12
+ kind = "postgres";
13
+ #client;
14
+ #schema;
15
+ #base;
16
+ #R;
17
+ #V;
18
+ /** the client, made once: two first calls share it, and a failure to make it is not kept */
19
+ #connect;
20
+ /** the client made, for `close` */
21
+ #made = null;
22
+ /** the tables, made once: a setup that failed (a connection blip) is made again by the next call */
23
+ #setup;
24
+ constructor(client, opts = {}) {
25
+ const base = opts.table ?? "ogygia";
26
+ const q = (n) => opts.schema ? `"${opts.schema}"."${base}_${n}"` : `"${base}_${n}"`;
27
+ this.#client = client;
28
+ this.#schema = opts.schema;
29
+ this.#base = base;
30
+ this.#R = q("reports");
31
+ this.#V = q("visits");
32
+ this.#connect = this.#connector();
33
+ this.#setup = flights({
34
+ name: "profiler postgres tables",
35
+ run: () => this.#create_tables()
36
+ });
37
+ }
38
+ async init() {
39
+ await this.#need();
40
+ }
41
+ async putReport(rec) {
42
+ await (await this.#need()).query(`INSERT INTO ${this.#R} (id,created,page,trigger,label,median,dump) VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb)
43
+ ON CONFLICT (id) DO UPDATE SET created=EXCLUDED.created,page=EXCLUDED.page,trigger=EXCLUDED.trigger,label=EXCLUDED.label,median=EXCLUDED.median,dump=EXCLUDED.dump`, [
44
+ rec.id,
45
+ rec.created,
46
+ rec.page,
47
+ rec.trigger,
48
+ rec.label,
49
+ rec.median,
50
+ rec.dump
51
+ ]);
52
+ }
53
+ async getReport(id) {
54
+ const { rows } = await (await this.#need()).query(`SELECT dump FROM ${this.#R} WHERE id = $1`, [id]);
55
+ if (!rows.length) return null;
56
+ const d = rows[0].dump;
57
+ return typeof d === "string" ? d : JSON.stringify(d);
58
+ }
59
+ async listReports(limit = 30) {
60
+ const { rows } = await (await this.#need()).query(`SELECT id,created,page,trigger,label,median FROM ${this.#R} ORDER BY created DESC LIMIT $1`, [limit]);
61
+ return rows.map((r) => ({
62
+ id: r.id,
63
+ created: Number(r.created),
64
+ page: r.page ?? null,
65
+ trigger: r.trigger,
66
+ label: r.label,
67
+ median: r.median === null ? null : Number(r.median)
68
+ }));
69
+ }
70
+ async deleteReport(id) {
71
+ await (await this.#need()).query(`DELETE FROM ${this.#R} WHERE id = $1`, [id]);
72
+ }
73
+ async prune(keep) {
74
+ const R = this.#R;
75
+ await (await this.#need()).query(`DELETE FROM ${R} WHERE id NOT IN (SELECT id FROM ${R} ORDER BY created DESC LIMIT $1)`, [keep]);
76
+ }
77
+ async putVisit(rec) {
78
+ const V = this.#V;
79
+ const db = await this.#need();
80
+ await db.query(`INSERT INTO ${V} (key,page,at,visit) VALUES ($1,$2,$3,$4::jsonb) ON CONFLICT (key) DO UPDATE SET at=EXCLUDED.at,visit=EXCLUDED.visit`, [
81
+ rec.key,
82
+ rec.page,
83
+ rec.at,
84
+ rec.visit
85
+ ]);
86
+ await db.query(`DELETE FROM ${V} WHERE page = $1 AND key NOT IN (SELECT key FROM ${V} WHERE page = $1 ORDER BY at DESC LIMIT $2)`, [rec.page, MAX_VISITS_PER_KEY]);
87
+ }
88
+ async listVisits(page, limit = 10) {
89
+ const { rows } = await (await this.#need()).query(`SELECT key,page,at,visit FROM ${this.#V} WHERE page = $1 ORDER BY at DESC LIMIT $2`, [page, limit]);
90
+ return rows.map((r) => ({
91
+ key: r.key,
92
+ page: r.page,
93
+ at: Number(r.at),
94
+ visit: typeof r.visit === "string" ? r.visit : JSON.stringify(r.visit)
95
+ }));
96
+ }
97
+ async close() {
98
+ const made = this.#made;
99
+ this.#made = null;
100
+ this.#connect = this.#connector();
101
+ await made?.end?.();
102
+ }
103
+ #connector() {
104
+ return flights({
105
+ name: "profiler postgres client",
106
+ run: async () => this.#made = await resolve(this.#client)
107
+ });
108
+ }
109
+ /** The client (nothing drops a connector, so it is never STALE; were it, the current one is
110
+ * asked). */
111
+ async #client_made() {
112
+ const c = await this.#connect.get();
113
+ return c === STALE ? this.#client_made() : c;
114
+ }
115
+ async #need() {
116
+ const c = await this.#client_made();
117
+ await this.#setup.get();
27
118
  return c;
28
- };
29
- return {
30
- kind: "postgres",
31
- async init() {
32
- await need();
33
- },
34
- async putReport(rec) {
35
- await (await need()).query(`INSERT INTO ${R} (id,created,page,trigger,label,median,dump) VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb)
36
- ON CONFLICT (id) DO UPDATE SET created=EXCLUDED.created,page=EXCLUDED.page,trigger=EXCLUDED.trigger,label=EXCLUDED.label,median=EXCLUDED.median,dump=EXCLUDED.dump`, [
37
- rec.id,
38
- rec.created,
39
- rec.page,
40
- rec.trigger,
41
- rec.label,
42
- rec.median,
43
- rec.dump
44
- ]);
45
- },
46
- async getReport(id) {
47
- const { rows } = await (await need()).query(`SELECT dump FROM ${R} WHERE id = $1`, [id]);
48
- if (!rows.length) return null;
49
- const d = rows[0].dump;
50
- return typeof d === "string" ? d : JSON.stringify(d);
51
- },
52
- async listReports(limit = 30) {
53
- const { rows } = await (await need()).query(`SELECT id,created,page,trigger,label,median FROM ${R} ORDER BY created DESC LIMIT $1`, [limit]);
54
- return rows.map((r) => ({
55
- id: r.id,
56
- created: Number(r.created),
57
- page: r.page ?? null,
58
- trigger: r.trigger,
59
- label: r.label,
60
- median: r.median === null ? null : Number(r.median)
61
- }));
62
- },
63
- async deleteReport(id) {
64
- await (await need()).query(`DELETE FROM ${R} WHERE id = $1`, [id]);
65
- },
66
- async prune(keep) {
67
- await (await need()).query(`DELETE FROM ${R} WHERE id NOT IN (SELECT id FROM ${R} ORDER BY created DESC LIMIT $1)`, [keep]);
68
- },
69
- async putVisit(rec) {
70
- const db = await need();
71
- await db.query(`INSERT INTO ${V} (key,page,at,visit) VALUES ($1,$2,$3,$4::jsonb) ON CONFLICT (key) DO UPDATE SET at=EXCLUDED.at,visit=EXCLUDED.visit`, [
72
- rec.key,
73
- rec.page,
74
- rec.at,
75
- rec.visit
76
- ]);
77
- await db.query(`DELETE FROM ${V} WHERE page = $1 AND key NOT IN (SELECT key FROM ${V} WHERE page = $1 ORDER BY at DESC LIMIT $2)`, [rec.page, MAX_VISITS_PER_KEY]);
78
- },
79
- async listVisits(page, limit = 10) {
80
- const { rows } = await (await need()).query(`SELECT key,page,at,visit FROM ${V} WHERE page = $1 ORDER BY at DESC LIMIT $2`, [page, limit]);
81
- return rows.map((r) => ({
82
- key: r.key,
83
- page: r.page,
84
- at: Number(r.at),
85
- visit: typeof r.visit === "string" ? r.visit : JSON.stringify(r.visit)
86
- }));
87
- },
88
- async close() {
89
- await c?.end?.();
90
- c = null;
91
- }
92
- };
93
- }
94
- export { postgresStore };
119
+ }
120
+ async #create_tables() {
121
+ const c = await this.#client_made();
122
+ const R = this.#R;
123
+ const V = this.#V;
124
+ const base = this.#base;
125
+ const schema = this.#schema;
126
+ if (schema) await c.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
127
+ await c.query(`CREATE TABLE IF NOT EXISTS ${R} (id TEXT PRIMARY KEY, created BIGINT NOT NULL, page TEXT, trigger TEXT, label TEXT, median DOUBLE PRECISION, dump JSONB NOT NULL)`);
128
+ await c.query(`CREATE INDEX IF NOT EXISTS ${base}_reports_created ON ${R} (created DESC)`);
129
+ await c.query(`CREATE TABLE IF NOT EXISTS ${V} (key TEXT PRIMARY KEY, page TEXT NOT NULL, at BIGINT NOT NULL, visit JSONB NOT NULL)`);
130
+ await c.query(`CREATE INDEX IF NOT EXISTS ${base}_visits_page ON ${V} (page, at DESC)`);
131
+ }
132
+ };
133
+ export { PostgresStore };
@@ -1,4 +1,4 @@
1
- import { ProfilerStore } from "./index.js";
1
+ import { ProfilerRecord, ProfilerStore, ProfilerSummary, ProfilerVisitRecord } from "./index.js";
2
2
  /** The subset of a Redis client this store uses — node-redis and ioredis both satisfy it (their
3
3
  * method names differ in case, and a few calls take options objects on one and arguments on the
4
4
  * other; the flavour is read once, below). */
@@ -12,8 +12,21 @@ interface RedisLike {
12
12
  zcard?(key: string): Promise<number>;
13
13
  connect?(): Promise<void>;
14
14
  }
15
- export declare function redisStore(client: RedisLike | string, opts?: {
16
- ttlSeconds?: number;
17
- prefix?: string;
18
- }): ProfilerStore;
15
+ export declare class RedisStore implements ProfilerStore {
16
+ #private;
17
+ readonly kind: string;
18
+ constructor(client: RedisLike | string, opts?: {
19
+ ttlSeconds?: number;
20
+ prefix?: string;
21
+ });
22
+ init(): Promise<void>;
23
+ putReport(rec: ProfilerRecord): Promise<void>;
24
+ getReport(id: string): Promise<string | null>;
25
+ listReports(limit?: number): Promise<ProfilerSummary[]>;
26
+ deleteReport(id: string): Promise<void>;
27
+ prune(keep: number): Promise<void>;
28
+ putVisit(rec: ProfilerVisitRecord): Promise<void>;
29
+ listVisits(page: string, limit?: number): Promise<ProfilerVisitRecord[]>;
30
+ close(): Promise<void>;
31
+ }
19
32
  export {};
@@ -1,4 +1,5 @@
1
1
  import { MAX_VISITS_PER_KEY } from "./visits.js";
2
+ import { STALE, flights } from "@ogygia/machine";
2
3
  // src/profiler/storage/redis.ts
3
4
  /** reports kept at most, whatever the host prunes to (a store used on its own still has a cap) */
4
5
  const MAX_REPORTS = 1e3;
@@ -6,143 +7,171 @@ async function resolve(client) {
6
7
  if (typeof client !== "string") return client;
7
8
  const nodeRedis = "redis";
8
9
  const ioRedis = "ioredis";
10
+ let node_redis = null;
9
11
  try {
10
- const { createClient } = await import(nodeRedis);
11
- const c = createClient({ url: client });
12
+ node_redis = await import(nodeRedis);
13
+ } catch {}
14
+ if (node_redis) {
15
+ const c = node_redis.createClient({ url: client });
12
16
  await c.connect?.();
13
17
  return c;
14
- } catch {}
18
+ }
15
19
  return new (await (import(ioRedis))).default(client);
16
20
  }
17
- function redisStore(client, opts = {}) {
18
- const ttl = opts.ttlSeconds;
19
- const prefix = opts.prefix ?? "og:";
20
- const R = `${prefix}report:`;
21
- const RZ = `${prefix}reports`;
22
- const V = `${prefix}visit:`;
23
- const VZ = (page) => `${V}z:${page}`;
24
- let c = null;
25
- const need = async () => c ??= await resolve(client);
26
- const node = (r) => typeof r.zAdd === "function" ? r : null;
27
- const setStr = async (r, key, val) => {
21
+ const node = (r) => typeof r.zAdd === "function" ? r : null;
22
+ const zadd = (r, key, score, member) => {
23
+ const n = node(r);
24
+ return n ? n.zAdd(key, {
25
+ score,
26
+ value: member
27
+ }) : r.zadd(key, score, member);
28
+ };
29
+ const zrem = (r, key, members) => {
30
+ if (!members.length) return Promise.resolve();
31
+ const n = node(r);
32
+ return n ? n.zRem(key, members) : r.zrem(key, ...members);
33
+ };
34
+ const zrange = (r, key, a, b, rev = false) => {
35
+ const n = node(r);
36
+ if (n) return rev ? n.zRange(key, a, b, { REV: true }) : n.zRange(key, a, b);
37
+ return rev ? r.zrange(key, a, b, "REV") : r.zrange(key, a, b);
38
+ };
39
+ const zcard = (r, key) => {
40
+ const n = node(r);
41
+ return n ? n.zCard(key) : r.zcard(key);
42
+ };
43
+ /** drop every member but the newest `keep`, with the keys they name */
44
+ const trim = async (r, key, item, keep) => {
45
+ const stale = await zrange(r, key, 0, -(keep + 1));
46
+ if (!stale.length) return;
47
+ await r.del(stale.map((m) => item + m));
48
+ await zrem(r, key, stale);
49
+ };
50
+ var RedisStore = class {
51
+ kind = "redis";
52
+ #client;
53
+ #ttl;
54
+ #R;
55
+ #RZ;
56
+ #V;
57
+ /** the client, made once: two first calls share it, and a failure to make it is not kept */
58
+ #connect;
59
+ /** the client made, for `close` */
60
+ #made = null;
61
+ constructor(client, opts = {}) {
62
+ const prefix = opts.prefix ?? "og:";
63
+ this.#client = client;
64
+ this.#ttl = opts.ttlSeconds;
65
+ this.#R = `${prefix}report:`;
66
+ this.#RZ = `${prefix}reports`;
67
+ this.#V = `${prefix}visit:`;
68
+ this.#connect = this.#connector();
69
+ }
70
+ async init() {
71
+ await this.#need();
72
+ }
73
+ async putReport(rec) {
74
+ const r = await this.#need();
75
+ const summary = {
76
+ id: rec.id,
77
+ created: rec.created,
78
+ page: rec.page,
79
+ trigger: rec.trigger,
80
+ label: rec.label,
81
+ median: rec.median
82
+ };
83
+ await this.#set_str(r, this.#R + rec.id, JSON.stringify({
84
+ summary,
85
+ dump: rec.dump
86
+ }));
87
+ await zadd(r, this.#RZ, rec.created, rec.id);
88
+ if (await zcard(r, this.#RZ) > MAX_REPORTS) await trim(r, this.#RZ, this.#R, MAX_REPORTS);
89
+ }
90
+ async getReport(id) {
91
+ const raw = await (await this.#need()).get(this.#R + id);
92
+ if (!raw) return null;
93
+ try {
94
+ return JSON.parse(raw).dump;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ async listReports(limit = 30) {
100
+ const r = await this.#need();
101
+ const ids = await zrange(r, this.#RZ, 0, limit - 1, true);
102
+ const out = [];
103
+ const gone = [];
104
+ for (const id of ids) {
105
+ const raw = await r.get(this.#R + id);
106
+ if (!raw) {
107
+ gone.push(id);
108
+ continue;
109
+ }
110
+ try {
111
+ out.push(JSON.parse(raw).summary);
112
+ } catch {}
113
+ }
114
+ await zrem(r, this.#RZ, gone);
115
+ return out;
116
+ }
117
+ async deleteReport(id) {
118
+ const r = await this.#need();
119
+ await r.del(this.#R + id);
120
+ await zrem(r, this.#RZ, [id]);
121
+ }
122
+ async prune(keep) {
123
+ await trim(await this.#need(), this.#RZ, this.#R, keep);
124
+ }
125
+ async putVisit(rec) {
126
+ const r = await this.#need();
127
+ const vz = this.#VZ(rec.page);
128
+ await this.#set_str(r, this.#V + rec.key, JSON.stringify(rec));
129
+ await zadd(r, vz, rec.at, rec.key);
130
+ if (await zcard(r, vz) > 2e3) await trim(r, vz, this.#V, MAX_VISITS_PER_KEY);
131
+ }
132
+ async listVisits(page, limit = 10) {
133
+ const r = await this.#need();
134
+ const keys = await zrange(r, this.#VZ(page), 0, limit - 1, true);
135
+ const out = [];
136
+ const gone = [];
137
+ for (const k of keys) {
138
+ const raw = await r.get(this.#V + k);
139
+ if (!raw) {
140
+ gone.push(k);
141
+ continue;
142
+ }
143
+ try {
144
+ out.push(JSON.parse(raw));
145
+ } catch {}
146
+ }
147
+ await zrem(r, this.#VZ(page), gone);
148
+ return out;
149
+ }
150
+ async close() {
151
+ const made = this.#made;
152
+ this.#made = null;
153
+ this.#connect = this.#connector();
154
+ await made?.quit?.().catch(() => {});
155
+ }
156
+ #connector() {
157
+ return flights({
158
+ name: "profiler redis client",
159
+ run: async () => this.#made = await resolve(this.#client)
160
+ });
161
+ }
162
+ async #need() {
163
+ const c = await this.#connect.get();
164
+ return c === STALE ? this.#need() : c;
165
+ }
166
+ #VZ(page) {
167
+ return `${this.#V}z:${page}`;
168
+ }
169
+ async #set_str(r, key, val) {
28
170
  const n = node(r);
171
+ const ttl = this.#ttl;
29
172
  if (!ttl) await r.set(key, val);
30
173
  else if (n) await n.set(key, val, { EX: ttl });
31
174
  else await r.set(key, val, "EX", ttl);
32
- };
33
- const zadd = (r, key, score, member) => {
34
- const n = node(r);
35
- return n ? n.zAdd(key, {
36
- score,
37
- value: member
38
- }) : r.zadd(key, score, member);
39
- };
40
- const zrem = (r, key, members) => {
41
- if (!members.length) return Promise.resolve();
42
- const n = node(r);
43
- return n ? n.zRem(key, members) : r.zrem(key, ...members);
44
- };
45
- const zrange = (r, key, a, b, rev = false) => {
46
- const n = node(r);
47
- if (n) return rev ? n.zRange(key, a, b, { REV: true }) : n.zRange(key, a, b);
48
- return rev ? r.zrange(key, a, b, "REV") : r.zrange(key, a, b);
49
- };
50
- const zcard = (r, key) => {
51
- const n = node(r);
52
- return n ? n.zCard(key) : r.zcard(key);
53
- };
54
- /** drop every member but the newest `keep`, with the keys they name */
55
- const trim = async (r, key, item, keep) => {
56
- const stale = await zrange(r, key, 0, -(keep + 1));
57
- if (!stale.length) return;
58
- await r.del(stale.map((m) => item + m));
59
- await zrem(r, key, stale);
60
- };
61
- return {
62
- kind: "redis",
63
- async init() {
64
- await need();
65
- },
66
- async putReport(rec) {
67
- const r = await need();
68
- const summary = {
69
- id: rec.id,
70
- created: rec.created,
71
- page: rec.page,
72
- trigger: rec.trigger,
73
- label: rec.label,
74
- median: rec.median
75
- };
76
- await setStr(r, R + rec.id, JSON.stringify({
77
- summary,
78
- dump: rec.dump
79
- }));
80
- await zadd(r, RZ, rec.created, rec.id);
81
- if (await zcard(r, RZ) > MAX_REPORTS) await trim(r, RZ, R, MAX_REPORTS);
82
- },
83
- async getReport(id) {
84
- const raw = await (await need()).get(R + id);
85
- if (!raw) return null;
86
- try {
87
- return JSON.parse(raw).dump;
88
- } catch {
89
- return null;
90
- }
91
- },
92
- async listReports(limit = 30) {
93
- const r = await need();
94
- const ids = await zrange(r, RZ, 0, limit - 1, true);
95
- const out = [];
96
- const gone = [];
97
- for (const id of ids) {
98
- const raw = await r.get(R + id);
99
- if (!raw) {
100
- gone.push(id);
101
- continue;
102
- }
103
- try {
104
- out.push(JSON.parse(raw).summary);
105
- } catch {}
106
- }
107
- await zrem(r, RZ, gone);
108
- return out;
109
- },
110
- async deleteReport(id) {
111
- const r = await need();
112
- await r.del(R + id);
113
- await zrem(r, RZ, [id]);
114
- },
115
- async prune(keep) {
116
- await trim(await need(), RZ, R, keep);
117
- },
118
- async putVisit(rec) {
119
- const r = await need();
120
- await setStr(r, V + rec.key, JSON.stringify(rec));
121
- await zadd(r, VZ(rec.page), rec.at, rec.key);
122
- if (await zcard(r, VZ(rec.page)) > 2e3) await trim(r, VZ(rec.page), V, MAX_VISITS_PER_KEY);
123
- },
124
- async listVisits(page, limit = 10) {
125
- const r = await need();
126
- const keys = await zrange(r, VZ(page), 0, limit - 1, true);
127
- const out = [];
128
- const gone = [];
129
- for (const k of keys) {
130
- const raw = await r.get(V + k);
131
- if (!raw) {
132
- gone.push(k);
133
- continue;
134
- }
135
- try {
136
- out.push(JSON.parse(raw));
137
- } catch {}
138
- }
139
- await zrem(r, VZ(page), gone);
140
- return out;
141
- },
142
- async close() {
143
- await c?.quit?.().catch(() => {});
144
- c = null;
145
- }
146
- };
147
- }
148
- export { redisStore };
175
+ }
176
+ };
177
+ export { RedisStore };
@@ -1,2 +1,15 @@
1
- import { ProfilerStore } from "./index.js";
2
- export declare function sqliteStore(filename?: string): ProfilerStore;
1
+ import { ProfilerRecord, ProfilerStore, ProfilerSummary, ProfilerVisitRecord } from "./index.js";
2
+ export declare class SqliteStore implements ProfilerStore {
3
+ #private;
4
+ readonly kind: string;
5
+ constructor(filename?: string);
6
+ init(): void;
7
+ putReport(rec: ProfilerRecord): void;
8
+ getReport(id: string): string | null;
9
+ listReports(limit?: number): ProfilerSummary[];
10
+ deleteReport(id: string): void;
11
+ prune(keep: number): void;
12
+ putVisit(rec: ProfilerVisitRecord): void;
13
+ listVisits(page: string, limit?: number): ProfilerVisitRecord[];
14
+ close(): void;
15
+ }