@noego/telemetry 0.0.1

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 (120) hide show
  1. package/README.md +130 -0
  2. package/dist/browser/index.cjs +1346 -0
  3. package/dist/browser/index.cjs.map +1 -0
  4. package/dist/browser/index.d.cts +5 -0
  5. package/dist/browser/index.d.ts +5 -0
  6. package/dist/browser/index.js +1318 -0
  7. package/dist/browser/index.js.map +1 -0
  8. package/dist/browser_telemetry_client-BzBPQm4m.d.cts +29 -0
  9. package/dist/browser_telemetry_client-Ch_VzwEa.d.ts +29 -0
  10. package/dist/cli/telemetry_bin.cjs +5531 -0
  11. package/dist/cli/telemetry_bin.cjs.map +1 -0
  12. package/dist/cli/telemetry_bin.d.cts +1 -0
  13. package/dist/cli/telemetry_bin.d.ts +1 -0
  14. package/dist/cli/telemetry_bin.js +5517 -0
  15. package/dist/cli/telemetry_bin.js.map +1 -0
  16. package/dist/cli/telemetry_cli.cjs +5523 -0
  17. package/dist/cli/telemetry_cli.cjs.map +1 -0
  18. package/dist/cli/telemetry_cli.d.cts +92 -0
  19. package/dist/cli/telemetry_cli.d.ts +92 -0
  20. package/dist/cli/telemetry_cli.js +5497 -0
  21. package/dist/cli/telemetry_cli.js.map +1 -0
  22. package/dist/client_defaults-B5tC-TgG.d.ts +8 -0
  23. package/dist/client_defaults-C9LnSX4j.d.cts +8 -0
  24. package/dist/core/index.cjs +2354 -0
  25. package/dist/core/index.cjs.map +1 -0
  26. package/dist/core/index.d.cts +168 -0
  27. package/dist/core/index.d.ts +168 -0
  28. package/dist/core/index.js +2318 -0
  29. package/dist/core/index.js.map +1 -0
  30. package/dist/electron/index.cjs +1379 -0
  31. package/dist/electron/index.cjs.map +1 -0
  32. package/dist/electron/index.d.cts +31 -0
  33. package/dist/electron/index.d.ts +31 -0
  34. package/dist/electron/index.js +1353 -0
  35. package/dist/electron/index.js.map +1 -0
  36. package/dist/envelope.types-rOLpWXRC.d.cts +42 -0
  37. package/dist/envelope.types-rOLpWXRC.d.ts +42 -0
  38. package/dist/in_memory_batch_transport-DKW9EZnU.d.cts +16 -0
  39. package/dist/in_memory_batch_transport-oOwNxXnA.d.ts +16 -0
  40. package/dist/index.cjs +3325 -0
  41. package/dist/index.cjs.map +1 -0
  42. package/dist/index.d.cts +66 -0
  43. package/dist/index.d.ts +66 -0
  44. package/dist/index.js +3285 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/maintenance_coordinator.types-B-bZUNvw.d.cts +127 -0
  47. package/dist/maintenance_coordinator.types-CBA5y74b.d.ts +127 -0
  48. package/dist/metric_definition_contract-BR6a_GzF.d.cts +12 -0
  49. package/dist/metric_definition_contract-l4i4SaAp.d.ts +12 -0
  50. package/dist/node/index.cjs +7157 -0
  51. package/dist/node/index.cjs.map +1 -0
  52. package/dist/node/index.d.cts +74 -0
  53. package/dist/node/index.d.ts +74 -0
  54. package/dist/node/index.js +7122 -0
  55. package/dist/node/index.js.map +1 -0
  56. package/dist/node/producer/index.cjs +1322 -0
  57. package/dist/node/producer/index.cjs.map +1 -0
  58. package/dist/node/producer/index.d.cts +21 -0
  59. package/dist/node/producer/index.d.ts +21 -0
  60. package/dist/node/producer/index.js +1294 -0
  61. package/dist/node/producer/index.js.map +1 -0
  62. package/dist/query.types-Bntwh6KF.d.cts +186 -0
  63. package/dist/query.types-DySoqYop.d.ts +186 -0
  64. package/dist/result.types-171Huujb.d.cts +9 -0
  65. package/dist/result.types-171Huujb.d.ts +9 -0
  66. package/dist/sqlite/index.cjs +3409 -0
  67. package/dist/sqlite/index.cjs.map +1 -0
  68. package/dist/sqlite/index.d.cts +77 -0
  69. package/dist/sqlite/index.d.ts +77 -0
  70. package/dist/sqlite/index.js +3382 -0
  71. package/dist/sqlite/index.js.map +1 -0
  72. package/dist/storage.types-BTCGVxOM.d.ts +76 -0
  73. package/dist/storage.types-DjuUMinz.d.cts +76 -0
  74. package/dist/telemetry_client-BXw8WH_F.d.ts +141 -0
  75. package/dist/telemetry_client-CYaPYfOl.d.cts +141 -0
  76. package/dist/telemetry_configuration_parser-C7Et_gwG.d.ts +39 -0
  77. package/dist/telemetry_configuration_parser-R-niZQRZ.d.cts +39 -0
  78. package/dist/testing/index.cjs +215 -0
  79. package/dist/testing/index.cjs.map +1 -0
  80. package/dist/testing/index.d.cts +32 -0
  81. package/dist/testing/index.d.ts +32 -0
  82. package/dist/testing/index.js +187 -0
  83. package/dist/testing/index.js.map +1 -0
  84. package/dist/trace-adapter/index.cjs +1034 -0
  85. package/dist/trace-adapter/index.cjs.map +1 -0
  86. package/dist/trace-adapter/index.d.cts +72 -0
  87. package/dist/trace-adapter/index.d.ts +72 -0
  88. package/dist/trace-adapter/index.js +1007 -0
  89. package/dist/trace-adapter/index.js.map +1 -0
  90. package/dist/transport.types-BAl_TRtU.d.cts +99 -0
  91. package/dist/transport.types-DDnZoKWi.d.ts +99 -0
  92. package/migrations/1783695629569_raw_events.down.sql +1 -0
  93. package/migrations/1783695629569_raw_events.up.sql +30 -0
  94. package/migrations/1783695630053_batch_acknowledgements.down.sql +1 -0
  95. package/migrations/1783695630053_batch_acknowledgements.up.sql +13 -0
  96. package/migrations/1783695630960_ingest_watermarks.down.sql +1 -0
  97. package/migrations/1783695630960_ingest_watermarks.up.sql +9 -0
  98. package/migrations/1783695631522_metric_definitions.down.sql +1 -0
  99. package/migrations/1783695631522_metric_definitions.up.sql +10 -0
  100. package/migrations/1783695631966_metric_samples.down.sql +1 -0
  101. package/migrations/1783695631966_metric_samples.up.sql +15 -0
  102. package/migrations/1783695632417_metric_rollups.down.sql +1 -0
  103. package/migrations/1783695632417_metric_rollups.up.sql +22 -0
  104. package/migrations/1783695632860_rollup_completions.down.sql +1 -0
  105. package/migrations/1783695632860_rollup_completions.up.sql +17 -0
  106. package/migrations/1783695633302_extraction_failures.down.sql +1 -0
  107. package/migrations/1783695633302_extraction_failures.up.sql +10 -0
  108. package/migrations/1783695633748_maintenance_runs.down.sql +1 -0
  109. package/migrations/1783695633748_maintenance_runs.up.sql +12 -0
  110. package/migrations/1783695634221_reader_leases.down.sql +1 -0
  111. package/migrations/1783695634221_reader_leases.up.sql +9 -0
  112. package/migrations/1783702703104_committed_drop_counters.down.sql +1 -0
  113. package/migrations/1783702703104_committed_drop_counters.up.sql +11 -0
  114. package/migrations/1783705542684_rollup_completion_state.down.sql +2 -0
  115. package/migrations/1783705542684_rollup_completion_state.up.sql +6 -0
  116. package/migrations/1784309799539_metric_samples_event_index.down.sql +1 -0
  117. package/migrations/1784309799539_metric_samples_event_index.up.sql +2 -0
  118. package/native/noego_telemetry_native.node +0 -0
  119. package/package.json +137 -0
  120. package/proper.json +8 -0
@@ -0,0 +1,3409 @@
1
+ /* @noego/telemetry */
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/transports/index.ts
32
+ var transports_exports = {};
33
+ __export(transports_exports, {
34
+ SQLiteTelemetryTransport: () => SQLiteTelemetryTransport
35
+ });
36
+ module.exports = __toCommonJS(transports_exports);
37
+
38
+ // src/transports/sqlite_transport.ts
39
+ var import_node_crypto2 = require("crypto");
40
+ var import_node_path2 = require("path");
41
+ var import_node_url = require("url");
42
+ var import_proper = require("@noego/proper");
43
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
44
+ var import_adapters = require("sqlstack/adapters");
45
+
46
+ // src/core/helper/canonical_json.ts
47
+ var UTF8_ENCODER = new TextEncoder();
48
+ function compareCanonicalKeys(left, right) {
49
+ if (left === right) return 0;
50
+ if (/^[\x00-\x7f]*$/.test(left) && /^[\x00-\x7f]*$/.test(right)) {
51
+ return left < right ? -1 : 1;
52
+ }
53
+ const leftBytes = UTF8_ENCODER.encode(left);
54
+ const rightBytes = UTF8_ENCODER.encode(right);
55
+ const length = Math.min(leftBytes.length, rightBytes.length);
56
+ for (let index = 0; index < length; index += 1) {
57
+ const difference = (leftBytes[index] ?? 0) - (rightBytes[index] ?? 0);
58
+ if (difference !== 0) return difference;
59
+ }
60
+ return leftBytes.length - rightBytes.length;
61
+ }
62
+ function canonicalJson(value) {
63
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
64
+ return JSON.stringify(value);
65
+ }
66
+ if (typeof value === "number") {
67
+ if (!Number.isFinite(value)) {
68
+ throw new Error("Canonical telemetry JSON cannot encode a non-finite number");
69
+ }
70
+ return JSON.stringify(value);
71
+ }
72
+ if (Array.isArray(value)) {
73
+ const items = value.map((item) => canonicalJson(item));
74
+ return `[${items.join(",")}]`;
75
+ }
76
+ if (typeof value !== "object") {
77
+ throw new Error(`Canonical telemetry JSON cannot encode ${typeof value}`);
78
+ }
79
+ const entries = Object.entries(value).sort(([left], [right]) => compareCanonicalKeys(left, right));
80
+ const properties = entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`);
81
+ return `{${properties.join(",")}}`;
82
+ }
83
+ function telemetryUtf8Length(value) {
84
+ return new TextEncoder().encode(value).byteLength;
85
+ }
86
+ function telemetryChecksum(value) {
87
+ const bytes = new TextEncoder().encode(canonicalJson(value));
88
+ let hash = 0xcbf29ce484222325n;
89
+ const prime = 0x100000001b3n;
90
+ for (const byte of bytes) {
91
+ hash ^= BigInt(byte);
92
+ hash = BigInt.asUintN(64, hash * prime);
93
+ }
94
+ return hash.toString(16).padStart(16, "0");
95
+ }
96
+
97
+ // src/storage/helper/sqlite_table_names.ts
98
+ var SQLITE_LOGICAL_TABLE_NAMES = [
99
+ "rawEvents",
100
+ "batchAcknowledgements",
101
+ "ingestWatermarks",
102
+ "metricDefinitions",
103
+ "metricSamples",
104
+ "metricRollups",
105
+ "rollupCompletions",
106
+ "extractionFailures",
107
+ "maintenanceRuns",
108
+ "readerLeases",
109
+ "committedDropCounters",
110
+ "migrationHistory"
111
+ ];
112
+ var SQLITE_DEFAULT_TABLE_NAMES = Object.freeze({
113
+ rawEvents: "raw_events",
114
+ batchAcknowledgements: "batch_acknowledgements",
115
+ ingestWatermarks: "ingest_watermarks",
116
+ metricDefinitions: "metric_definitions",
117
+ metricSamples: "metric_samples",
118
+ metricRollups: "metric_rollups",
119
+ rollupCompletions: "rollup_completions",
120
+ extractionFailures: "extraction_failures",
121
+ maintenanceRuns: "maintenance_runs",
122
+ readerLeases: "reader_leases",
123
+ committedDropCounters: "committed_drop_counters",
124
+ migrationHistory: "migration_history"
125
+ });
126
+ var SQLITE_SCHEMA_MANIFEST_TABLE = "telemetry_schema_manifest";
127
+
128
+ // src/storage/sqlite_ownership_lease.ts
129
+ var import_node_fs = require("fs");
130
+ var import_node_path = require("path");
131
+ var SqliteOwnershipLease = class {
132
+ constructor(options) {
133
+ this.options = options;
134
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(options.databasePath), { recursive: true });
135
+ const canonicalDirectory = (0, import_node_fs.realpathSync)((0, import_node_path.dirname)((0, import_node_path.resolve)(options.databasePath)));
136
+ const databaseName = (0, import_node_path.resolve)(options.databasePath).slice((0, import_node_path.dirname)((0, import_node_path.resolve)(options.databasePath)).length + 1);
137
+ this.canonicalDatabasePath = (0, import_node_path.resolve)(canonicalDirectory, databaseName);
138
+ this.leasePath = `${this.canonicalDatabasePath}.telemetry-owner.json`;
139
+ }
140
+ options;
141
+ canonicalDatabasePath;
142
+ leasePath;
143
+ held = false;
144
+ acquire() {
145
+ const firstAttempt = this.writeExclusive(this.createRecord());
146
+ if (firstAttempt.ok) {
147
+ this.held = true;
148
+ return firstAttempt;
149
+ }
150
+ const recovered = this.recoverStaleLease();
151
+ if (!recovered.ok) {
152
+ return recovered;
153
+ }
154
+ const secondAttempt = this.writeExclusive(this.createRecord());
155
+ if (secondAttempt.ok) {
156
+ this.held = true;
157
+ }
158
+ return secondAttempt;
159
+ }
160
+ heartbeat() {
161
+ if (!this.held) {
162
+ return { ok: false, error: "Cannot heartbeat a telemetry lease that is not held" };
163
+ }
164
+ const current = this.readRecord();
165
+ if (!current.ok) {
166
+ return current;
167
+ }
168
+ if (current.value.processToken !== this.options.processToken) {
169
+ this.held = false;
170
+ return { ok: false, error: "Telemetry ownership lease was replaced by another owner" };
171
+ }
172
+ const now = this.options.now();
173
+ return this.writeReplace({
174
+ ...current.value,
175
+ heartbeatAtMs: now,
176
+ expiresAtMs: now + this.options.expiryMs
177
+ });
178
+ }
179
+ release() {
180
+ if (!this.held || !(0, import_node_fs.existsSync)(this.leasePath)) {
181
+ this.held = false;
182
+ return;
183
+ }
184
+ const current = this.readRecord();
185
+ if (current.ok && current.value.processToken === this.options.processToken) {
186
+ try {
187
+ (0, import_node_fs.rmSync)(this.leasePath, { force: true });
188
+ } catch {
189
+ }
190
+ }
191
+ this.held = false;
192
+ }
193
+ isHeld() {
194
+ return this.held;
195
+ }
196
+ path() {
197
+ return this.leasePath;
198
+ }
199
+ createRecord() {
200
+ const now = this.options.now();
201
+ return {
202
+ formatVersion: 1,
203
+ targetId: this.options.targetId,
204
+ databasePath: this.canonicalDatabasePath,
205
+ mode: this.options.mode,
206
+ pid: process.pid,
207
+ processToken: this.options.processToken,
208
+ acquiredAtMs: now,
209
+ heartbeatAtMs: now,
210
+ expiresAtMs: now + this.options.expiryMs
211
+ };
212
+ }
213
+ writeExclusive(record) {
214
+ let descriptor = null;
215
+ try {
216
+ descriptor = (0, import_node_fs.openSync)(this.leasePath, "wx", 384);
217
+ (0, import_node_fs.writeFileSync)(descriptor, JSON.stringify(record), { encoding: "utf8" });
218
+ (0, import_node_fs.closeSync)(descriptor);
219
+ return { ok: true, value: void 0 };
220
+ } catch (cause) {
221
+ if (descriptor !== null) {
222
+ try {
223
+ (0, import_node_fs.closeSync)(descriptor);
224
+ } catch {
225
+ }
226
+ }
227
+ return { ok: false, error: this.errorMessage(cause) };
228
+ }
229
+ }
230
+ writeReplace(record) {
231
+ const temporaryPath = `${this.leasePath}.${this.options.processToken}.tmp`;
232
+ try {
233
+ (0, import_node_fs.writeFileSync)(temporaryPath, JSON.stringify(record), { encoding: "utf8", mode: 384 });
234
+ (0, import_node_fs.renameSync)(temporaryPath, this.leasePath);
235
+ return { ok: true, value: void 0 };
236
+ } catch (cause) {
237
+ try {
238
+ (0, import_node_fs.rmSync)(temporaryPath, { force: true });
239
+ } catch {
240
+ }
241
+ return { ok: false, error: this.errorMessage(cause) };
242
+ }
243
+ }
244
+ readRecord() {
245
+ try {
246
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(this.leasePath, "utf8"));
247
+ if (!this.isLeaseRecord(parsed)) {
248
+ return { ok: false, error: `Invalid telemetry ownership lease at ${this.leasePath}` };
249
+ }
250
+ return { ok: true, value: parsed };
251
+ } catch (cause) {
252
+ return { ok: false, error: this.errorMessage(cause) };
253
+ }
254
+ }
255
+ recoverStaleLease() {
256
+ const current = this.readRecord();
257
+ if (!current.ok) {
258
+ return current;
259
+ }
260
+ const expired = current.value.expiresAtMs < this.options.now();
261
+ const processAlive = this.isProcessAlive(current.value.pid);
262
+ if (!expired || processAlive) {
263
+ return {
264
+ ok: false,
265
+ error: `Telemetry target ${this.options.targetId} is owned by pid ${current.value.pid} (${current.value.mode})`
266
+ };
267
+ }
268
+ try {
269
+ const stalePath = `${this.leasePath}.stale.${current.value.processToken}`;
270
+ (0, import_node_fs.renameSync)(this.leasePath, stalePath);
271
+ (0, import_node_fs.rmSync)(stalePath, { force: true });
272
+ return { ok: true, value: void 0 };
273
+ } catch (cause) {
274
+ return { ok: false, error: this.errorMessage(cause) };
275
+ }
276
+ }
277
+ isProcessAlive(pid) {
278
+ try {
279
+ process.kill(pid, 0);
280
+ return true;
281
+ } catch (cause) {
282
+ const code = typeof cause === "object" && cause !== null && "code" in cause ? Reflect.get(cause, "code") : null;
283
+ return code === "EPERM";
284
+ }
285
+ }
286
+ isLeaseRecord(value) {
287
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
288
+ return false;
289
+ }
290
+ return Reflect.get(value, "formatVersion") === 1 && typeof Reflect.get(value, "targetId") === "string" && typeof Reflect.get(value, "databasePath") === "string" && (Reflect.get(value, "mode") === "writer" || Reflect.get(value, "mode") === "offline-maintenance") && Number.isInteger(Reflect.get(value, "pid")) && typeof Reflect.get(value, "processToken") === "string" && typeof Reflect.get(value, "acquiredAtMs") === "number" && typeof Reflect.get(value, "heartbeatAtMs") === "number" && typeof Reflect.get(value, "expiresAtMs") === "number";
291
+ }
292
+ errorMessage(cause) {
293
+ return cause instanceof Error ? cause.message : String(cause);
294
+ }
295
+ };
296
+
297
+ // src/storage/telemetry_proper_sql_runner.ts
298
+ var TelemetryProperSqlRunner = class {
299
+ constructor(connection, tableMap) {
300
+ this.connection = connection;
301
+ this.tableMap = tableMap;
302
+ }
303
+ connection;
304
+ tableMap;
305
+ async query(sql, params = []) {
306
+ const substituted = this.substitute(sql);
307
+ const normalized = this.leadingKeyword(substituted);
308
+ if (normalized === "SELECT" || normalized === "WITH" || normalized === "PRAGMA" || normalized === "EXPLAIN") {
309
+ return [this.connection.prepare(substituted).all(...params), void 0];
310
+ }
311
+ if (params.length > 0) {
312
+ return [this.connection.prepare(substituted).run(...params), void 0];
313
+ }
314
+ this.connection.exec(substituted);
315
+ return [{ changes: 0 }, void 0];
316
+ }
317
+ async execute(sql, params = []) {
318
+ const substituted = this.substitute(sql);
319
+ if (params.length > 0) {
320
+ return [this.connection.prepare(substituted).run(...params), void 0];
321
+ }
322
+ this.connection.exec(substituted);
323
+ return [{ changes: 0 }, void 0];
324
+ }
325
+ async end() {
326
+ }
327
+ substitute(sql) {
328
+ const substituted = this.tableMap.substitute(sql);
329
+ if (!substituted.ok) {
330
+ throw new Error(substituted.error);
331
+ }
332
+ return substituted.value;
333
+ }
334
+ leadingKeyword(sql) {
335
+ const withoutComments = sql.replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trimStart();
336
+ return withoutComments.split(/\s+/, 1)[0]?.toUpperCase() ?? "";
337
+ }
338
+ };
339
+
340
+ // src/storage/telemetry_repository.ts
341
+ var import_node_crypto = require("crypto");
342
+
343
+ // src/core/metric_definition_contract.ts
344
+ var MetricDefinitionContract = class {
345
+ normalize(definition) {
346
+ const parsedSelector = this.parseSelector(definition.selector);
347
+ if (!parsedSelector.ok) {
348
+ return parsedSelector;
349
+ }
350
+ const bracketGroupBy = parsedSelector.value.groupBy;
351
+ const configuredGroupBy = definition.groupBy;
352
+ const bothSpecifyDifferentGrouping = bracketGroupBy.length > 0 && configuredGroupBy.length > 0 && bracketGroupBy.join(".") !== configuredGroupBy.join(".");
353
+ if (bothSpecifyDifferentGrouping) {
354
+ return { ok: false, error: `Metric ${definition.name} has conflicting selector and groupBy declarations` };
355
+ }
356
+ const groupBy = configuredGroupBy.length > 0 ? configuredGroupBy : bracketGroupBy;
357
+ const basicError = this.validateBasic(definition, parsedSelector.value.selector, groupBy);
358
+ if (basicError !== null) {
359
+ return { ok: false, error: basicError };
360
+ }
361
+ if (parsedSelector.value.wildcardUngrouped && definition.measurements.some((measurement) => measurement.kind !== "count")) {
362
+ return {
363
+ ok: false,
364
+ error: `Metric ${definition.name} uses [*] and may only declare count`
365
+ };
366
+ }
367
+ return {
368
+ ok: true,
369
+ value: Object.freeze({
370
+ ...definition,
371
+ selector: parsedSelector.value.selector,
372
+ groupBy: Object.freeze([...groupBy]),
373
+ resolutionsMs: Object.freeze([...definition.resolutionsMs].sort((left, right) => left - right)),
374
+ measurements: Object.freeze([...definition.measurements]),
375
+ targetIds: Object.freeze([...definition.targetIds]),
376
+ cardinality: Object.freeze({ ...definition.cardinality })
377
+ })
378
+ };
379
+ }
380
+ matches(selector, label) {
381
+ const selectorSegments = selector.split(".");
382
+ const labelSegments = label.split(".");
383
+ return this.matchSegments(selectorSegments, labelSegments, 0, 0);
384
+ }
385
+ parseSelector(selector) {
386
+ if (selector.startsWith("[")) {
387
+ return { ok: false, error: "A metric selector cannot omit its event label or glob" };
388
+ }
389
+ const bracketStart = selector.lastIndexOf("[");
390
+ if (bracketStart < 0) {
391
+ return { ok: true, value: { selector, groupBy: [], wildcardUngrouped: false } };
392
+ }
393
+ if (!selector.endsWith("]")) {
394
+ return { ok: false, error: `Invalid metric selector ${selector}` };
395
+ }
396
+ const eventSelector = selector.slice(0, bracketStart);
397
+ const bracket = selector.slice(bracketStart + 1, -1).trim();
398
+ if (!eventSelector || !bracket) {
399
+ return { ok: false, error: `Invalid metric selector ${selector}` };
400
+ }
401
+ return {
402
+ ok: true,
403
+ value: {
404
+ selector: eventSelector,
405
+ groupBy: bracket === "*" ? [] : [bracket],
406
+ wildcardUngrouped: bracket === "*"
407
+ }
408
+ };
409
+ }
410
+ validateBasic(definition, selector, groupBy) {
411
+ if (!/^[A-Za-z0-9_*][A-Za-z0-9_.:*\-]*$/.test(selector)) {
412
+ return `Metric ${definition.name} has an invalid selector`;
413
+ }
414
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(definition.name)) {
415
+ return `Metric name ${definition.name} is invalid`;
416
+ }
417
+ if (!Number.isInteger(definition.version) || definition.version <= 0) {
418
+ return `Metric ${definition.name} must have a positive integer version`;
419
+ }
420
+ if (definition.measurements.length === 0) {
421
+ return `Metric ${definition.name} must declare at least one measurement`;
422
+ }
423
+ const invalidResolution = definition.resolutionsMs.some(
424
+ (resolution) => !Number.isInteger(resolution) || resolution <= 0
425
+ );
426
+ const duplicateResolution = new Set(definition.resolutionsMs).size !== definition.resolutionsMs.length;
427
+ if (invalidResolution || duplicateResolution || definition.resolutionsMs.length === 0) {
428
+ return `Metric ${definition.name} has invalid or duplicate resolutions`;
429
+ }
430
+ const missingCardinality = groupBy.find((field) => definition.cardinality[field] === void 0);
431
+ if (missingCardinality) {
432
+ return `Metric ${definition.name} is missing cardinality policy for ${missingCardinality}`;
433
+ }
434
+ const invalidCardinality = Object.values(definition.cardinality).some(
435
+ (rule) => !Number.isInteger(rule.maxValuesPerBucket) || rule.maxValuesPerBucket <= 0
436
+ );
437
+ if (invalidCardinality) {
438
+ return `Metric ${definition.name} has an invalid cardinality limit`;
439
+ }
440
+ return null;
441
+ }
442
+ matchSegments(selector, label, selectorIndex, labelIndex) {
443
+ if (selectorIndex === selector.length) {
444
+ return labelIndex === label.length;
445
+ }
446
+ const segment = selector[selectorIndex];
447
+ if (segment === "**") {
448
+ if (selectorIndex === selector.length - 1) {
449
+ return true;
450
+ }
451
+ for (let next = labelIndex; next <= label.length; next += 1) {
452
+ if (this.matchSegments(selector, label, selectorIndex + 1, next)) {
453
+ return true;
454
+ }
455
+ }
456
+ return false;
457
+ }
458
+ if (labelIndex >= label.length) {
459
+ return false;
460
+ }
461
+ const segmentMatches = segment === "*" || segment === label[labelIndex];
462
+ return segmentMatches && this.matchSegments(selector, label, selectorIndex + 1, labelIndex + 1);
463
+ }
464
+ };
465
+
466
+ // src/storage/schema/telemetry_repository_rows.ts
467
+ var import_zod = require("zod");
468
+ var integer = import_zod.z.number().int();
469
+ var RawTelemetryEventRowSchema = import_zod.z.object({
470
+ producer_id: import_zod.z.string(),
471
+ sequence: integer.nonnegative(),
472
+ event_id: import_zod.z.string(),
473
+ occurred_at_ms: integer,
474
+ monotonic_ns: import_zod.z.string().nullable(),
475
+ source: import_zod.z.string(),
476
+ process_kind: import_zod.z.string(),
477
+ level: import_zod.z.string(),
478
+ label: import_zod.z.string(),
479
+ conversation_id: import_zod.z.string().nullable(),
480
+ conversation_id_kind: import_zod.z.enum(["string", "number"]).nullable(),
481
+ correlation_id: import_zod.z.string().nullable(),
482
+ metadata_json: import_zod.z.string(),
483
+ schema_version: integer.positive()
484
+ }).strict();
485
+ var BatchAcknowledgementRowSchema = import_zod.z.object({
486
+ idempotency_key: import_zod.z.string(),
487
+ producer_id: import_zod.z.string(),
488
+ sequence_start: integer.nonnegative(),
489
+ sequence_end: integer.nonnegative(),
490
+ schema_version: integer.positive(),
491
+ checksum: import_zod.z.string(),
492
+ accepted_events: integer.nonnegative(),
493
+ committed_at_ms: integer
494
+ }).strict();
495
+ var MetricSampleRowSchema = import_zod.z.object({
496
+ metric_name: import_zod.z.string(),
497
+ definition_version: integer.positive(),
498
+ occurred_at_ms: integer,
499
+ producer_id: import_zod.z.string(),
500
+ sequence: integer.nonnegative(),
501
+ dimension_key: import_zod.z.string(),
502
+ dimensions_json: import_zod.z.string(),
503
+ values_json: import_zod.z.string()
504
+ }).strict();
505
+ var MetricRollupRowSchema = import_zod.z.object({
506
+ metric_name: import_zod.z.string(),
507
+ definition_version: integer.positive(),
508
+ resolution_ms: integer.positive(),
509
+ bucket_start_ms: integer,
510
+ dimension_key: import_zod.z.string(),
511
+ dimensions_json: import_zod.z.string(),
512
+ values_json: import_zod.z.string()
513
+ }).strict();
514
+ var RollupCompletionRowSchema = import_zod.z.object({
515
+ metric_name: import_zod.z.string(),
516
+ definition_version: integer.positive(),
517
+ resolution_ms: integer.positive(),
518
+ bucket_start_ms: integer,
519
+ sample_count: integer.nonnegative(),
520
+ sample_watermark_ms: integer,
521
+ completed_at_ms: integer,
522
+ state: import_zod.z.enum(["completed", "rejected"]),
523
+ detail: import_zod.z.string().nullable()
524
+ }).strict();
525
+ var TimelineCursorSchema = import_zod.z.object({
526
+ version: import_zod.z.literal(1),
527
+ order: import_zod.z.enum(["asc", "desc"]),
528
+ occurredAtMs: integer,
529
+ producerId: import_zod.z.string(),
530
+ sequence: integer.nonnegative()
531
+ }).strict();
532
+ var MetricCursorSchema = import_zod.z.object({
533
+ version: import_zod.z.literal(1),
534
+ order: import_zod.z.enum(["asc", "desc"]),
535
+ bucketStartMs: integer,
536
+ resolutionMs: integer.positive(),
537
+ definitionVersion: integer.positive(),
538
+ dimensionKey: import_zod.z.string()
539
+ }).strict();
540
+ var TelemetryJsonObjectSchema = import_zod.z.record(import_zod.z.string(), import_zod.z.json());
541
+ var NumericValuesSchema = import_zod.z.record(import_zod.z.string(), import_zod.z.number().finite());
542
+ var StoredMetricMeasurementSchema = import_zod.z.discriminatedUnion("kind", [
543
+ import_zod.z.object({ kind: import_zod.z.literal("count") }).strict(),
544
+ import_zod.z.object({
545
+ kind: import_zod.z.literal("field"),
546
+ field: import_zod.z.string().min(1),
547
+ aggregates: import_zod.z.array(import_zod.z.enum(["sum", "min", "max", "last"])).min(1)
548
+ }).strict()
549
+ ]);
550
+ var StoredMetricDefinitionSchema = import_zod.z.object({
551
+ name: import_zod.z.string().min(1),
552
+ version: integer.positive(),
553
+ selector: import_zod.z.string().min(1),
554
+ groupBy: import_zod.z.array(import_zod.z.string().min(1)),
555
+ measurements: import_zod.z.array(StoredMetricMeasurementSchema).min(1),
556
+ cardinality: import_zod.z.record(
557
+ import_zod.z.string(),
558
+ import_zod.z.object({
559
+ maxValuesPerBucket: integer.positive(),
560
+ overflow: import_zod.z.enum(["other", "drop", "reject"])
561
+ }).strict()
562
+ ),
563
+ targetIds: import_zod.z.array(import_zod.z.string().min(1)),
564
+ resolutionsMs: import_zod.z.array(integer.positive()).min(1),
565
+ missingField: import_zod.z.enum(["skip", "record-failure"])
566
+ }).strict();
567
+ var StoredMetricDefinitionRowSchema = import_zod.z.object({
568
+ metric_name: import_zod.z.string(),
569
+ definition_version: integer.positive(),
570
+ definition_json: import_zod.z.string(),
571
+ definition_checksum: import_zod.z.string(),
572
+ active: integer.min(0).max(1)
573
+ }).strict();
574
+ var MetricRollupValuesSchema = import_zod.z.object({
575
+ count: integer.nonnegative(),
576
+ fields: import_zod.z.record(
577
+ import_zod.z.string(),
578
+ import_zod.z.object({
579
+ count: integer.nonnegative(),
580
+ sum: import_zod.z.number().finite(),
581
+ min: import_zod.z.number().finite(),
582
+ max: import_zod.z.number().finite(),
583
+ last: import_zod.z.number().finite(),
584
+ lastOccurredAtMs: integer,
585
+ lastProducerId: import_zod.z.string(),
586
+ lastSequence: integer.nonnegative()
587
+ }).strict()
588
+ )
589
+ }).strict();
590
+
591
+ // src/storage/telemetry_repository_row_parser.ts
592
+ var TelemetryRepositoryRowParser = class {
593
+ parseRawEnvelope(input) {
594
+ const parsed = RawTelemetryEventRowSchema.safeParse(input);
595
+ if (!parsed.success) {
596
+ return { ok: false, error: parsed.error.message };
597
+ }
598
+ const metadata = this.parseJsonObject(parsed.data.metadata_json);
599
+ if (!metadata.ok) {
600
+ return metadata;
601
+ }
602
+ const conversationId = this.parseConversationId(
603
+ parsed.data.conversation_id,
604
+ parsed.data.conversation_id_kind
605
+ );
606
+ if (!conversationId.ok) {
607
+ return conversationId;
608
+ }
609
+ return {
610
+ ok: true,
611
+ value: Object.freeze({
612
+ schemaVersion: parsed.data.schema_version,
613
+ eventId: parsed.data.event_id,
614
+ producerId: parsed.data.producer_id,
615
+ sequence: parsed.data.sequence,
616
+ occurredAtMs: parsed.data.occurred_at_ms,
617
+ monotonicNs: parsed.data.monotonic_ns,
618
+ source: parsed.data.source,
619
+ processKind: parsed.data.process_kind,
620
+ level: parsed.data.level,
621
+ label: parsed.data.label,
622
+ conversationId: conversationId.value,
623
+ correlationId: parsed.data.correlation_id,
624
+ metadata: metadata.value
625
+ })
626
+ };
627
+ }
628
+ parseMetricSample(input) {
629
+ const parsed = MetricSampleRowSchema.safeParse(input);
630
+ return parsed.success ? { ok: true, value: parsed.data } : { ok: false, error: parsed.error.message };
631
+ }
632
+ parseMetricRollup(input) {
633
+ const parsed = MetricRollupRowSchema.safeParse(input);
634
+ return parsed.success ? { ok: true, value: parsed.data } : { ok: false, error: parsed.error.message };
635
+ }
636
+ parseJsonObject(input) {
637
+ try {
638
+ const decoded = JSON.parse(input);
639
+ const parsed = TelemetryJsonObjectSchema.safeParse(decoded);
640
+ return parsed.success ? { ok: true, value: Object.freeze(parsed.data) } : { ok: false, error: parsed.error.message };
641
+ } catch (cause) {
642
+ return { ok: false, error: cause instanceof Error ? cause.message : String(cause) };
643
+ }
644
+ }
645
+ parseNumericValues(input) {
646
+ try {
647
+ const decoded = JSON.parse(input);
648
+ const parsed = NumericValuesSchema.safeParse(decoded);
649
+ return parsed.success ? { ok: true, value: Object.freeze(parsed.data) } : { ok: false, error: parsed.error.message };
650
+ } catch (cause) {
651
+ return { ok: false, error: cause instanceof Error ? cause.message : String(cause) };
652
+ }
653
+ }
654
+ parseRollupValues(input) {
655
+ try {
656
+ const decoded = JSON.parse(input);
657
+ const parsed = MetricRollupValuesSchema.safeParse(decoded);
658
+ return parsed.success ? { ok: true, value: parsed.data } : { ok: false, error: parsed.error.message };
659
+ } catch (cause) {
660
+ return { ok: false, error: cause instanceof Error ? cause.message : String(cause) };
661
+ }
662
+ }
663
+ parseTimelineCursor(input) {
664
+ return this.parseCursor(input, TimelineCursorSchema);
665
+ }
666
+ parseMetricCursor(input) {
667
+ return this.parseCursor(input, MetricCursorSchema);
668
+ }
669
+ parseCursor(input, schema) {
670
+ try {
671
+ const decodedText = Buffer.from(input, "base64url").toString("utf8");
672
+ const decoded = JSON.parse(decodedText);
673
+ const parsed = schema.safeParse(decoded);
674
+ return parsed.success ? { ok: true, value: parsed.data } : { ok: false, error: parsed.error.message };
675
+ } catch (cause) {
676
+ return { ok: false, error: cause instanceof Error ? cause.message : String(cause) };
677
+ }
678
+ }
679
+ parseConversationId(value, kind) {
680
+ if (value === null) {
681
+ return { ok: true, value: null };
682
+ }
683
+ if (kind === "string") {
684
+ return { ok: true, value };
685
+ }
686
+ if (kind === "number") {
687
+ const numeric = Number(value);
688
+ return Number.isFinite(numeric) ? { ok: true, value: numeric } : { ok: false, error: `Invalid numeric conversation id ${value}` };
689
+ }
690
+ return { ok: false, error: "Conversation id kind is missing" };
691
+ }
692
+ };
693
+
694
+ // src/storage/telemetry_repository.ts
695
+ var RETENTION_PAGE_SIZE = 500;
696
+ var MetricCardinalityRejectionError = class extends Error {
697
+ constructor(message) {
698
+ super(message);
699
+ this.name = "MetricCardinalityRejectionError";
700
+ }
701
+ };
702
+ var TelemetryRepository = class {
703
+ constructor(options) {
704
+ this.options = options;
705
+ }
706
+ options;
707
+ rowParser = new TelemetryRepositoryRowParser();
708
+ definitionContract = new MetricDefinitionContract();
709
+ definitions = /* @__PURE__ */ new Map();
710
+ registerDefinitions(definitions) {
711
+ try {
712
+ const normalizedDefinitions = this.normalizeDefinitions(definitions);
713
+ const registeredAtMs = this.options.now();
714
+ const byName = /* @__PURE__ */ new Map();
715
+ for (const definition of normalizedDefinitions) {
716
+ const versions = byName.get(definition.name) ?? [];
717
+ versions.push(definition);
718
+ byName.set(definition.name, versions);
719
+ }
720
+ const transaction = this.options.connection.transaction(() => {
721
+ const table = this.options.tableMap.quoted("metricDefinitions");
722
+ const deactivate = this.options.connection.prepare(
723
+ `UPDATE ${table} SET active = 0 WHERE target_id = ? AND metric_name = ?`
724
+ );
725
+ const existing = this.options.connection.prepare(
726
+ `SELECT definition_checksum FROM ${table}
727
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?`
728
+ );
729
+ const insert = this.options.connection.prepare(
730
+ `INSERT INTO ${table} (
731
+ target_id, metric_name, definition_version, definition_json,
732
+ definition_checksum, active, registered_at_ms
733
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`
734
+ );
735
+ const activate = this.options.connection.prepare(
736
+ `UPDATE ${table} SET active = 1
737
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?`
738
+ );
739
+ for (const [metricName, versions] of byName) {
740
+ deactivate.run(this.options.targetId, metricName);
741
+ const latestVersion = Math.max(...versions.map((definition) => definition.version));
742
+ for (const definition of versions) {
743
+ const definitionJson = canonicalJson(definition);
744
+ const definitionChecksum = telemetryChecksum(definition);
745
+ const prior = existing.get(this.options.targetId, definition.name, definition.version);
746
+ if (prior !== void 0) {
747
+ const priorChecksum = this.readStringField(prior, "definition_checksum");
748
+ if (priorChecksum !== definitionChecksum) {
749
+ throw new Error(
750
+ `Metric ${definition.name} version ${definition.version} changed without a version bump`
751
+ );
752
+ }
753
+ } else {
754
+ insert.run(
755
+ this.options.targetId,
756
+ definition.name,
757
+ definition.version,
758
+ definitionJson,
759
+ definitionChecksum,
760
+ definition.version === latestVersion ? 1 : 0,
761
+ registeredAtMs
762
+ );
763
+ }
764
+ }
765
+ activate.run(this.options.targetId, metricName, latestVersion);
766
+ }
767
+ });
768
+ transaction();
769
+ const loaded = this.loadDefinitionsReadOnly(normalizedDefinitions);
770
+ if (!loaded.ok) {
771
+ throw new Error(loaded.error);
772
+ }
773
+ return { ok: true, value: void 0 };
774
+ } catch (cause) {
775
+ return { ok: false, error: this.errorMessage(cause) };
776
+ }
777
+ }
778
+ loadDefinitionsReadOnly(expectedDefinitions) {
779
+ try {
780
+ const normalizedExpectedDefinitions = this.normalizeDefinitions(expectedDefinitions);
781
+ const table = this.options.tableMap.quoted("metricDefinitions");
782
+ const rows = this.options.connection.prepare(
783
+ `SELECT metric_name, definition_version, definition_json, definition_checksum, active
784
+ FROM ${table} WHERE target_id = ?`
785
+ ).all(this.options.targetId);
786
+ const loaded = /* @__PURE__ */ new Map();
787
+ for (const rawRow of rows) {
788
+ const row = StoredMetricDefinitionRowSchema.parse(rawRow);
789
+ const parsedJson = JSON.parse(row.definition_json);
790
+ const parsedDefinition = StoredMetricDefinitionSchema.parse(parsedJson);
791
+ const normalized = this.definitionContract.normalize(parsedDefinition);
792
+ if (!normalized.ok) {
793
+ throw new Error(normalized.error);
794
+ }
795
+ const definition = normalized.value;
796
+ const identityMatches = definition.name === row.metric_name && definition.version === row.definition_version;
797
+ if (!identityMatches) {
798
+ throw new Error("Stored metric definition identity does not match its row");
799
+ }
800
+ const checksum = telemetryChecksum(definition);
801
+ const canonicalRowMatches = checksum === row.definition_checksum && canonicalJson(definition) === row.definition_json;
802
+ const legacyRowMatches = !canonicalRowMatches && (0, import_node_crypto.createHash)("sha256").update(row.definition_json, "utf8").digest("hex") === row.definition_checksum;
803
+ if (!canonicalRowMatches && !legacyRowMatches) {
804
+ if (row.active !== 1) {
805
+ continue;
806
+ }
807
+ throw new Error(
808
+ `Stored metric ${definition.name} version ${definition.version} checksum is invalid`
809
+ );
810
+ }
811
+ loaded.set(this.definitionKey(definition.name, definition.version), definition);
812
+ }
813
+ for (const expected of normalizedExpectedDefinitions) {
814
+ const stored = loaded.get(this.definitionKey(expected.name, expected.version));
815
+ if (stored === void 0 || telemetryChecksum(stored) !== telemetryChecksum(expected)) {
816
+ throw new Error(
817
+ `Configured metric ${expected.name} version ${expected.version} is not registered in target ${this.options.targetId}`
818
+ );
819
+ }
820
+ }
821
+ this.definitions.clear();
822
+ for (const [identity, definition] of loaded) {
823
+ this.definitions.set(identity, definition);
824
+ }
825
+ return { ok: true, value: void 0 };
826
+ } catch (cause) {
827
+ return { ok: false, error: this.errorMessage(cause) };
828
+ }
829
+ }
830
+ persist(request) {
831
+ try {
832
+ const outsideHorizon = this.outsideIngestionHorizon(request.batch);
833
+ if (outsideHorizon !== null) {
834
+ return { kind: "acknowledged", acknowledgement: outsideHorizon };
835
+ }
836
+ const replay = this.findReplay(request.batch);
837
+ if (replay !== null) {
838
+ return { kind: "acknowledged", acknowledgement: replay };
839
+ }
840
+ const acknowledgement = this.persistTransaction(request);
841
+ return { kind: "acknowledged", acknowledgement };
842
+ } catch (cause) {
843
+ return { kind: "failed", reason: this.errorMessage(cause) };
844
+ }
845
+ }
846
+ timeline(query, rawRetentionCutoffMs) {
847
+ try {
848
+ const cursor = query.cursor === null ? null : this.rowParser.parseTimelineCursor(query.cursor);
849
+ if (cursor !== null && !cursor.ok) {
850
+ return { ok: false, error: { kind: "invalid-query", message: cursor.error } };
851
+ }
852
+ if (cursor !== null && cursor.value.order !== query.order) {
853
+ return {
854
+ ok: false,
855
+ error: { kind: "invalid-query", message: "Timeline cursor order changed" }
856
+ };
857
+ }
858
+ const availableFromMs = Math.max(query.fromMs, rawRetentionCutoffMs);
859
+ const clauses = ["target_id = ?", "occurred_at_ms >= ?", "occurred_at_ms < ?"];
860
+ const params = [this.options.targetId, availableFromMs, query.toMs];
861
+ this.addTimelineFilters(query, clauses, params);
862
+ if (cursor?.ok) {
863
+ this.addTimelineCursor(cursor.value, query.order, clauses, params);
864
+ }
865
+ const direction = query.order === "asc" ? "ASC" : "DESC";
866
+ const table = this.options.tableMap.quoted("rawEvents");
867
+ const sql = `SELECT
868
+ producer_id, sequence, event_id, occurred_at_ms, monotonic_ns,
869
+ source, process_kind, level, label, conversation_id,
870
+ conversation_id_kind, correlation_id, metadata_json, schema_version
871
+ FROM ${table}
872
+ WHERE ${clauses.join(" AND ")}
873
+ ORDER BY occurred_at_ms ${direction}, producer_id ${direction}, sequence ${direction}
874
+ LIMIT ?`;
875
+ params.push(query.limit + 1);
876
+ const rows = this.options.connection.prepare(sql).all(...params);
877
+ const hasNextPage = rows.length > query.limit;
878
+ const pageRows = hasNextPage ? rows.slice(0, query.limit) : rows;
879
+ const events = [];
880
+ for (const row of pageRows) {
881
+ const parsed = this.rowParser.parseRawEnvelope(row);
882
+ if (!parsed.ok) {
883
+ return { ok: false, error: { kind: "storage-failure", message: parsed.error } };
884
+ }
885
+ events.push(parsed.value);
886
+ }
887
+ const last = events[events.length - 1];
888
+ const nextCursor = hasNextPage && last ? this.encodeCursor({
889
+ version: 1,
890
+ order: query.order,
891
+ occurredAtMs: last.occurredAtMs,
892
+ producerId: last.producerId,
893
+ sequence: last.sequence
894
+ }) : null;
895
+ const unavailableEnd = Math.min(query.toMs, rawRetentionCutoffMs);
896
+ const gaps = query.fromMs < unavailableEnd ? [{ fromMs: query.fromMs, toMs: unavailableEnd, reason: "raw-retention" }] : [];
897
+ return {
898
+ ok: true,
899
+ value: {
900
+ targetId: this.options.targetId,
901
+ events: Object.freeze(events),
902
+ gaps: Object.freeze(gaps),
903
+ nextCursor
904
+ }
905
+ };
906
+ } catch (cause) {
907
+ return { ok: false, error: { kind: "storage-failure", message: this.errorMessage(cause) } };
908
+ }
909
+ }
910
+ metrics(query) {
911
+ try {
912
+ const definitions = this.metricDefinitions(query.metric);
913
+ if (definitions.length === 0) {
914
+ return {
915
+ ok: false,
916
+ error: { kind: "invalid-query", message: `Unknown metric ${query.metric}` }
917
+ };
918
+ }
919
+ const validationError = this.validateMetricQuery(query, definitions);
920
+ if (validationError !== null) {
921
+ return { ok: false, error: { kind: "invalid-query", message: validationError } };
922
+ }
923
+ const cursor = query.cursor === null ? null : this.rowParser.parseMetricCursor(query.cursor);
924
+ if (cursor !== null && !cursor.ok) {
925
+ return { ok: false, error: { kind: "invalid-query", message: cursor.error } };
926
+ }
927
+ if (cursor !== null && cursor.value.order !== query.order) {
928
+ return {
929
+ ok: false,
930
+ error: { kind: "invalid-query", message: "Metric cursor order changed" }
931
+ };
932
+ }
933
+ const coverage = this.planCoverage(query, definitions);
934
+ const rows = this.readRollupsForCoverage(query, coverage.intervals);
935
+ const filtered = rows.filter((row) => this.matchesWhere(row.bucket.dimensions, query.where));
936
+ const ordered = filtered.sort(
937
+ (left, right) => this.compareParsedRollups(left, right, query.order)
938
+ );
939
+ const afterCursor = cursor?.ok ? ordered.filter((row) => this.isAfterMetricCursor(row, cursor.value, query.order)) : ordered;
940
+ const hasNextPage = afterCursor.length > query.limit;
941
+ const page = hasNextPage ? afterCursor.slice(0, query.limit) : afterCursor;
942
+ const projected = page.map((row) => ({
943
+ ...row,
944
+ bucket: {
945
+ ...row.bucket,
946
+ values: this.selectMeasurements(row.bucket.values, query.measurements)
947
+ }
948
+ }));
949
+ const segments = this.rowsToSegments(coverage.intervals, projected);
950
+ const last = page[page.length - 1];
951
+ const nextCursor = hasNextPage && last ? this.encodeCursor({
952
+ version: 1,
953
+ order: query.order,
954
+ bucketStartMs: last.bucket.startMs,
955
+ resolutionMs: last.resolutionMs,
956
+ definitionVersion: last.definitionVersion,
957
+ dimensionKey: last.dimensionKey
958
+ }) : null;
959
+ return {
960
+ ok: true,
961
+ value: {
962
+ targetId: this.options.targetId,
963
+ metric: query.metric,
964
+ segments,
965
+ gaps: coverage.gaps,
966
+ nextCursor,
967
+ partialFailures: []
968
+ }
969
+ };
970
+ } catch (cause) {
971
+ return { ok: false, error: { kind: "storage-failure", message: this.errorMessage(cause) } };
972
+ }
973
+ }
974
+ materializeRollups(nowMs, maxWorkMs) {
975
+ const startedAt = this.options.now();
976
+ let affectedRows = 0;
977
+ let rejectedBucketCount = 0;
978
+ const rejectedBucketExamples = [];
979
+ try {
980
+ for (const definition of this.definitions.values()) {
981
+ for (const resolutionMs of definition.resolutionsMs) {
982
+ const dueBeforeMs = nowMs - this.options.retention.lateArrivalMs;
983
+ const bucketStarts = this.findDueBucketStarts(definition, resolutionMs, dueBeforeMs);
984
+ for (const bucketStartMs of bucketStarts) {
985
+ if (this.options.now() - startedAt >= maxWorkMs) {
986
+ return {
987
+ ok: true,
988
+ value: {
989
+ stage: "rollup",
990
+ affectedRows,
991
+ affectedPages: 0,
992
+ elapsedMs: this.options.now() - startedAt,
993
+ detail: this.rollupStageDetail(
994
+ rejectedBucketCount,
995
+ rejectedBucketExamples,
996
+ true
997
+ )
998
+ }
999
+ };
1000
+ }
1001
+ const work = this.loadBucketWork(definition, resolutionMs, bucketStartMs);
1002
+ if (this.bucketIsCurrent(work)) {
1003
+ continue;
1004
+ }
1005
+ try {
1006
+ affectedRows += this.materializeBucket(work, nowMs);
1007
+ } catch (cause) {
1008
+ if (!(cause instanceof MetricCardinalityRejectionError)) {
1009
+ throw cause;
1010
+ }
1011
+ this.recordRejectedBucket(work, nowMs, cause.message);
1012
+ rejectedBucketCount += 1;
1013
+ if (rejectedBucketExamples.length < 3) {
1014
+ rejectedBucketExamples.push(
1015
+ `${definition.name}:${definition.version}:${resolutionMs}:${bucketStartMs}: ${cause.message}`
1016
+ );
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+ return {
1023
+ ok: true,
1024
+ value: {
1025
+ stage: "rollup",
1026
+ affectedRows,
1027
+ affectedPages: 0,
1028
+ elapsedMs: this.options.now() - startedAt,
1029
+ detail: this.rollupStageDetail(
1030
+ rejectedBucketCount,
1031
+ rejectedBucketExamples,
1032
+ false
1033
+ )
1034
+ }
1035
+ };
1036
+ } catch (cause) {
1037
+ return { ok: false, error: this.errorMessage(cause) };
1038
+ }
1039
+ }
1040
+ beginRetentionDrain(nowMs) {
1041
+ const steps = [
1042
+ this.createRawEventRetentionStep(nowMs),
1043
+ this.createSampleRetentionStep(nowMs),
1044
+ ...this.createRollupRetentionSteps(nowMs),
1045
+ ...this.createAuxiliaryRetentionSteps(nowMs)
1046
+ ];
1047
+ let index = 0;
1048
+ return {
1049
+ nextBatch: () => {
1050
+ try {
1051
+ if (steps.length === 0) {
1052
+ return { ok: true, value: { deletedRows: 0, hasMore: false } };
1053
+ }
1054
+ if (index >= steps.length) {
1055
+ index = 0;
1056
+ }
1057
+ const step = steps[index];
1058
+ if (step === void 0) {
1059
+ return { ok: true, value: { deletedRows: 0, hasMore: false } };
1060
+ }
1061
+ const outcome = step();
1062
+ if (outcome.done) {
1063
+ steps.splice(index, 1);
1064
+ } else {
1065
+ index += 1;
1066
+ }
1067
+ return {
1068
+ ok: true,
1069
+ value: { deletedRows: outcome.deleted, hasMore: steps.length > 0 }
1070
+ };
1071
+ } catch (cause) {
1072
+ return { ok: false, error: this.errorMessage(cause) };
1073
+ }
1074
+ }
1075
+ };
1076
+ }
1077
+ checkpoint() {
1078
+ const startedAt = this.options.now();
1079
+ try {
1080
+ const rows = this.options.connection.pragma("wal_checkpoint(PASSIVE)");
1081
+ return {
1082
+ ok: true,
1083
+ value: {
1084
+ stage: "checkpoint",
1085
+ affectedRows: Array.isArray(rows) ? rows.length : 0,
1086
+ affectedPages: 0,
1087
+ elapsedMs: this.options.now() - startedAt,
1088
+ detail: null
1089
+ }
1090
+ };
1091
+ } catch (cause) {
1092
+ return { ok: false, error: this.errorMessage(cause) };
1093
+ }
1094
+ }
1095
+ reclaim(maxPages) {
1096
+ const startedAt = this.options.now();
1097
+ try {
1098
+ const before = Number(this.options.connection.pragma("freelist_count", { simple: true }));
1099
+ this.options.connection.pragma(`incremental_vacuum(${Math.max(0, Math.floor(maxPages))})`);
1100
+ const after = Number(this.options.connection.pragma("freelist_count", { simple: true }));
1101
+ return {
1102
+ ok: true,
1103
+ value: {
1104
+ stage: "reclaim",
1105
+ affectedRows: 0,
1106
+ affectedPages: Math.max(0, before - after),
1107
+ elapsedMs: this.options.now() - startedAt,
1108
+ detail: null
1109
+ }
1110
+ };
1111
+ } catch (cause) {
1112
+ return { ok: false, error: this.errorMessage(cause) };
1113
+ }
1114
+ }
1115
+ pageStats() {
1116
+ try {
1117
+ return {
1118
+ ok: true,
1119
+ value: {
1120
+ pageCount: Number(this.options.connection.pragma("page_count", { simple: true })),
1121
+ freePages: Number(this.options.connection.pragma("freelist_count", { simple: true })),
1122
+ pageSize: Number(this.options.connection.pragma("page_size", { simple: true }))
1123
+ }
1124
+ };
1125
+ } catch (cause) {
1126
+ return { ok: false, error: this.errorMessage(cause) };
1127
+ }
1128
+ }
1129
+ recordMaintenance(record) {
1130
+ try {
1131
+ const table = this.options.tableMap.quoted("maintenanceRuns");
1132
+ const affectedRows = record.stages.reduce((sum, stage) => sum + stage.affectedRows, 0);
1133
+ const affectedPages = record.stages.reduce((sum, stage) => sum + stage.affectedPages, 0);
1134
+ const detail = canonicalJson({
1135
+ stages: record.stages,
1136
+ failedStage: record.failedStage,
1137
+ error: record.error
1138
+ });
1139
+ this.options.connection.prepare(
1140
+ `INSERT INTO ${table} (
1141
+ target_id, run_id, state, stage, started_at_ms, completed_at_ms,
1142
+ affected_rows, affected_pages, detail
1143
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1144
+ ON CONFLICT(target_id, run_id, stage) DO UPDATE SET
1145
+ state = excluded.state,
1146
+ completed_at_ms = excluded.completed_at_ms,
1147
+ affected_rows = excluded.affected_rows,
1148
+ affected_pages = excluded.affected_pages,
1149
+ detail = excluded.detail`
1150
+ ).run(
1151
+ this.options.targetId,
1152
+ record.runId,
1153
+ record.state,
1154
+ record.kind,
1155
+ record.startedAtMs,
1156
+ record.completedAtMs,
1157
+ affectedRows,
1158
+ affectedPages,
1159
+ detail
1160
+ );
1161
+ return { ok: true, value: void 0 };
1162
+ } catch (cause) {
1163
+ return { ok: false, error: this.errorMessage(cause) };
1164
+ }
1165
+ }
1166
+ recoverInterruptedMaintenance(nowMs) {
1167
+ try {
1168
+ const table = this.options.tableMap.quoted("maintenanceRuns");
1169
+ const detail = canonicalJson({
1170
+ stages: [],
1171
+ failedStage: null,
1172
+ error: "interrupted-before-restart"
1173
+ });
1174
+ const result = this.options.connection.prepare(
1175
+ `UPDATE ${table}
1176
+ SET state = 'failed', completed_at_ms = ?, detail = ?
1177
+ WHERE target_id = ? AND state = 'started'`
1178
+ ).run(nowMs, detail, this.options.targetId);
1179
+ return { ok: true, value: result.changes };
1180
+ } catch (cause) {
1181
+ return { ok: false, error: this.errorMessage(cause) };
1182
+ }
1183
+ }
1184
+ committedDropCounters() {
1185
+ try {
1186
+ const table = this.options.tableMap.quoted("committedDropCounters");
1187
+ const rows = this.options.connection.prepare(
1188
+ `SELECT reason, priority, count
1189
+ FROM ${table}
1190
+ WHERE target_id = ?
1191
+ ORDER BY reason ASC, priority ASC`
1192
+ ).all(this.options.targetId);
1193
+ const counters = rows.map((row) => this.parseCommittedDropCounter(row));
1194
+ return { ok: true, value: Object.freeze(counters) };
1195
+ } catch (cause) {
1196
+ return { ok: false, error: this.errorMessage(cause) };
1197
+ }
1198
+ }
1199
+ findReplay(batch) {
1200
+ const table = this.options.tableMap.quoted("batchAcknowledgements");
1201
+ const rows = this.options.connection.prepare(
1202
+ `SELECT idempotency_key, producer_id, sequence_start, sequence_end,
1203
+ schema_version, checksum, accepted_events, committed_at_ms
1204
+ FROM ${table}
1205
+ WHERE target_id = ? AND producer_id = ?
1206
+ AND sequence_start <= ? AND sequence_end >= ?`
1207
+ ).all(this.options.targetId, batch.producerId, batch.sequenceEnd, batch.sequenceStart);
1208
+ if (rows.length === 0) {
1209
+ return this.findPrunedReplayConflict(batch);
1210
+ }
1211
+ for (const row of rows) {
1212
+ const parsed = BatchAcknowledgementRowSchema.safeParse(row);
1213
+ if (!parsed.success) {
1214
+ throw new Error(parsed.error.message);
1215
+ }
1216
+ const identical = parsed.data.idempotency_key === batch.idempotencyKey && parsed.data.checksum === batch.checksum && parsed.data.sequence_start === batch.sequenceStart && parsed.data.sequence_end === batch.sequenceEnd;
1217
+ if (identical) {
1218
+ return {
1219
+ kind: "committed",
1220
+ idempotencyKey: batch.idempotencyKey,
1221
+ committedAtMs: parsed.data.committed_at_ms,
1222
+ acceptedEvents: parsed.data.accepted_events,
1223
+ duplicate: true,
1224
+ targetStates: [this.committedTargetState(0)]
1225
+ };
1226
+ }
1227
+ }
1228
+ return {
1229
+ kind: "conflict",
1230
+ idempotencyKey: batch.idempotencyKey,
1231
+ reason: "Producer sequence range overlaps a different committed batch",
1232
+ targetStates: [
1233
+ {
1234
+ targetId: this.options.targetId,
1235
+ delivery: "required",
1236
+ state: "failed",
1237
+ attempts: 1,
1238
+ reason: "sequence-range-conflict"
1239
+ }
1240
+ ]
1241
+ };
1242
+ }
1243
+ outsideIngestionHorizon(batch) {
1244
+ const cutoffMs = this.options.now() - this.longestRetentionMs();
1245
+ const containsExpiredEvent = batch.envelopes.some(
1246
+ (envelope) => envelope.occurredAtMs < cutoffMs
1247
+ );
1248
+ if (!containsExpiredEvent) {
1249
+ return null;
1250
+ }
1251
+ return {
1252
+ kind: "rejected",
1253
+ idempotencyKey: batch.idempotencyKey,
1254
+ code: "invalid-batch",
1255
+ reason: "Telemetry batch is older than the configured ingestion horizon",
1256
+ targetStates: [
1257
+ {
1258
+ targetId: this.options.targetId,
1259
+ delivery: "required",
1260
+ state: "failed",
1261
+ attempts: 1,
1262
+ reason: "batch-outside-ingestion-horizon"
1263
+ }
1264
+ ]
1265
+ };
1266
+ }
1267
+ findPrunedReplayConflict(batch) {
1268
+ const table = this.options.tableMap.quoted("ingestWatermarks");
1269
+ const row = this.options.connection.prepare(
1270
+ `SELECT highest_sequence
1271
+ FROM ${table}
1272
+ WHERE target_id = ? AND producer_id = ?`
1273
+ ).get(this.options.targetId, batch.producerId);
1274
+ if (row === void 0) {
1275
+ return null;
1276
+ }
1277
+ const highestSequence = this.readNumberField(row, "highest_sequence");
1278
+ if (batch.sequenceStart > highestSequence) {
1279
+ return null;
1280
+ }
1281
+ return {
1282
+ kind: "conflict",
1283
+ idempotencyKey: batch.idempotencyKey,
1284
+ reason: "Producer sequence range is at or below the durable ingest watermark",
1285
+ targetStates: [
1286
+ {
1287
+ targetId: this.options.targetId,
1288
+ delivery: "required",
1289
+ state: "failed",
1290
+ attempts: 1,
1291
+ reason: "sequence-at-or-below-watermark"
1292
+ }
1293
+ ]
1294
+ };
1295
+ }
1296
+ persistTransaction(request) {
1297
+ const committedAtMs = this.options.now();
1298
+ const transaction = this.options.connection.transaction(() => {
1299
+ this.insertRawEvents(request, committedAtMs);
1300
+ this.insertMetricSamples(request, committedAtMs);
1301
+ this.insertExtractionFailures(request, committedAtMs);
1302
+ this.upsertCommittedDropCounters(request.batch, committedAtMs);
1303
+ this.insertBatchAcknowledgement(request.batch, committedAtMs);
1304
+ this.updateIngestWatermark(request.batch, committedAtMs);
1305
+ });
1306
+ transaction();
1307
+ return {
1308
+ kind: "committed",
1309
+ idempotencyKey: request.batch.idempotencyKey,
1310
+ committedAtMs,
1311
+ acceptedEvents: request.batch.envelopes.length,
1312
+ duplicate: false,
1313
+ targetStates: [this.committedTargetState(1)]
1314
+ };
1315
+ }
1316
+ insertRawEvents(request, insertedAtMs) {
1317
+ const table = this.options.tableMap.quoted("rawEvents");
1318
+ const insert = this.options.connection.prepare(
1319
+ `INSERT INTO ${table} (
1320
+ target_id, producer_id, sequence, event_id, batch_idempotency_key,
1321
+ occurred_at_ms, monotonic_ns, source, process_kind, level, label,
1322
+ conversation_id, conversation_id_kind, correlation_id, metadata_json,
1323
+ schema_version, inserted_at_ms
1324
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1325
+ );
1326
+ for (const envelope of request.batch.envelopes) {
1327
+ const conversationId = envelope.conversationId === null ? null : String(envelope.conversationId);
1328
+ const conversationKind = envelope.conversationId === null ? null : typeof envelope.conversationId;
1329
+ insert.run(
1330
+ this.options.targetId,
1331
+ envelope.producerId,
1332
+ envelope.sequence,
1333
+ envelope.eventId,
1334
+ request.batch.idempotencyKey,
1335
+ envelope.occurredAtMs,
1336
+ envelope.monotonicNs,
1337
+ envelope.source,
1338
+ envelope.processKind,
1339
+ envelope.level,
1340
+ envelope.label,
1341
+ conversationId,
1342
+ conversationKind,
1343
+ envelope.correlationId,
1344
+ canonicalJson(envelope.metadata),
1345
+ envelope.schemaVersion,
1346
+ insertedAtMs
1347
+ );
1348
+ }
1349
+ }
1350
+ insertMetricSamples(request, _insertedAtMs) {
1351
+ const samplesTable = this.options.tableMap.quoted("metricSamples");
1352
+ const completionTable = this.options.tableMap.quoted("rollupCompletions");
1353
+ const insert = this.options.connection.prepare(
1354
+ `INSERT INTO ${samplesTable} (
1355
+ target_id, metric_name, definition_version, occurred_at_ms,
1356
+ producer_id, sequence, dimension_key, dimensions_json, values_json
1357
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1358
+ );
1359
+ const invalidate = this.options.connection.prepare(
1360
+ `DELETE FROM ${completionTable}
1361
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
1362
+ AND resolution_ms = ? AND bucket_start_ms = ?`
1363
+ );
1364
+ for (const sample of request.samples) {
1365
+ insert.run(
1366
+ this.options.targetId,
1367
+ sample.metricName,
1368
+ sample.definitionVersion,
1369
+ sample.occurredAtMs,
1370
+ sample.producerId,
1371
+ sample.sequence,
1372
+ sample.dimensionKey,
1373
+ canonicalJson(sample.dimensions),
1374
+ canonicalJson(sample.values)
1375
+ );
1376
+ const definition = this.definitions.get(
1377
+ this.definitionKey(sample.metricName, sample.definitionVersion)
1378
+ );
1379
+ if (!definition) {
1380
+ throw new Error(
1381
+ `Missing registered metric definition ${sample.metricName}:${sample.definitionVersion}`
1382
+ );
1383
+ }
1384
+ for (const resolutionMs of definition.resolutionsMs) {
1385
+ const bucketStartMs = this.bucketStart(sample.occurredAtMs, resolutionMs);
1386
+ invalidate.run(
1387
+ this.options.targetId,
1388
+ sample.metricName,
1389
+ sample.definitionVersion,
1390
+ resolutionMs,
1391
+ bucketStartMs
1392
+ );
1393
+ }
1394
+ }
1395
+ }
1396
+ insertExtractionFailures(request, recordedAtMs) {
1397
+ const table = this.options.tableMap.quoted("extractionFailures");
1398
+ const insert = this.options.connection.prepare(
1399
+ `INSERT OR REPLACE INTO ${table} (
1400
+ target_id, metric_name, definition_version, producer_id,
1401
+ sequence, reason, recorded_at_ms
1402
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`
1403
+ );
1404
+ for (const failure of request.extractionFailures) {
1405
+ insert.run(
1406
+ this.options.targetId,
1407
+ failure.metricName,
1408
+ failure.definitionVersion,
1409
+ failure.producerId,
1410
+ failure.sequence,
1411
+ failure.reason.slice(0, 1024),
1412
+ recordedAtMs
1413
+ );
1414
+ }
1415
+ }
1416
+ upsertCommittedDropCounters(batch, committedAtMs) {
1417
+ const table = this.options.tableMap.quoted("committedDropCounters");
1418
+ const upsert = this.options.connection.prepare(
1419
+ `INSERT INTO ${table} (
1420
+ target_id, reason, priority, count, first_committed_at_ms, last_committed_at_ms
1421
+ ) VALUES (?, ?, ?, ?, ?, ?)
1422
+ ON CONFLICT(target_id, reason, priority) DO UPDATE SET
1423
+ count = MIN(9007199254740991, ${table}.count + excluded.count),
1424
+ first_committed_at_ms = MIN(
1425
+ ${table}.first_committed_at_ms,
1426
+ excluded.first_committed_at_ms
1427
+ ),
1428
+ last_committed_at_ms = MAX(
1429
+ ${table}.last_committed_at_ms,
1430
+ excluded.last_committed_at_ms
1431
+ )`
1432
+ );
1433
+ for (const counter of batch.dropCounters) {
1434
+ if (!Number.isSafeInteger(counter.count) || counter.count <= 0) {
1435
+ throw new Error("Committed telemetry drop counters must be positive safe integers");
1436
+ }
1437
+ upsert.run(
1438
+ this.options.targetId,
1439
+ counter.reason,
1440
+ counter.priority,
1441
+ counter.count,
1442
+ committedAtMs,
1443
+ committedAtMs
1444
+ );
1445
+ }
1446
+ }
1447
+ insertBatchAcknowledgement(batch, committedAtMs) {
1448
+ const table = this.options.tableMap.quoted("batchAcknowledgements");
1449
+ this.options.connection.prepare(
1450
+ `INSERT INTO ${table} (
1451
+ target_id, idempotency_key, producer_id, sequence_start, sequence_end,
1452
+ schema_version, checksum, accepted_events, committed_at_ms
1453
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1454
+ ).run(
1455
+ this.options.targetId,
1456
+ batch.idempotencyKey,
1457
+ batch.producerId,
1458
+ batch.sequenceStart,
1459
+ batch.sequenceEnd,
1460
+ batch.schemaVersion,
1461
+ batch.checksum,
1462
+ batch.envelopes.length,
1463
+ committedAtMs
1464
+ );
1465
+ }
1466
+ updateIngestWatermark(batch, committedAtMs) {
1467
+ const table = this.options.tableMap.quoted("ingestWatermarks");
1468
+ const lastOccurredAtMs = Math.max(...batch.envelopes.map((envelope) => envelope.occurredAtMs));
1469
+ const internalGapCount = batch.sequenceEnd - batch.sequenceStart + 1 - batch.envelopes.length;
1470
+ const initialGapCount = batch.sequenceStart + internalGapCount;
1471
+ this.options.connection.prepare(
1472
+ `INSERT INTO ${table} (
1473
+ target_id, producer_id, highest_sequence, last_occurred_at_ms,
1474
+ last_committed_at_ms, observed_gap_count
1475
+ ) VALUES (?, ?, ?, ?, ?, ?)
1476
+ ON CONFLICT(target_id, producer_id) DO UPDATE SET
1477
+ observed_gap_count = ${table}.observed_gap_count
1478
+ + MAX(0, ? - ${table}.highest_sequence - 1)
1479
+ + ?,
1480
+ highest_sequence = MAX(${table}.highest_sequence, excluded.highest_sequence),
1481
+ last_occurred_at_ms = MAX(${table}.last_occurred_at_ms, excluded.last_occurred_at_ms),
1482
+ last_committed_at_ms = excluded.last_committed_at_ms`
1483
+ ).run(
1484
+ this.options.targetId,
1485
+ batch.producerId,
1486
+ batch.sequenceEnd,
1487
+ lastOccurredAtMs,
1488
+ committedAtMs,
1489
+ initialGapCount,
1490
+ batch.sequenceStart,
1491
+ internalGapCount
1492
+ );
1493
+ }
1494
+ addTimelineFilters(query, clauses, params) {
1495
+ const filters = [
1496
+ [
1497
+ query.conversationId,
1498
+ "conversation_id = ?",
1499
+ query.conversationId === null ? null : String(query.conversationId)
1500
+ ],
1501
+ [query.source, "source = ?", query.source],
1502
+ [query.label, "label = ?", query.label],
1503
+ [query.level, "level = ?", query.level],
1504
+ [query.producerId, "producer_id = ?", query.producerId],
1505
+ [query.correlationId, "correlation_id = ?", query.correlationId]
1506
+ ];
1507
+ for (const [guard, clause, parameter] of filters) {
1508
+ if (guard !== null) {
1509
+ clauses.push(clause);
1510
+ params.push(parameter);
1511
+ }
1512
+ }
1513
+ }
1514
+ addTimelineCursor(cursor, order, clauses, params) {
1515
+ const comparison = order === "asc" ? ">" : "<";
1516
+ clauses.push(`(
1517
+ occurred_at_ms ${comparison} ?
1518
+ OR (occurred_at_ms = ? AND producer_id ${comparison} ?)
1519
+ OR (occurred_at_ms = ? AND producer_id = ? AND sequence ${comparison} ?)
1520
+ )`);
1521
+ params.push(
1522
+ cursor.occurredAtMs,
1523
+ cursor.occurredAtMs,
1524
+ cursor.producerId,
1525
+ cursor.occurredAtMs,
1526
+ cursor.producerId,
1527
+ cursor.sequence
1528
+ );
1529
+ }
1530
+ findDueBucketStarts(definition, resolutionMs, dueBeforeMs) {
1531
+ const sampleTable = this.options.tableMap.quoted("metricSamples");
1532
+ const completionTable = this.options.tableMap.quoted("rollupCompletions");
1533
+ const rows = this.options.connection.prepare(
1534
+ `WITH sample_buckets AS (
1535
+ SELECT
1536
+ CAST(occurred_at_ms / ? AS INTEGER) * ? AS bucket_start_ms,
1537
+ COUNT(*) AS sample_count,
1538
+ MAX(occurred_at_ms) AS sample_watermark_ms
1539
+ FROM ${sampleTable}
1540
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
1541
+ AND occurred_at_ms < ?
1542
+ AND (CAST(occurred_at_ms / ? AS INTEGER) * ?) + ? <= ?
1543
+ GROUP BY bucket_start_ms
1544
+ )
1545
+ SELECT bucket.bucket_start_ms
1546
+ FROM sample_buckets AS bucket
1547
+ LEFT JOIN ${completionTable} AS completion
1548
+ ON completion.target_id = ?
1549
+ AND completion.metric_name = ?
1550
+ AND completion.definition_version = ?
1551
+ AND completion.resolution_ms = ?
1552
+ AND completion.bucket_start_ms = bucket.bucket_start_ms
1553
+ WHERE completion.bucket_start_ms IS NULL
1554
+ OR completion.sample_count <> bucket.sample_count
1555
+ OR completion.sample_watermark_ms <> bucket.sample_watermark_ms
1556
+ ORDER BY bucket.bucket_start_ms ASC
1557
+ LIMIT 256`
1558
+ ).all(
1559
+ resolutionMs,
1560
+ resolutionMs,
1561
+ this.options.targetId,
1562
+ definition.name,
1563
+ definition.version,
1564
+ dueBeforeMs,
1565
+ resolutionMs,
1566
+ resolutionMs,
1567
+ resolutionMs,
1568
+ dueBeforeMs,
1569
+ this.options.targetId,
1570
+ definition.name,
1571
+ definition.version,
1572
+ resolutionMs
1573
+ );
1574
+ return rows.map((row) => this.readNumberField(row, "bucket_start_ms"));
1575
+ }
1576
+ loadBucketWork(definition, resolutionMs, bucketStartMs) {
1577
+ const table = this.options.tableMap.quoted("metricSamples");
1578
+ const rows = this.options.connection.prepare(
1579
+ `SELECT metric_name, definition_version, occurred_at_ms, producer_id,
1580
+ sequence, dimension_key, dimensions_json, values_json
1581
+ FROM ${table}
1582
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
1583
+ AND occurred_at_ms >= ? AND occurred_at_ms < ?
1584
+ ORDER BY occurred_at_ms ASC, producer_id ASC, sequence ASC`
1585
+ ).all(
1586
+ this.options.targetId,
1587
+ definition.name,
1588
+ definition.version,
1589
+ bucketStartMs,
1590
+ bucketStartMs + resolutionMs
1591
+ );
1592
+ const samples = rows.map((row) => {
1593
+ const parsed = this.rowParser.parseMetricSample(row);
1594
+ if (!parsed.ok) {
1595
+ throw new Error(parsed.error);
1596
+ }
1597
+ return parsed.value;
1598
+ });
1599
+ return { definition, resolutionMs, bucketStartMs, samples };
1600
+ }
1601
+ bucketIsCurrent(work) {
1602
+ const table = this.options.tableMap.quoted("rollupCompletions");
1603
+ const row = this.options.connection.prepare(
1604
+ `SELECT sample_count, sample_watermark_ms
1605
+ FROM ${table}
1606
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
1607
+ AND resolution_ms = ? AND bucket_start_ms = ?`
1608
+ ).get(
1609
+ this.options.targetId,
1610
+ work.definition.name,
1611
+ work.definition.version,
1612
+ work.resolutionMs,
1613
+ work.bucketStartMs
1614
+ );
1615
+ if (row === void 0) {
1616
+ return false;
1617
+ }
1618
+ const sampleCount = this.readNumberField(row, "sample_count");
1619
+ const watermark = this.readNumberField(row, "sample_watermark_ms");
1620
+ return sampleCount === work.samples.length && watermark === this.sampleWatermark(work);
1621
+ }
1622
+ sampleWatermark(work) {
1623
+ return work.samples.length === 0 ? work.bucketStartMs : Math.max(...work.samples.map((sample) => sample.occurred_at_ms));
1624
+ }
1625
+ materializeBucket(work, materializedAtMs) {
1626
+ const normalized = this.normalizeSamplesForCardinality(work);
1627
+ const groups = this.aggregateSamples(work.definition.measurements, normalized);
1628
+ const rollupTable = this.options.tableMap.quoted("metricRollups");
1629
+ const completionTable = this.options.tableMap.quoted("rollupCompletions");
1630
+ const transaction = this.options.connection.transaction(() => {
1631
+ this.options.connection.prepare(
1632
+ `DELETE FROM ${rollupTable}
1633
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
1634
+ AND resolution_ms = ? AND bucket_start_ms = ?`
1635
+ ).run(
1636
+ this.options.targetId,
1637
+ work.definition.name,
1638
+ work.definition.version,
1639
+ work.resolutionMs,
1640
+ work.bucketStartMs
1641
+ );
1642
+ const insert = this.options.connection.prepare(
1643
+ `INSERT INTO ${rollupTable} (
1644
+ target_id, metric_name, definition_version, resolution_ms,
1645
+ bucket_start_ms, dimension_key, dimensions_json, values_json,
1646
+ materialized_at_ms
1647
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1648
+ );
1649
+ for (const [dimensionKey, group] of groups) {
1650
+ insert.run(
1651
+ this.options.targetId,
1652
+ work.definition.name,
1653
+ work.definition.version,
1654
+ work.resolutionMs,
1655
+ work.bucketStartMs,
1656
+ dimensionKey,
1657
+ group.dimensionsJson,
1658
+ canonicalJson(group.values),
1659
+ materializedAtMs
1660
+ );
1661
+ }
1662
+ this.options.connection.prepare(
1663
+ `INSERT INTO ${completionTable} (
1664
+ target_id, metric_name, definition_version, resolution_ms,
1665
+ bucket_start_ms, sample_count, sample_watermark_ms, completed_at_ms,
1666
+ state, detail
1667
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'completed', NULL)
1668
+ ON CONFLICT(
1669
+ target_id, metric_name, definition_version, resolution_ms, bucket_start_ms
1670
+ ) DO UPDATE SET
1671
+ sample_count = excluded.sample_count,
1672
+ sample_watermark_ms = excluded.sample_watermark_ms,
1673
+ completed_at_ms = excluded.completed_at_ms,
1674
+ state = 'completed',
1675
+ detail = NULL`
1676
+ ).run(
1677
+ this.options.targetId,
1678
+ work.definition.name,
1679
+ work.definition.version,
1680
+ work.resolutionMs,
1681
+ work.bucketStartMs,
1682
+ work.samples.length,
1683
+ this.sampleWatermark(work),
1684
+ materializedAtMs
1685
+ );
1686
+ });
1687
+ transaction();
1688
+ return groups.size;
1689
+ }
1690
+ recordRejectedBucket(work, recordedAtMs, detail) {
1691
+ const rollupTable = this.options.tableMap.quoted("metricRollups");
1692
+ const completionTable = this.options.tableMap.quoted("rollupCompletions");
1693
+ const transaction = this.options.connection.transaction(() => {
1694
+ this.options.connection.prepare(
1695
+ `DELETE FROM ${rollupTable}
1696
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
1697
+ AND resolution_ms = ? AND bucket_start_ms = ?`
1698
+ ).run(
1699
+ this.options.targetId,
1700
+ work.definition.name,
1701
+ work.definition.version,
1702
+ work.resolutionMs,
1703
+ work.bucketStartMs
1704
+ );
1705
+ this.options.connection.prepare(
1706
+ `INSERT INTO ${completionTable} (
1707
+ target_id, metric_name, definition_version, resolution_ms,
1708
+ bucket_start_ms, sample_count, sample_watermark_ms, completed_at_ms,
1709
+ state, detail
1710
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'rejected', ?)
1711
+ ON CONFLICT(
1712
+ target_id, metric_name, definition_version, resolution_ms, bucket_start_ms
1713
+ ) DO UPDATE SET
1714
+ sample_count = excluded.sample_count,
1715
+ sample_watermark_ms = excluded.sample_watermark_ms,
1716
+ completed_at_ms = excluded.completed_at_ms,
1717
+ state = 'rejected',
1718
+ detail = excluded.detail`
1719
+ ).run(
1720
+ this.options.targetId,
1721
+ work.definition.name,
1722
+ work.definition.version,
1723
+ work.resolutionMs,
1724
+ work.bucketStartMs,
1725
+ work.samples.length,
1726
+ this.sampleWatermark(work),
1727
+ recordedAtMs,
1728
+ detail.slice(0, 1024)
1729
+ );
1730
+ });
1731
+ transaction();
1732
+ }
1733
+ rollupStageDetail(rejectedBucketCount, examples, workBudgetReached) {
1734
+ const rejected = rejectedBucketCount === 0 ? null : `cardinality-rejected-buckets=${rejectedBucketCount}; ${examples.join("; ")}`;
1735
+ const detail = workBudgetReached ? ["work-budget-reached", rejected].filter((value) => value !== null).join("; ") : rejected;
1736
+ return detail?.slice(0, 2048) ?? null;
1737
+ }
1738
+ normalizeSamplesForCardinality(work) {
1739
+ const admittedByField = /* @__PURE__ */ new Map();
1740
+ const normalized = [];
1741
+ for (const sample of work.samples) {
1742
+ const dimensions = this.rowParser.parseJsonObject(sample.dimensions_json);
1743
+ const values = this.rowParser.parseNumericValues(sample.values_json);
1744
+ if (!dimensions.ok) {
1745
+ throw new Error(dimensions.error);
1746
+ }
1747
+ if (!values.ok) {
1748
+ throw new Error(values.error);
1749
+ }
1750
+ const admittedDimensions = { ...dimensions.value };
1751
+ let dropSample = false;
1752
+ for (const field of work.definition.groupBy) {
1753
+ const rule = work.definition.cardinality[field];
1754
+ if (!rule) {
1755
+ throw new Error(`Missing cardinality rule for ${field}`);
1756
+ }
1757
+ const dimensionValue = admittedDimensions[field];
1758
+ const valueKey = canonicalJson(dimensionValue);
1759
+ const admittedValues = admittedByField.get(field) ?? /* @__PURE__ */ new Set();
1760
+ if (!admittedValues.has(valueKey) && admittedValues.size >= rule.maxValuesPerBucket) {
1761
+ if (rule.overflow === "other") {
1762
+ admittedDimensions[field] = "__telemetry_other__";
1763
+ } else if (rule.overflow === "reject") {
1764
+ throw new MetricCardinalityRejectionError(
1765
+ `Metric ${work.definition.name} rejected cardinality overflow for ${field}`
1766
+ );
1767
+ } else {
1768
+ dropSample = true;
1769
+ break;
1770
+ }
1771
+ } else {
1772
+ admittedValues.add(valueKey);
1773
+ admittedByField.set(field, admittedValues);
1774
+ }
1775
+ }
1776
+ if (dropSample) {
1777
+ continue;
1778
+ }
1779
+ const dimensionsJson = canonicalJson(admittedDimensions);
1780
+ normalized.push({
1781
+ dimensionKey: dimensionsJson,
1782
+ dimensionsJson,
1783
+ values: values.value,
1784
+ occurredAtMs: sample.occurred_at_ms,
1785
+ producerId: sample.producer_id,
1786
+ sequence: sample.sequence
1787
+ });
1788
+ }
1789
+ return normalized;
1790
+ }
1791
+ aggregateSamples(measurements, samples) {
1792
+ const groups = /* @__PURE__ */ new Map();
1793
+ const fields = measurements.filter((measurement) => {
1794
+ return measurement.kind === "field";
1795
+ }).map((measurement) => measurement.field);
1796
+ for (const sample of samples) {
1797
+ const group = groups.get(sample.dimensionKey) ?? {
1798
+ dimensionsJson: sample.dimensionsJson,
1799
+ values: { count: 0, fields: {} }
1800
+ };
1801
+ group.values.count += 1;
1802
+ for (const field of fields) {
1803
+ const measured = sample.values[field];
1804
+ if (measured === void 0) {
1805
+ continue;
1806
+ }
1807
+ const aggregate = group.values.fields[field] ?? this.newFieldAggregate(measured, sample);
1808
+ aggregate.count += group.values.fields[field] === void 0 ? 0 : 1;
1809
+ aggregate.sum += group.values.fields[field] === void 0 ? 0 : measured;
1810
+ aggregate.min = Math.min(aggregate.min, measured);
1811
+ aggregate.max = Math.max(aggregate.max, measured);
1812
+ aggregate.last = measured;
1813
+ aggregate.lastOccurredAtMs = sample.occurredAtMs;
1814
+ aggregate.lastProducerId = sample.producerId;
1815
+ aggregate.lastSequence = sample.sequence;
1816
+ group.values.fields[field] = aggregate;
1817
+ }
1818
+ groups.set(sample.dimensionKey, group);
1819
+ }
1820
+ return groups;
1821
+ }
1822
+ newFieldAggregate(measured, sample) {
1823
+ return {
1824
+ count: 1,
1825
+ sum: measured,
1826
+ min: measured,
1827
+ max: measured,
1828
+ last: measured,
1829
+ lastOccurredAtMs: sample.occurredAtMs,
1830
+ lastProducerId: sample.producerId,
1831
+ lastSequence: sample.sequence
1832
+ };
1833
+ }
1834
+ createRawEventRetentionStep(nowMs) {
1835
+ const cutoffMs = nowMs - this.options.retention.rawMs;
1836
+ const table = this.options.tableMap.quoted("rawEvents");
1837
+ const readFirstPage = this.options.connection.prepare(
1838
+ `SELECT producer_id, sequence, event_id, occurred_at_ms, monotonic_ns,
1839
+ source, process_kind, level, label, conversation_id,
1840
+ conversation_id_kind, correlation_id, metadata_json, schema_version
1841
+ FROM ${table}
1842
+ WHERE target_id = ? AND occurred_at_ms < ?
1843
+ ORDER BY occurred_at_ms ASC, producer_id ASC, sequence ASC
1844
+ LIMIT ${RETENTION_PAGE_SIZE}`
1845
+ );
1846
+ const readNextPage = this.options.connection.prepare(
1847
+ `SELECT producer_id, sequence, event_id, occurred_at_ms, monotonic_ns,
1848
+ source, process_kind, level, label, conversation_id,
1849
+ conversation_id_kind, correlation_id, metadata_json, schema_version
1850
+ FROM ${table}
1851
+ WHERE target_id = ? AND occurred_at_ms < ?
1852
+ AND (occurred_at_ms, producer_id, sequence) > (?, ?, ?)
1853
+ ORDER BY occurred_at_ms ASC, producer_id ASC, sequence ASC
1854
+ LIMIT ${RETENTION_PAGE_SIZE}`
1855
+ );
1856
+ const remove = this.options.connection.prepare(
1857
+ `DELETE FROM ${table} WHERE target_id = ? AND producer_id = ? AND sequence = ?`
1858
+ );
1859
+ let cursor = null;
1860
+ return () => {
1861
+ const rows = cursor === null ? readFirstPage.all(this.options.targetId, cutoffMs) : readNextPage.all(
1862
+ this.options.targetId,
1863
+ cutoffMs,
1864
+ cursor.occurredAtMs,
1865
+ cursor.producerId,
1866
+ cursor.sequence
1867
+ );
1868
+ if (rows.length === 0) {
1869
+ return { deleted: 0, done: true };
1870
+ }
1871
+ const scanned = rows.map((row) => {
1872
+ const parsed = this.rowParser.parseRawEnvelope(row);
1873
+ if (!parsed.ok) {
1874
+ throw new Error(parsed.error);
1875
+ }
1876
+ return parsed.value;
1877
+ });
1878
+ const eligible = scanned.filter((envelope) => this.rawEventHasRequiredRollups(envelope));
1879
+ const transaction = this.options.connection.transaction(() => {
1880
+ let deleted2 = 0;
1881
+ for (const envelope of eligible) {
1882
+ deleted2 += remove.run(
1883
+ this.options.targetId,
1884
+ envelope.producerId,
1885
+ envelope.sequence
1886
+ ).changes;
1887
+ }
1888
+ return deleted2;
1889
+ });
1890
+ const deleted = transaction();
1891
+ const last = scanned[scanned.length - 1];
1892
+ if (last === void 0 || rows.length < RETENTION_PAGE_SIZE) {
1893
+ return { deleted, done: true };
1894
+ }
1895
+ cursor = {
1896
+ occurredAtMs: last.occurredAtMs,
1897
+ producerId: last.producerId,
1898
+ sequence: last.sequence
1899
+ };
1900
+ return { deleted, done: false };
1901
+ };
1902
+ }
1903
+ rawEventHasRequiredRollups(envelope) {
1904
+ const applicable = [...this.definitions.values()].filter((definition) => {
1905
+ return this.definitionContract.matches(definition.selector, envelope.label);
1906
+ });
1907
+ return applicable.every((definition) => {
1908
+ if (!this.metricSampleExists(definition, envelope)) {
1909
+ return true;
1910
+ }
1911
+ return definition.resolutionsMs.every((resolutionMs) => {
1912
+ return this.hasCompletion(
1913
+ definition.name,
1914
+ definition.version,
1915
+ resolutionMs,
1916
+ this.bucketStart(envelope.occurredAtMs, resolutionMs)
1917
+ );
1918
+ });
1919
+ });
1920
+ }
1921
+ metricSampleExists(definition, envelope) {
1922
+ const table = this.options.tableMap.quoted("metricSamples");
1923
+ const row = this.options.connection.prepare(
1924
+ `SELECT 1 AS present
1925
+ FROM ${table}
1926
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
1927
+ AND producer_id = ? AND sequence = ?`
1928
+ ).get(
1929
+ this.options.targetId,
1930
+ definition.name,
1931
+ definition.version,
1932
+ envelope.producerId,
1933
+ envelope.sequence
1934
+ );
1935
+ return row !== void 0;
1936
+ }
1937
+ createSampleRetentionStep(nowMs) {
1938
+ const cutoffMs = nowMs - this.options.retention.samplesMs;
1939
+ const table = this.options.tableMap.quoted("metricSamples");
1940
+ const readFirstPage = this.options.connection.prepare(
1941
+ `SELECT metric_name, definition_version, occurred_at_ms, producer_id,
1942
+ sequence, dimension_key, dimensions_json, values_json
1943
+ FROM ${table}
1944
+ WHERE target_id = ? AND occurred_at_ms < ?
1945
+ ORDER BY occurred_at_ms ASC, metric_name ASC, definition_version ASC,
1946
+ producer_id ASC, sequence ASC
1947
+ LIMIT ${RETENTION_PAGE_SIZE}`
1948
+ );
1949
+ const readNextPage = this.options.connection.prepare(
1950
+ `SELECT metric_name, definition_version, occurred_at_ms, producer_id,
1951
+ sequence, dimension_key, dimensions_json, values_json
1952
+ FROM ${table}
1953
+ WHERE target_id = ? AND occurred_at_ms < ?
1954
+ AND (
1955
+ occurred_at_ms, metric_name, definition_version, producer_id, sequence
1956
+ ) > (?, ?, ?, ?, ?)
1957
+ ORDER BY occurred_at_ms ASC, metric_name ASC, definition_version ASC,
1958
+ producer_id ASC, sequence ASC
1959
+ LIMIT ${RETENTION_PAGE_SIZE}`
1960
+ );
1961
+ const remove = this.options.connection.prepare(
1962
+ `DELETE FROM ${table}
1963
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
1964
+ AND producer_id = ? AND sequence = ?`
1965
+ );
1966
+ let cursor = null;
1967
+ return () => {
1968
+ const rows = cursor === null ? readFirstPage.all(this.options.targetId, cutoffMs) : readNextPage.all(
1969
+ this.options.targetId,
1970
+ cutoffMs,
1971
+ cursor.occurredAtMs,
1972
+ cursor.metricName,
1973
+ cursor.definitionVersion,
1974
+ cursor.producerId,
1975
+ cursor.sequence
1976
+ );
1977
+ if (rows.length === 0) {
1978
+ return { deleted: 0, done: true };
1979
+ }
1980
+ const samples = rows.map((row) => {
1981
+ const parsed = this.rowParser.parseMetricSample(row);
1982
+ if (!parsed.ok) {
1983
+ throw new Error(parsed.error);
1984
+ }
1985
+ return parsed.value;
1986
+ });
1987
+ const transaction = this.options.connection.transaction(() => {
1988
+ let deleted2 = 0;
1989
+ for (const sample of samples) {
1990
+ const definition = this.definitions.get(
1991
+ this.definitionKey(sample.metric_name, sample.definition_version)
1992
+ );
1993
+ if (!definition) {
1994
+ continue;
1995
+ }
1996
+ const complete = definition.resolutionsMs.every((resolutionMs) => {
1997
+ return this.hasCompletion(
1998
+ definition.name,
1999
+ definition.version,
2000
+ resolutionMs,
2001
+ this.bucketStart(sample.occurred_at_ms, resolutionMs)
2002
+ );
2003
+ });
2004
+ if (complete) {
2005
+ deleted2 += remove.run(
2006
+ this.options.targetId,
2007
+ sample.metric_name,
2008
+ sample.definition_version,
2009
+ sample.producer_id,
2010
+ sample.sequence
2011
+ ).changes;
2012
+ }
2013
+ }
2014
+ return deleted2;
2015
+ });
2016
+ const deleted = transaction();
2017
+ const last = samples[samples.length - 1];
2018
+ if (last === void 0 || rows.length < RETENTION_PAGE_SIZE) {
2019
+ return { deleted, done: true };
2020
+ }
2021
+ cursor = {
2022
+ occurredAtMs: last.occurred_at_ms,
2023
+ metricName: last.metric_name,
2024
+ definitionVersion: last.definition_version,
2025
+ producerId: last.producer_id,
2026
+ sequence: last.sequence
2027
+ };
2028
+ return { deleted, done: false };
2029
+ };
2030
+ }
2031
+ createRollupRetentionSteps(nowMs) {
2032
+ const rollupTable = this.options.tableMap.quoted("metricRollups");
2033
+ const completionTable = this.options.tableMap.quoted("rollupCompletions");
2034
+ const steps = [];
2035
+ for (const tier of this.options.retention.rollups) {
2036
+ const cutoffMs = nowMs - tier.retainMs;
2037
+ steps.push(this.createExpiredRollupStep(rollupTable, tier.resolutionMs, cutoffMs, false));
2038
+ steps.push(this.createExpiredRollupStep(completionTable, tier.resolutionMs, cutoffMs, true));
2039
+ }
2040
+ return steps;
2041
+ }
2042
+ createExpiredRollupStep(table, resolutionMs, cutoffMs, completedOnly) {
2043
+ const statePredicate = completedOnly ? " AND state = 'completed'" : "";
2044
+ const remove = this.options.connection.prepare(
2045
+ `DELETE FROM ${table}
2046
+ WHERE rowid IN (
2047
+ SELECT rowid FROM ${table}
2048
+ WHERE target_id = ? AND resolution_ms = ? AND bucket_start_ms < ?${statePredicate}
2049
+ ORDER BY bucket_start_ms ASC
2050
+ LIMIT 500
2051
+ )`
2052
+ );
2053
+ return this.createPagedDeleteStep(
2054
+ () => remove.run(this.options.targetId, resolutionMs, cutoffMs).changes
2055
+ );
2056
+ }
2057
+ createPagedDeleteStep(deletePage) {
2058
+ return () => {
2059
+ const changes = deletePage();
2060
+ return { deleted: changes, done: changes < RETENTION_PAGE_SIZE };
2061
+ };
2062
+ }
2063
+ createAuxiliaryRetentionSteps(nowMs) {
2064
+ const acknowledgementCutoffMs = nowMs - this.acknowledgementRetentionMs();
2065
+ const ingestionCutoffMs = nowMs - this.longestRetentionMs();
2066
+ const extractionCutoffMs = nowMs - this.options.retention.samplesMs;
2067
+ return [
2068
+ this.createRowsBeforeStep("batchAcknowledgements", "committed_at_ms", acknowledgementCutoffMs),
2069
+ this.createRowsBeforeStep("extractionFailures", "recorded_at_ms", extractionCutoffMs),
2070
+ this.createFinishedMaintenanceRunStep(acknowledgementCutoffMs),
2071
+ ...this.createReaderLeaseSteps(nowMs),
2072
+ this.createStaleIngestWatermarkStep(ingestionCutoffMs),
2073
+ this.createUnreferencedMetricDefinitionStep()
2074
+ ];
2075
+ }
2076
+ createRowsBeforeStep(logicalTable, timestampColumn, cutoffMs) {
2077
+ const table = this.options.tableMap.quoted(logicalTable);
2078
+ const remove = this.options.connection.prepare(
2079
+ `DELETE FROM ${table}
2080
+ WHERE rowid IN (
2081
+ SELECT rowid FROM ${table}
2082
+ WHERE target_id = ? AND ${timestampColumn} < ?
2083
+ ORDER BY ${timestampColumn} ASC
2084
+ LIMIT 500
2085
+ )`
2086
+ );
2087
+ return this.createPagedDeleteStep(
2088
+ () => remove.run(this.options.targetId, cutoffMs).changes
2089
+ );
2090
+ }
2091
+ createFinishedMaintenanceRunStep(cutoffMs) {
2092
+ const table = this.options.tableMap.quoted("maintenanceRuns");
2093
+ const remove = this.options.connection.prepare(
2094
+ `DELETE FROM ${table}
2095
+ WHERE rowid IN (
2096
+ SELECT rowid FROM ${table}
2097
+ WHERE target_id = ? AND state IN ('completed', 'failed')
2098
+ AND COALESCE(completed_at_ms, started_at_ms) < ?
2099
+ ORDER BY COALESCE(completed_at_ms, started_at_ms) ASC
2100
+ LIMIT 500
2101
+ )`
2102
+ );
2103
+ return this.createPagedDeleteStep(
2104
+ () => remove.run(this.options.targetId, cutoffMs).changes
2105
+ );
2106
+ }
2107
+ createReaderLeaseSteps(nowMs) {
2108
+ const table = this.options.tableMap.quoted("readerLeases");
2109
+ const expire = this.options.connection.prepare(
2110
+ `UPDATE ${table}
2111
+ SET state = 'expired'
2112
+ WHERE rowid IN (
2113
+ SELECT rowid FROM ${table}
2114
+ WHERE target_id = ? AND state = 'active' AND expires_at_ms <= ?
2115
+ ORDER BY expires_at_ms ASC
2116
+ LIMIT 500
2117
+ )`
2118
+ );
2119
+ const remove = this.options.connection.prepare(
2120
+ `DELETE FROM ${table}
2121
+ WHERE rowid IN (
2122
+ SELECT rowid FROM ${table}
2123
+ WHERE target_id = ? AND state IN ('released', 'expired')
2124
+ AND expires_at_ms <= ?
2125
+ ORDER BY expires_at_ms ASC
2126
+ LIMIT 500
2127
+ )`
2128
+ );
2129
+ return [
2130
+ this.createPagedDeleteStep(() => expire.run(this.options.targetId, nowMs).changes),
2131
+ this.createPagedDeleteStep(() => remove.run(this.options.targetId, nowMs).changes)
2132
+ ];
2133
+ }
2134
+ createStaleIngestWatermarkStep(cutoffMs) {
2135
+ const watermarks = this.options.tableMap.quoted("ingestWatermarks");
2136
+ const acknowledgements = this.options.tableMap.quoted("batchAcknowledgements");
2137
+ const rawEvents = this.options.tableMap.quoted("rawEvents");
2138
+ const samples = this.options.tableMap.quoted("metricSamples");
2139
+ const failures = this.options.tableMap.quoted("extractionFailures");
2140
+ const remove = this.options.connection.prepare(
2141
+ `DELETE FROM ${watermarks}
2142
+ WHERE rowid IN (
2143
+ SELECT watermark.rowid
2144
+ FROM ${watermarks} AS watermark
2145
+ WHERE watermark.target_id = ?
2146
+ AND watermark.last_occurred_at_ms < ?
2147
+ AND watermark.last_committed_at_ms < ?
2148
+ AND NOT EXISTS (
2149
+ SELECT 1 FROM ${acknowledgements} AS acknowledgement
2150
+ WHERE acknowledgement.target_id = watermark.target_id
2151
+ AND acknowledgement.producer_id = watermark.producer_id
2152
+ )
2153
+ AND NOT EXISTS (
2154
+ SELECT 1 FROM ${rawEvents} AS raw_event
2155
+ WHERE raw_event.target_id = watermark.target_id
2156
+ AND raw_event.producer_id = watermark.producer_id
2157
+ )
2158
+ AND NOT EXISTS (
2159
+ SELECT 1 FROM ${samples} AS sample
2160
+ WHERE sample.target_id = watermark.target_id
2161
+ AND sample.producer_id = watermark.producer_id
2162
+ )
2163
+ AND NOT EXISTS (
2164
+ SELECT 1 FROM ${failures} AS failure
2165
+ WHERE failure.target_id = watermark.target_id
2166
+ AND failure.producer_id = watermark.producer_id
2167
+ )
2168
+ ORDER BY watermark.last_committed_at_ms ASC
2169
+ LIMIT 500
2170
+ )`
2171
+ );
2172
+ return this.createPagedDeleteStep(
2173
+ () => remove.run(this.options.targetId, cutoffMs, cutoffMs).changes
2174
+ );
2175
+ }
2176
+ createUnreferencedMetricDefinitionStep() {
2177
+ const definitions = this.options.tableMap.quoted("metricDefinitions");
2178
+ const samples = this.options.tableMap.quoted("metricSamples");
2179
+ const rollups = this.options.tableMap.quoted("metricRollups");
2180
+ const completions = this.options.tableMap.quoted("rollupCompletions");
2181
+ const failures = this.options.tableMap.quoted("extractionFailures");
2182
+ const read = this.options.connection.prepare(
2183
+ `SELECT definition.metric_name, definition.definition_version
2184
+ FROM ${definitions} AS definition
2185
+ WHERE definition.target_id = ? AND definition.active = 0
2186
+ AND NOT EXISTS (
2187
+ SELECT 1 FROM ${samples} AS sample
2188
+ WHERE sample.target_id = definition.target_id
2189
+ AND sample.metric_name = definition.metric_name
2190
+ AND sample.definition_version = definition.definition_version
2191
+ )
2192
+ AND NOT EXISTS (
2193
+ SELECT 1 FROM ${rollups} AS rollup
2194
+ WHERE rollup.target_id = definition.target_id
2195
+ AND rollup.metric_name = definition.metric_name
2196
+ AND rollup.definition_version = definition.definition_version
2197
+ )
2198
+ AND NOT EXISTS (
2199
+ SELECT 1 FROM ${completions} AS completion
2200
+ WHERE completion.target_id = definition.target_id
2201
+ AND completion.metric_name = definition.metric_name
2202
+ AND completion.definition_version = definition.definition_version
2203
+ )
2204
+ AND NOT EXISTS (
2205
+ SELECT 1 FROM ${failures} AS failure
2206
+ WHERE failure.target_id = definition.target_id
2207
+ AND failure.metric_name = definition.metric_name
2208
+ AND failure.definition_version = definition.definition_version
2209
+ )
2210
+ ORDER BY definition.registered_at_ms ASC
2211
+ LIMIT 500`
2212
+ );
2213
+ const remove = this.options.connection.prepare(
2214
+ `DELETE FROM ${definitions}
2215
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ? AND active = 0`
2216
+ );
2217
+ return () => {
2218
+ const rows = read.all(this.options.targetId).map((row) => ({
2219
+ metricName: this.readStringField(row, "metric_name"),
2220
+ definitionVersion: this.readNumberField(row, "definition_version")
2221
+ }));
2222
+ if (rows.length === 0) {
2223
+ return { deleted: 0, done: true };
2224
+ }
2225
+ const transaction = this.options.connection.transaction(() => {
2226
+ let deleted2 = 0;
2227
+ for (const row of rows) {
2228
+ deleted2 += remove.run(
2229
+ this.options.targetId,
2230
+ row.metricName,
2231
+ row.definitionVersion
2232
+ ).changes;
2233
+ }
2234
+ return deleted2;
2235
+ });
2236
+ const deleted = transaction();
2237
+ for (const row of rows) {
2238
+ this.definitions.delete(this.definitionKey(row.metricName, row.definitionVersion));
2239
+ }
2240
+ return { deleted, done: deleted === 0 || rows.length < RETENTION_PAGE_SIZE };
2241
+ };
2242
+ }
2243
+ acknowledgementRetentionMs() {
2244
+ return this.options.retention.rawMs + this.options.retention.lateArrivalMs;
2245
+ }
2246
+ longestRetentionMs() {
2247
+ return Math.max(
2248
+ this.options.retention.rawMs,
2249
+ this.options.retention.samplesMs,
2250
+ ...this.options.retention.rollups.map((tier) => tier.retainMs)
2251
+ );
2252
+ }
2253
+ hasCompletion(metricName, definitionVersion, resolutionMs, bucketStartMs) {
2254
+ const table = this.options.tableMap.quoted("rollupCompletions");
2255
+ const row = this.options.connection.prepare(
2256
+ `SELECT 1 AS present FROM ${table}
2257
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
2258
+ AND resolution_ms = ? AND bucket_start_ms = ?
2259
+ AND state = 'completed'`
2260
+ ).get(this.options.targetId, metricName, definitionVersion, resolutionMs, bucketStartMs);
2261
+ return row !== void 0;
2262
+ }
2263
+ metricDefinitions(metricName) {
2264
+ return [...this.definitions.values()].filter((definition) => definition.name === metricName).sort((left, right) => left.version - right.version);
2265
+ }
2266
+ validateMetricQuery(query, definitions) {
2267
+ const dimensionSets = definitions.map((definition) => new Set(definition.groupBy));
2268
+ const invalidDimension = Object.keys(query.where).find((field) => {
2269
+ return dimensionSets.some((dimensions) => !dimensions.has(field));
2270
+ });
2271
+ if (invalidDimension) {
2272
+ return `Metric ${query.metric} does not declare dimension ${invalidDimension}`;
2273
+ }
2274
+ if (query.resolution !== "auto") {
2275
+ const explicitResolution = query.resolution;
2276
+ const unavailable = definitions.every(
2277
+ (definition) => !definition.resolutionsMs.includes(explicitResolution)
2278
+ );
2279
+ if (unavailable) {
2280
+ return `Resolution ${query.resolution} is not configured for metric ${query.metric}`;
2281
+ }
2282
+ }
2283
+ const invalidMeasurement = query.measurements.find((measurement) => {
2284
+ return definitions.some(
2285
+ (definition) => !this.definitionHasMeasurement(definition, measurement)
2286
+ );
2287
+ });
2288
+ return invalidMeasurement ? `Metric ${query.metric} does not declare measurement ${invalidMeasurement}` : null;
2289
+ }
2290
+ definitionHasMeasurement(definition, requested) {
2291
+ if (requested === "count") {
2292
+ return definition.measurements.some((measurement) => measurement.kind === "count");
2293
+ }
2294
+ const [field, aggregate] = requested.split(".");
2295
+ if (!field || !aggregate) {
2296
+ return false;
2297
+ }
2298
+ return definition.measurements.some((measurement) => {
2299
+ return measurement.kind === "field" && measurement.field === field && (aggregate === "avg" && measurement.aggregates.includes("sum") || this.isMetricAggregate(aggregate) && measurement.aggregates.includes(aggregate));
2300
+ });
2301
+ }
2302
+ planCoverage(query, definitions) {
2303
+ const table = this.options.tableMap.quoted("rollupCompletions");
2304
+ const rows = this.options.connection.prepare(
2305
+ `SELECT definition_version, resolution_ms, bucket_start_ms
2306
+ FROM ${table}
2307
+ WHERE target_id = ? AND metric_name = ?
2308
+ AND state = 'completed'
2309
+ AND bucket_start_ms < ?
2310
+ AND bucket_start_ms + resolution_ms > ?
2311
+ ORDER BY bucket_start_ms ASC, resolution_ms ASC, definition_version DESC`
2312
+ ).all(this.options.targetId, query.metric, query.toMs, query.fromMs);
2313
+ const definitionVersions = new Set(definitions.map((definition) => definition.version));
2314
+ const available = rows.map((row) => ({
2315
+ fromMs: this.readNumberField(row, "bucket_start_ms"),
2316
+ toMs: this.readNumberField(row, "bucket_start_ms") + this.readNumberField(row, "resolution_ms"),
2317
+ definitionVersion: this.readNumberField(row, "definition_version"),
2318
+ resolutionMs: this.readNumberField(row, "resolution_ms")
2319
+ })).filter((row) => {
2320
+ return definitionVersions.has(row.definitionVersion) && (query.resolution === "auto" || row.resolutionMs === query.resolution);
2321
+ });
2322
+ const intervals = [];
2323
+ for (const definition of definitions) {
2324
+ const versionCandidates = available.filter(
2325
+ (candidate) => candidate.definitionVersion === definition.version
2326
+ );
2327
+ intervals.push(...this.planDefinitionCoverage(query, versionCandidates));
2328
+ }
2329
+ const orderedIntervals = intervals.sort((left, right) => {
2330
+ return left.fromMs - right.fromMs || left.definitionVersion - right.definitionVersion || left.resolutionMs - right.resolutionMs;
2331
+ });
2332
+ return {
2333
+ intervals: orderedIntervals,
2334
+ gaps: this.coverageGaps(query, orderedIntervals, available)
2335
+ };
2336
+ }
2337
+ planDefinitionCoverage(query, candidates) {
2338
+ const contained = candidates.filter((candidate) => {
2339
+ return candidate.fromMs >= query.fromMs && candidate.toMs <= query.toMs;
2340
+ });
2341
+ const intervals = [];
2342
+ let cursor = query.fromMs;
2343
+ while (cursor < query.toMs) {
2344
+ const starting = contained.filter((candidate) => candidate.fromMs === cursor).sort((left, right) => left.resolutionMs - right.resolutionMs);
2345
+ const chosen = starting[0];
2346
+ if (chosen) {
2347
+ this.appendCoverageInterval(intervals, chosen);
2348
+ cursor = chosen.toMs;
2349
+ continue;
2350
+ }
2351
+ const nextStart = contained.map((candidate) => candidate.fromMs).filter((startMs) => startMs > cursor).sort((left, right) => left - right)[0];
2352
+ cursor = nextStart ?? query.toMs;
2353
+ }
2354
+ return intervals;
2355
+ }
2356
+ appendCoverageInterval(intervals, candidate) {
2357
+ const previous = intervals[intervals.length - 1];
2358
+ const extendsPrevious = previous !== void 0 && previous.toMs === candidate.fromMs && previous.resolutionMs === candidate.resolutionMs && previous.definitionVersion === candidate.definitionVersion;
2359
+ if (extendsPrevious) {
2360
+ intervals[intervals.length - 1] = { ...previous, toMs: candidate.toMs };
2361
+ return;
2362
+ }
2363
+ intervals.push(candidate);
2364
+ }
2365
+ coverageGaps(query, intervals, candidates) {
2366
+ const coverage = [...intervals].sort(
2367
+ (left, right) => left.fromMs - right.fromMs || right.toMs - left.toMs
2368
+ );
2369
+ const gaps = [];
2370
+ let cursor = query.fromMs;
2371
+ for (const interval of coverage) {
2372
+ if (interval.fromMs > cursor) {
2373
+ this.appendSplitCoverageGaps(gaps, cursor, interval.fromMs, query, candidates);
2374
+ }
2375
+ cursor = Math.max(cursor, interval.toMs);
2376
+ }
2377
+ if (cursor < query.toMs) {
2378
+ this.appendSplitCoverageGaps(gaps, cursor, query.toMs, query, candidates);
2379
+ }
2380
+ return Object.freeze(gaps);
2381
+ }
2382
+ appendSplitCoverageGaps(gaps, fromMs, toMs, query, candidates) {
2383
+ const partialCandidates = candidates.filter((candidate) => {
2384
+ const outsideQuery = candidate.fromMs < query.fromMs || candidate.toMs > query.toMs;
2385
+ return outsideQuery && candidate.fromMs < toMs && candidate.toMs > fromMs;
2386
+ });
2387
+ const boundaries = /* @__PURE__ */ new Set([fromMs, toMs]);
2388
+ for (const candidate of partialCandidates) {
2389
+ boundaries.add(Math.max(fromMs, candidate.fromMs));
2390
+ boundaries.add(Math.min(toMs, candidate.toMs));
2391
+ }
2392
+ const ordered = [...boundaries].sort((left, right) => left - right);
2393
+ for (let index = 0; index < ordered.length - 1; index += 1) {
2394
+ const gapStart = ordered[index];
2395
+ const gapEnd = ordered[index + 1];
2396
+ if (gapStart === void 0 || gapEnd === void 0 || gapStart >= gapEnd) {
2397
+ continue;
2398
+ }
2399
+ const isPartialBucket = partialCandidates.some((candidate) => {
2400
+ return candidate.fromMs < gapEnd && candidate.toMs > gapStart;
2401
+ });
2402
+ this.appendMetricGap(gaps, {
2403
+ fromMs: gapStart,
2404
+ toMs: gapEnd,
2405
+ reason: isPartialBucket ? "partial-bucket" : "not-materialized"
2406
+ });
2407
+ }
2408
+ }
2409
+ appendMetricGap(gaps, gap) {
2410
+ const previous = gaps[gaps.length - 1];
2411
+ if (previous && previous.toMs === gap.fromMs && previous.reason === gap.reason) {
2412
+ gaps[gaps.length - 1] = { ...previous, toMs: gap.toMs };
2413
+ return;
2414
+ }
2415
+ gaps.push(gap);
2416
+ }
2417
+ readRollupsForCoverage(query, intervals) {
2418
+ const table = this.options.tableMap.quoted("metricRollups");
2419
+ const parsedRows = [];
2420
+ for (const interval of intervals) {
2421
+ const rows = this.options.connection.prepare(
2422
+ `SELECT metric_name, definition_version, resolution_ms, bucket_start_ms,
2423
+ dimension_key, dimensions_json, values_json
2424
+ FROM ${table}
2425
+ WHERE target_id = ? AND metric_name = ? AND definition_version = ?
2426
+ AND resolution_ms = ? AND bucket_start_ms >= ?
2427
+ AND bucket_start_ms + resolution_ms <= ?`
2428
+ ).all(
2429
+ this.options.targetId,
2430
+ query.metric,
2431
+ interval.definitionVersion,
2432
+ interval.resolutionMs,
2433
+ interval.fromMs,
2434
+ interval.toMs
2435
+ );
2436
+ for (const row of rows) {
2437
+ const parsed = this.rowParser.parseMetricRollup(row);
2438
+ if (!parsed.ok) {
2439
+ throw new Error(parsed.error);
2440
+ }
2441
+ const dimensions = this.rowParser.parseJsonObject(parsed.value.dimensions_json);
2442
+ const values = this.rowParser.parseRollupValues(parsed.value.values_json);
2443
+ if (!dimensions.ok) {
2444
+ throw new Error(dimensions.error);
2445
+ }
2446
+ if (!values.ok) {
2447
+ throw new Error(values.error);
2448
+ }
2449
+ parsedRows.push({
2450
+ bucket: {
2451
+ startMs: parsed.value.bucket_start_ms,
2452
+ endMs: parsed.value.bucket_start_ms + parsed.value.resolution_ms,
2453
+ dimensions: dimensions.value,
2454
+ values: this.rollupValuesToJson(values.value)
2455
+ },
2456
+ resolutionMs: parsed.value.resolution_ms,
2457
+ definitionVersion: parsed.value.definition_version,
2458
+ dimensionKey: parsed.value.dimension_key
2459
+ });
2460
+ }
2461
+ }
2462
+ return parsedRows;
2463
+ }
2464
+ rollupValuesToJson(values) {
2465
+ const fields = {};
2466
+ for (const [field, aggregate] of Object.entries(values.fields)) {
2467
+ fields[field] = {
2468
+ count: aggregate.count,
2469
+ sum: aggregate.sum,
2470
+ min: aggregate.min,
2471
+ max: aggregate.max,
2472
+ last: aggregate.last,
2473
+ avg: aggregate.count === 0 ? 0 : aggregate.sum / aggregate.count
2474
+ };
2475
+ }
2476
+ return Object.freeze({ count: values.count, ...fields });
2477
+ }
2478
+ selectMeasurements(values, measurements) {
2479
+ const selected = {};
2480
+ for (const measurement of measurements) {
2481
+ if (measurement === "count") {
2482
+ selected.count = values.count ?? 0;
2483
+ continue;
2484
+ }
2485
+ const [field, aggregate] = measurement.split(".");
2486
+ const fieldValues = field ? values[field] : null;
2487
+ if (!field || !aggregate) {
2488
+ continue;
2489
+ }
2490
+ if (!this.isJsonObject(fieldValues)) {
2491
+ continue;
2492
+ }
2493
+ const existing = selected[field];
2494
+ const selectedField = this.isJsonObject(existing) ? { ...existing } : {};
2495
+ const aggregateValue = fieldValues[aggregate];
2496
+ if (aggregateValue !== void 0) {
2497
+ selectedField[aggregate] = aggregateValue;
2498
+ }
2499
+ selected[field] = selectedField;
2500
+ }
2501
+ return Object.freeze(selected);
2502
+ }
2503
+ matchesWhere(dimensions, where) {
2504
+ return Object.entries(where).every(([field, expected]) => {
2505
+ const actual = dimensions[field];
2506
+ const candidates = Array.isArray(expected) ? expected : [expected];
2507
+ return candidates.some((candidate) => canonicalJson(candidate) === canonicalJson(actual));
2508
+ });
2509
+ }
2510
+ compareParsedRollups(left, right, order) {
2511
+ const direction = order === "asc" ? 1 : -1;
2512
+ return direction * this.compareRollupIdentity(
2513
+ left.bucket.startMs,
2514
+ left.resolutionMs,
2515
+ left.definitionVersion,
2516
+ left.dimensionKey,
2517
+ right.bucket.startMs,
2518
+ right.resolutionMs,
2519
+ right.definitionVersion,
2520
+ right.dimensionKey
2521
+ );
2522
+ }
2523
+ isAfterMetricCursor(row, cursor, order) {
2524
+ const comparison = this.compareRollupIdentity(
2525
+ row.bucket.startMs,
2526
+ row.resolutionMs,
2527
+ row.definitionVersion,
2528
+ row.dimensionKey,
2529
+ cursor.bucketStartMs,
2530
+ cursor.resolutionMs,
2531
+ cursor.definitionVersion,
2532
+ cursor.dimensionKey
2533
+ );
2534
+ return order === "asc" ? comparison > 0 : comparison < 0;
2535
+ }
2536
+ compareRollupIdentity(leftStartMs, leftResolutionMs, leftDefinitionVersion, leftDimensionKey, rightStartMs, rightResolutionMs, rightDefinitionVersion, rightDimensionKey) {
2537
+ return leftStartMs - rightStartMs || leftResolutionMs - rightResolutionMs || leftDefinitionVersion - rightDefinitionVersion || leftDimensionKey.localeCompare(rightDimensionKey);
2538
+ }
2539
+ rowsToSegments(intervals, rows) {
2540
+ return Object.freeze(
2541
+ intervals.map((interval) => ({
2542
+ fromMs: interval.fromMs,
2543
+ toMs: interval.toMs,
2544
+ resolutionMs: interval.resolutionMs,
2545
+ isAggregated: true,
2546
+ definitionVersion: interval.definitionVersion,
2547
+ buckets: Object.freeze(
2548
+ rows.filter(
2549
+ (row) => row.resolutionMs === interval.resolutionMs && row.definitionVersion === interval.definitionVersion && row.bucket.startMs >= interval.fromMs && row.bucket.endMs <= interval.toMs
2550
+ ).map((row) => row.bucket)
2551
+ )
2552
+ }))
2553
+ );
2554
+ }
2555
+ committedTargetState(attempts) {
2556
+ return {
2557
+ targetId: this.options.targetId,
2558
+ delivery: "required",
2559
+ state: "committed",
2560
+ attempts,
2561
+ reason: null
2562
+ };
2563
+ }
2564
+ definitionKey(name, version) {
2565
+ return `${name}:${version}`;
2566
+ }
2567
+ normalizeDefinitions(definitions) {
2568
+ return definitions.map((definition) => {
2569
+ const normalized = this.definitionContract.normalize(definition);
2570
+ if (!normalized.ok) {
2571
+ throw new Error(normalized.error);
2572
+ }
2573
+ return normalized.value;
2574
+ });
2575
+ }
2576
+ bucketStart(occurredAtMs, resolutionMs) {
2577
+ return Math.floor(occurredAtMs / resolutionMs) * resolutionMs;
2578
+ }
2579
+ encodeCursor(cursor) {
2580
+ return Buffer.from(canonicalJson(cursor), "utf8").toString("base64url");
2581
+ }
2582
+ readStringField(row, field) {
2583
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
2584
+ throw new Error(`Expected a SQLite row while reading ${field}`);
2585
+ }
2586
+ const value = Reflect.get(row, field);
2587
+ if (typeof value !== "string") {
2588
+ throw new Error(`Expected string field ${field}`);
2589
+ }
2590
+ return value;
2591
+ }
2592
+ readNumberField(row, field) {
2593
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
2594
+ throw new Error(`Expected a SQLite row while reading ${field}`);
2595
+ }
2596
+ const value = Reflect.get(row, field);
2597
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2598
+ throw new Error(`Expected numeric field ${field}`);
2599
+ }
2600
+ return value;
2601
+ }
2602
+ parseCommittedDropCounter(row) {
2603
+ const reason = this.readStringField(row, "reason");
2604
+ const priority = this.readStringField(row, "priority");
2605
+ const count = this.readNumberField(row, "count");
2606
+ if (!this.isDropReason(reason) || !this.isDropPriority(priority)) {
2607
+ throw new Error("Stored committed telemetry drop counter identity is invalid");
2608
+ }
2609
+ if (!Number.isSafeInteger(count) || count < 0) {
2610
+ throw new Error("Stored committed telemetry drop counter value is invalid");
2611
+ }
2612
+ return Object.freeze({ reason, priority, count });
2613
+ }
2614
+ errorMessage(cause) {
2615
+ return cause instanceof Error ? cause.message : String(cause);
2616
+ }
2617
+ isJsonObject(value) {
2618
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2619
+ }
2620
+ isMetricAggregate(value) {
2621
+ return value === "sum" || value === "min" || value === "max" || value === "last";
2622
+ }
2623
+ isDropReason(value) {
2624
+ return value === "capacity" || value === "sampled" || value === "stopped" || value === "invalid" || value === "transport";
2625
+ }
2626
+ isDropPriority(value) {
2627
+ return value === "low" || value === "normal" || value === "high";
2628
+ }
2629
+ };
2630
+
2631
+ // src/storage/telemetry_table_map.ts
2632
+ var TelemetryTableMap = class _TelemetryTableMap {
2633
+ constructor(mapping, checksum) {
2634
+ this.mapping = mapping;
2635
+ this.checksum = checksum;
2636
+ this.tokenMap = Object.freeze({
2637
+ ...mapping,
2638
+ rawEventsOccurredIndex: this.indexName(mapping.rawEvents, "occurred"),
2639
+ rawEventsConversationIndex: this.indexName(mapping.rawEvents, "conversation"),
2640
+ rawEventsLabelIndex: this.indexName(mapping.rawEvents, "label"),
2641
+ metricSamplesTimeIndex: this.indexName(mapping.metricSamples, "time"),
2642
+ metricSamplesEventIndex: this.indexName(mapping.metricSamples, "event"),
2643
+ metricRollupsTimeIndex: this.indexName(mapping.metricRollups, "time")
2644
+ });
2645
+ }
2646
+ mapping;
2647
+ checksum;
2648
+ tokenMap;
2649
+ static create(configuration) {
2650
+ const prefixError = _TelemetryTableMap.validateOptionalPrefix(configuration.tablePrefix);
2651
+ if (prefixError !== null) {
2652
+ return { ok: false, error: prefixError };
2653
+ }
2654
+ const entries = SQLITE_LOGICAL_TABLE_NAMES.map((logicalName) => {
2655
+ const override = configuration.tables[logicalName];
2656
+ const defaultName = SQLITE_DEFAULT_TABLE_NAMES[logicalName];
2657
+ const physicalName = override ?? (configuration.tablePrefix === null ? defaultName : `${configuration.tablePrefix}${defaultName}`);
2658
+ return [logicalName, physicalName];
2659
+ });
2660
+ for (const [logicalName, physicalName] of entries) {
2661
+ const identifierError = _TelemetryTableMap.validateIdentifier(physicalName);
2662
+ if (identifierError !== null) {
2663
+ return { ok: false, error: `${logicalName}: ${identifierError}` };
2664
+ }
2665
+ if (physicalName.toLowerCase() === SQLITE_SCHEMA_MANIFEST_TABLE) {
2666
+ return { ok: false, error: `${logicalName} cannot use the fixed schema manifest name` };
2667
+ }
2668
+ }
2669
+ const foldedNames = entries.map(([, physicalName]) => physicalName.toLowerCase());
2670
+ if (new Set(foldedNames).size !== foldedNames.length) {
2671
+ return { ok: false, error: "Resolved telemetry table names must be unique after case folding" };
2672
+ }
2673
+ const mapping = Object.freeze(Object.fromEntries(entries));
2674
+ const checksum = telemetryChecksum(mapping);
2675
+ return { ok: true, value: new _TelemetryTableMap(mapping, checksum) };
2676
+ }
2677
+ physical(logicalName) {
2678
+ return this.mapping[logicalName];
2679
+ }
2680
+ quoted(logicalName) {
2681
+ return this.quote(this.mapping[logicalName]);
2682
+ }
2683
+ manifestMapping() {
2684
+ return this.mapping;
2685
+ }
2686
+ substitute(sql) {
2687
+ const unknownTokens = [];
2688
+ const substituted = sql.replace(/\{\{([A-Za-z][A-Za-z0-9]*)\}\}/g, (_match, token) => {
2689
+ const identifier = this.tokenMap[token];
2690
+ if (identifier === void 0) {
2691
+ unknownTokens.push(token);
2692
+ return `{{${token}}}`;
2693
+ }
2694
+ return this.quote(identifier);
2695
+ });
2696
+ if (unknownTokens.length > 0 || substituted.includes("{{")) {
2697
+ return {
2698
+ ok: false,
2699
+ error: `Unknown telemetry migration identifier token(s): ${unknownTokens.join(", ") || "malformed token"}`
2700
+ };
2701
+ }
2702
+ return { ok: true, value: substituted };
2703
+ }
2704
+ quote(identifier) {
2705
+ return `"${identifier.replaceAll('"', '""')}"`;
2706
+ }
2707
+ indexName(tableName, purpose) {
2708
+ const raw = `${tableName}_${purpose}_idx`;
2709
+ if (raw.length <= 63) {
2710
+ return raw;
2711
+ }
2712
+ const suffix = telemetryChecksum(raw).slice(0, 8);
2713
+ return `${raw.slice(0, 54)}_${suffix}`;
2714
+ }
2715
+ static validateOptionalPrefix(prefix) {
2716
+ if (prefix === null) {
2717
+ return null;
2718
+ }
2719
+ return this.validateIdentifier(prefix);
2720
+ }
2721
+ static validateIdentifier(identifier) {
2722
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
2723
+ return `Invalid SQLite identifier ${identifier}`;
2724
+ }
2725
+ if (identifier.length > 63) {
2726
+ return `SQLite identifier ${identifier} exceeds 63 characters`;
2727
+ }
2728
+ if (identifier.toLowerCase().startsWith("sqlite_")) {
2729
+ return `SQLite identifier ${identifier} uses the reserved sqlite_ prefix`;
2730
+ }
2731
+ return null;
2732
+ }
2733
+ };
2734
+
2735
+ // src/transports/schema/telemetry_schema_manifest.ts
2736
+ var import_zod2 = require("zod");
2737
+ var TelemetrySchemaManifestRowSchema = import_zod2.z.object({
2738
+ target_id: import_zod2.z.string(),
2739
+ format_version: import_zod2.z.number().int().positive(),
2740
+ mapping_json: import_zod2.z.string(),
2741
+ mapping_checksum: import_zod2.z.string(),
2742
+ state: import_zod2.z.enum(["ready", "renaming"]),
2743
+ pending_mapping_json: import_zod2.z.string().nullable(),
2744
+ updated_at_ms: import_zod2.z.number().int()
2745
+ }).strict();
2746
+
2747
+ // src/transports/sqlite_transport.ts
2748
+ var import_meta = {};
2749
+ var DEFAULT_MIGRATIONS_DIRECTORY = typeof __dirname === "string" ? (0, import_node_path2.resolve)(__dirname, "../../migrations") : (0, import_node_url.fileURLToPath)(new URL("../../migrations/", import_meta.url));
2750
+ var SQLiteTelemetryTransport = class {
2751
+ constructor(options) {
2752
+ this.options = options;
2753
+ this.targetId = options.configuration.targetId;
2754
+ this.validateConfiguration();
2755
+ }
2756
+ options;
2757
+ targetId;
2758
+ capabilities = Object.freeze({
2759
+ rawPersistence: true,
2760
+ metricSamples: true,
2761
+ rollups: true,
2762
+ queries: true,
2763
+ retention: true,
2764
+ maintenance: true
2765
+ });
2766
+ state = "created";
2767
+ detail = null;
2768
+ tableMap = null;
2769
+ connection = null;
2770
+ repository = null;
2771
+ definitions = Object.freeze([]);
2772
+ lease = null;
2773
+ heartbeatHandle = null;
2774
+ activeReaders = 0;
2775
+ activeWrites = 0;
2776
+ accepting = false;
2777
+ static defaults(configuration) {
2778
+ return {
2779
+ configuration,
2780
+ migrationsDirectory: DEFAULT_MIGRATIONS_DIRECTORY,
2781
+ processToken: (0, import_node_crypto2.randomUUID)(),
2782
+ now: () => Date.now(),
2783
+ createInterval: (work, intervalMs) => setInterval(work, intervalMs),
2784
+ clearInterval: (handle) => clearInterval(handle),
2785
+ delay: (durationMs) => new Promise((resolve3) => setTimeout(resolve3, durationMs))
2786
+ };
2787
+ }
2788
+ async start(definitions) {
2789
+ if (this.state !== "created" && this.state !== "stopped") {
2790
+ return { ok: false, error: `Cannot start SQLite telemetry target from state ${this.state}` };
2791
+ }
2792
+ this.state = "starting";
2793
+ this.detail = null;
2794
+ const tableMap = TelemetryTableMap.create(this.options.configuration.schema);
2795
+ if (!tableMap.ok) {
2796
+ return this.failStart(tableMap.error);
2797
+ }
2798
+ this.tableMap = tableMap.value;
2799
+ this.lease = new SqliteOwnershipLease({
2800
+ targetId: this.targetId,
2801
+ databasePath: this.options.configuration.path,
2802
+ mode: "writer",
2803
+ expiryMs: this.options.configuration.leaseExpiryMs,
2804
+ processToken: this.options.processToken,
2805
+ now: this.options.now
2806
+ });
2807
+ const acquired = this.lease.acquire();
2808
+ if (!acquired.ok) {
2809
+ return this.failStart(acquired.error);
2810
+ }
2811
+ try {
2812
+ const connection = new import_better_sqlite3.default(this.options.configuration.path);
2813
+ this.connection = connection;
2814
+ this.configureConnection(connection);
2815
+ const manifestNeedsUpgrade = this.initializeAndVerifyManifest(connection, tableMap.value);
2816
+ await this.runMigrations(connection, tableMap.value);
2817
+ this.verifySchema(connection, tableMap.value);
2818
+ if (manifestNeedsUpgrade) {
2819
+ this.upgradeManifestMapping(connection, tableMap.value);
2820
+ }
2821
+ const stackDatabase = await (0, import_adapters.createSqliteDb)(connection);
2822
+ this.repository = new TelemetryRepository({
2823
+ targetId: this.targetId,
2824
+ connection,
2825
+ stackDatabase,
2826
+ tableMap: tableMap.value,
2827
+ retention: this.options.configuration.retention,
2828
+ now: this.options.now
2829
+ });
2830
+ const recovered = this.repository.recoverInterruptedMaintenance(this.options.now());
2831
+ if (!recovered.ok) {
2832
+ throw new Error(recovered.error);
2833
+ }
2834
+ const registered = this.repository.registerDefinitions(definitions);
2835
+ if (!registered.ok) {
2836
+ throw new Error(registered.error);
2837
+ }
2838
+ this.definitions = Object.freeze([...definitions]);
2839
+ this.startHeartbeat();
2840
+ this.accepting = true;
2841
+ this.state = "ready";
2842
+ return { ok: true, value: void 0 };
2843
+ } catch (cause) {
2844
+ this.closeOwnedResources();
2845
+ return this.failStart(this.errorMessage(cause));
2846
+ }
2847
+ }
2848
+ async persist(request) {
2849
+ if (!this.accepting || this.state !== "ready" || this.repository === null) {
2850
+ return this.closedAcknowledgement(request.batch.idempotencyKey);
2851
+ }
2852
+ this.activeWrites += 1;
2853
+ try {
2854
+ const persisted = this.repository.persist(request);
2855
+ if (persisted.kind === "acknowledged") {
2856
+ return persisted.acknowledgement;
2857
+ }
2858
+ this.detail = persisted.reason;
2859
+ return {
2860
+ kind: "retryable",
2861
+ idempotencyKey: request.batch.idempotencyKey,
2862
+ reason: persisted.reason,
2863
+ retryAfterMs: 100,
2864
+ targetStates: [{
2865
+ targetId: this.targetId,
2866
+ delivery: this.options.configuration.delivery,
2867
+ state: "failed",
2868
+ attempts: 1,
2869
+ reason: persisted.reason
2870
+ }]
2871
+ };
2872
+ } finally {
2873
+ this.activeWrites -= 1;
2874
+ }
2875
+ }
2876
+ async timeline(query) {
2877
+ const queryError = this.validateTimelineQuery(query);
2878
+ if (queryError !== null) {
2879
+ return { ok: false, error: { kind: "invalid-query", message: queryError } };
2880
+ }
2881
+ const result = this.withReaderLease(() => {
2882
+ const repository = this.requireRepository();
2883
+ const rawCutoffMs = this.options.now() - this.options.configuration.retention.rawMs;
2884
+ return repository.timeline(query, rawCutoffMs);
2885
+ });
2886
+ return this.enforceResponseBytes(result);
2887
+ }
2888
+ async metrics(query) {
2889
+ const queryError = this.validateMetricQuery(query);
2890
+ if (queryError !== null) {
2891
+ return { ok: false, error: { kind: "invalid-query", message: queryError } };
2892
+ }
2893
+ const result = this.withReaderLease(() => this.requireRepository().metrics(query));
2894
+ if (result.ok) {
2895
+ const bucketCount = result.value.segments.reduce((count, segment) => count + segment.buckets.length, 0);
2896
+ if (bucketCount > this.options.configuration.query.maxBuckets) {
2897
+ return {
2898
+ ok: false,
2899
+ error: { kind: "invalid-query", message: "Metric result exceeds the configured bucket maximum" }
2900
+ };
2901
+ }
2902
+ }
2903
+ return this.enforceResponseBytes(result);
2904
+ }
2905
+ async materializeRollups(nowMs, maxWorkMs) {
2906
+ return this.requireRepositoryResult((repository) => repository.materializeRollups(nowMs, maxWorkMs));
2907
+ }
2908
+ async enforceRetention(nowMs, shouldContinue = () => true) {
2909
+ const startedAt = this.options.now();
2910
+ let drain;
2911
+ try {
2912
+ drain = this.requireRepository().beginRetentionDrain(nowMs);
2913
+ } catch (cause) {
2914
+ return { ok: false, error: this.errorMessage(cause) };
2915
+ }
2916
+ let affectedRows = 0;
2917
+ let deletionRounds = 0;
2918
+ let interrupted = false;
2919
+ for (; ; ) {
2920
+ if (!shouldContinue()) {
2921
+ interrupted = true;
2922
+ break;
2923
+ }
2924
+ const batch = drain.nextBatch();
2925
+ if (!batch.ok) {
2926
+ return batch;
2927
+ }
2928
+ affectedRows += batch.value.deletedRows;
2929
+ deletionRounds += 1;
2930
+ if (!batch.value.hasMore) {
2931
+ break;
2932
+ }
2933
+ await this.options.delay(0);
2934
+ }
2935
+ return {
2936
+ ok: true,
2937
+ value: {
2938
+ stage: "retention",
2939
+ affectedRows,
2940
+ affectedPages: 0,
2941
+ elapsedMs: this.options.now() - startedAt,
2942
+ detail: interrupted ? "interrupted" : null,
2943
+ deletionRounds,
2944
+ completed: !interrupted,
2945
+ interrupted
2946
+ }
2947
+ };
2948
+ }
2949
+ async checkpoint() {
2950
+ return this.requireRepositoryResult((repository) => repository.checkpoint());
2951
+ }
2952
+ async reclaim(maxPages) {
2953
+ return this.requireRepositoryResult((repository) => repository.reclaim(maxPages));
2954
+ }
2955
+ async compact(deadlineMs) {
2956
+ const startedAt = this.options.now();
2957
+ this.accepting = false;
2958
+ while ((this.activeReaders > 0 || this.activeWrites > 0) && this.options.now() < deadlineMs) {
2959
+ await this.options.delay(1);
2960
+ }
2961
+ if (this.activeReaders > 0 || this.activeWrites > 0) {
2962
+ this.accepting = true;
2963
+ return { ok: false, error: "Timed out waiting for telemetry readers/writers to quiesce" };
2964
+ }
2965
+ try {
2966
+ const tableMap = this.requireTableMap();
2967
+ this.closeDatabaseConnectionForCompaction();
2968
+ this.compactClosedDatabase(tableMap);
2969
+ await this.reopenDatabaseAfterCompaction(tableMap);
2970
+ this.accepting = true;
2971
+ return {
2972
+ ok: true,
2973
+ value: {
2974
+ stage: "compaction",
2975
+ affectedRows: 0,
2976
+ affectedPages: 0,
2977
+ elapsedMs: this.options.now() - startedAt,
2978
+ detail: null
2979
+ }
2980
+ };
2981
+ } catch (cause) {
2982
+ const compactionError = this.errorMessage(cause);
2983
+ try {
2984
+ await this.reopenDatabaseAfterCompaction(this.requireTableMap());
2985
+ this.accepting = true;
2986
+ } catch (reopenCause) {
2987
+ this.state = "failed";
2988
+ this.detail = this.errorMessage(reopenCause);
2989
+ }
2990
+ return { ok: false, error: compactionError };
2991
+ }
2992
+ }
2993
+ async recordMaintenance(record) {
2994
+ try {
2995
+ return this.requireRepository().recordMaintenance(record);
2996
+ } catch (cause) {
2997
+ return { ok: false, error: this.errorMessage(cause) };
2998
+ }
2999
+ }
3000
+ status() {
3001
+ const committedDropCounters = this.repository?.committedDropCounters();
3002
+ return Object.freeze({
3003
+ targetId: this.targetId,
3004
+ state: this.state,
3005
+ detail: this.detail,
3006
+ writerLeaseHeld: this.lease?.isHeld() ?? false,
3007
+ committedDropCounters: committedDropCounters?.ok === true ? committedDropCounters.value : Object.freeze([])
3008
+ });
3009
+ }
3010
+ maintenanceConfiguration() {
3011
+ return this.options.configuration.maintenance;
3012
+ }
3013
+ async stop(deadlineMs) {
3014
+ this.accepting = false;
3015
+ while ((this.activeReaders > 0 || this.activeWrites > 0) && this.options.now() < deadlineMs) {
3016
+ await this.options.delay(1);
3017
+ }
3018
+ this.closeOwnedResources();
3019
+ this.state = "stopped";
3020
+ this.detail = null;
3021
+ }
3022
+ configureConnection(connection) {
3023
+ const userTableCount = this.readCount(connection.prepare(
3024
+ `SELECT COUNT(*) AS count FROM sqlite_master
3025
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`
3026
+ ).get());
3027
+ if (userTableCount === 0) {
3028
+ connection.pragma("auto_vacuum = INCREMENTAL");
3029
+ connection.exec("VACUUM");
3030
+ }
3031
+ const autoVacuum = Number(connection.pragma("auto_vacuum", { simple: true }));
3032
+ if (autoVacuum !== 2) {
3033
+ throw new Error("Telemetry SQLite target must use auto_vacuum=INCREMENTAL");
3034
+ }
3035
+ connection.pragma("journal_mode = WAL");
3036
+ connection.pragma("synchronous = NORMAL");
3037
+ connection.pragma("foreign_keys = ON");
3038
+ connection.pragma("busy_timeout = 5000");
3039
+ }
3040
+ closeDatabaseConnectionForCompaction() {
3041
+ const connection = this.requireConnection();
3042
+ connection.pragma("wal_checkpoint(TRUNCATE)");
3043
+ this.repository = null;
3044
+ this.connection = null;
3045
+ connection.close();
3046
+ }
3047
+ compactClosedDatabase(tableMap) {
3048
+ const connection = new import_better_sqlite3.default(this.options.configuration.path);
3049
+ try {
3050
+ this.configureConnection(connection);
3051
+ connection.exec("VACUUM");
3052
+ this.verifySchema(connection, tableMap);
3053
+ } finally {
3054
+ connection.close();
3055
+ }
3056
+ }
3057
+ async reopenDatabaseAfterCompaction(tableMap) {
3058
+ if (this.connection !== null && this.repository !== null) {
3059
+ return;
3060
+ }
3061
+ const connection = new import_better_sqlite3.default(this.options.configuration.path);
3062
+ try {
3063
+ this.configureConnection(connection);
3064
+ this.initializeAndVerifyManifest(connection, tableMap);
3065
+ this.verifySchema(connection, tableMap);
3066
+ const stackDatabase = await (0, import_adapters.createSqliteDb)(connection);
3067
+ const repository = new TelemetryRepository({
3068
+ targetId: this.targetId,
3069
+ connection,
3070
+ stackDatabase,
3071
+ tableMap,
3072
+ retention: this.options.configuration.retention,
3073
+ now: this.options.now
3074
+ });
3075
+ const loaded = repository.loadDefinitionsReadOnly(this.definitions);
3076
+ if (!loaded.ok) {
3077
+ throw new Error(loaded.error);
3078
+ }
3079
+ this.connection = connection;
3080
+ this.repository = repository;
3081
+ } catch (cause) {
3082
+ connection.close();
3083
+ throw cause;
3084
+ }
3085
+ }
3086
+ initializeAndVerifyManifest(connection, tableMap) {
3087
+ connection.exec(
3088
+ `CREATE TABLE IF NOT EXISTS "${SQLITE_SCHEMA_MANIFEST_TABLE}" (
3089
+ target_id TEXT PRIMARY KEY,
3090
+ format_version INTEGER NOT NULL,
3091
+ mapping_json TEXT NOT NULL,
3092
+ mapping_checksum TEXT NOT NULL,
3093
+ state TEXT NOT NULL CHECK (state IN ('ready', 'renaming')),
3094
+ pending_mapping_json TEXT,
3095
+ updated_at_ms INTEGER NOT NULL
3096
+ )`
3097
+ );
3098
+ const rows = connection.prepare(
3099
+ `SELECT target_id, format_version, mapping_json, mapping_checksum,
3100
+ state, pending_mapping_json, updated_at_ms
3101
+ FROM "${SQLITE_SCHEMA_MANIFEST_TABLE}"`
3102
+ ).all();
3103
+ if (rows.length === 0) {
3104
+ connection.prepare(
3105
+ `INSERT INTO "${SQLITE_SCHEMA_MANIFEST_TABLE}" (
3106
+ target_id, format_version, mapping_json, mapping_checksum,
3107
+ state, pending_mapping_json, updated_at_ms
3108
+ ) VALUES (?, ?, ?, ?, 'ready', NULL, ?)`
3109
+ ).run(
3110
+ this.targetId,
3111
+ 2,
3112
+ JSON.stringify(tableMap.manifestMapping()),
3113
+ tableMap.checksum,
3114
+ this.options.now()
3115
+ );
3116
+ return false;
3117
+ }
3118
+ if (rows.length !== 1) {
3119
+ throw new Error("Telemetry schema manifest must contain exactly one target mapping");
3120
+ }
3121
+ const parsed = TelemetrySchemaManifestRowSchema.safeParse(rows[0]);
3122
+ if (!parsed.success) {
3123
+ throw new Error(parsed.error.message);
3124
+ }
3125
+ if (parsed.data.target_id !== this.targetId) {
3126
+ throw new Error(`Telemetry database belongs to target ${parsed.data.target_id}, not ${this.targetId}`);
3127
+ }
3128
+ if (parsed.data.state !== "ready") {
3129
+ throw new Error("Telemetry schema rename recovery is required before startup");
3130
+ }
3131
+ if (parsed.data.mapping_checksum === tableMap.checksum) {
3132
+ return false;
3133
+ }
3134
+ if (this.isLegacyManifestMapping(parsed.data, tableMap)) {
3135
+ return true;
3136
+ }
3137
+ throw new Error("Telemetry table mapping differs from the schema manifest; run offline schema rename");
3138
+ }
3139
+ isLegacyManifestMapping(manifest, tableMap) {
3140
+ if (manifest.format_version !== 1) {
3141
+ return false;
3142
+ }
3143
+ const parsed = JSON.parse(manifest.mapping_json);
3144
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3145
+ return false;
3146
+ }
3147
+ if (telemetryChecksum(parsed) !== manifest.mapping_checksum) {
3148
+ return false;
3149
+ }
3150
+ const expected = tableMap.manifestMapping();
3151
+ const legacyEntries = Object.entries(expected).filter(
3152
+ ([logicalName]) => logicalName !== "committedDropCounters"
3153
+ );
3154
+ const actualEntries = Object.entries(parsed);
3155
+ return actualEntries.length === legacyEntries.length && legacyEntries.every(([logicalName, physicalName]) => {
3156
+ return Reflect.get(parsed, logicalName) === physicalName;
3157
+ });
3158
+ }
3159
+ upgradeManifestMapping(connection, tableMap) {
3160
+ connection.prepare(
3161
+ `UPDATE "${SQLITE_SCHEMA_MANIFEST_TABLE}"
3162
+ SET format_version = 2, mapping_json = ?, mapping_checksum = ?, updated_at_ms = ?
3163
+ WHERE target_id = ?`
3164
+ ).run(
3165
+ JSON.stringify(tableMap.manifestMapping()),
3166
+ tableMap.checksum,
3167
+ this.options.now(),
3168
+ this.targetId
3169
+ );
3170
+ }
3171
+ async runMigrations(connection, tableMap) {
3172
+ const sqlRunner = new TelemetryProperSqlRunner(connection, tableMap);
3173
+ const migrationRunner = await new import_proper.MigrationRunnerFactory().create({
3174
+ migration_folder: this.options.migrationsDirectory,
3175
+ migration_table: tableMap.quoted("migrationHistory"),
3176
+ database: "sqlite",
3177
+ sqlite: { database: this.options.configuration.path }
3178
+ }, sqlRunner);
3179
+ const pending = await migrationRunner.getPendingMigrations();
3180
+ await migrationRunner.migrate(pending, true);
3181
+ }
3182
+ verifySchema(connection, tableMap) {
3183
+ const rows = connection.prepare(
3184
+ `SELECT name FROM sqlite_master WHERE type = 'table'`
3185
+ ).all();
3186
+ const names = new Set(rows.map((row) => this.readName(row)));
3187
+ if (!names.has(SQLITE_SCHEMA_MANIFEST_TABLE)) {
3188
+ throw new Error("Telemetry schema manifest is missing");
3189
+ }
3190
+ const missing = SQLITE_LOGICAL_TABLE_NAMES.map((logicalName) => tableMap.physical(logicalName)).filter((physicalName) => !names.has(physicalName));
3191
+ if (missing.length > 0) {
3192
+ throw new Error(`Telemetry schema is missing table(s): ${missing.join(", ")}`);
3193
+ }
3194
+ }
3195
+ startHeartbeat() {
3196
+ const intervalMs = Math.max(1e3, Math.floor(this.options.configuration.leaseExpiryMs / 3));
3197
+ this.heartbeatHandle = this.options.createInterval(() => {
3198
+ const heartbeat = this.lease?.heartbeat();
3199
+ if (heartbeat && !heartbeat.ok) {
3200
+ this.accepting = false;
3201
+ this.state = "failed";
3202
+ this.detail = heartbeat.error;
3203
+ }
3204
+ }, intervalMs);
3205
+ }
3206
+ withReaderLease(work) {
3207
+ if (!this.accepting || this.state !== "ready") {
3208
+ return { ok: false, error: { kind: "storage-failure", message: "Telemetry target is not accepting readers" } };
3209
+ }
3210
+ this.activeReaders += 1;
3211
+ const leaseId = (0, import_node_crypto2.randomUUID)();
3212
+ const table = this.requireTableMap().quoted("readerLeases");
3213
+ const connection = this.requireConnection();
3214
+ try {
3215
+ const acquiredAtMs = this.options.now();
3216
+ connection.prepare(
3217
+ `INSERT INTO ${table} (
3218
+ target_id, lease_id, owner_id, acquired_at_ms, expires_at_ms, state
3219
+ ) VALUES (?, ?, ?, ?, ?, 'active')`
3220
+ ).run(
3221
+ this.targetId,
3222
+ leaseId,
3223
+ this.options.processToken,
3224
+ acquiredAtMs,
3225
+ acquiredAtMs + this.options.configuration.leaseExpiryMs
3226
+ );
3227
+ return work();
3228
+ } finally {
3229
+ connection.prepare(
3230
+ `UPDATE ${table} SET state = 'released' WHERE target_id = ? AND lease_id = ?`
3231
+ ).run(this.targetId, leaseId);
3232
+ this.activeReaders -= 1;
3233
+ }
3234
+ }
3235
+ requireRepositoryResult(work) {
3236
+ try {
3237
+ return work(this.requireRepository());
3238
+ } catch (cause) {
3239
+ return { ok: false, error: this.errorMessage(cause) };
3240
+ }
3241
+ }
3242
+ validateTimelineQuery(query) {
3243
+ const commonError = this.validateQueryRange(query.targetId, query.fromMs, query.toMs, query.limit);
3244
+ return commonError;
3245
+ }
3246
+ validateMetricQuery(query) {
3247
+ const rangeError = this.validateQueryRange(query.targetId, query.fromMs, query.toMs, query.limit);
3248
+ if (rangeError !== null) {
3249
+ return rangeError;
3250
+ }
3251
+ if (query.limit > this.options.configuration.query.maxBuckets) {
3252
+ return "Metric query limit exceeds the configured bucket maximum";
3253
+ }
3254
+ return null;
3255
+ }
3256
+ validateQueryRange(targetId, fromMs, toMs, limit) {
3257
+ if (targetId !== null && targetId !== this.targetId) {
3258
+ return `Query target ${targetId} does not match ${this.targetId}`;
3259
+ }
3260
+ if (!Number.isSafeInteger(fromMs) || !Number.isSafeInteger(toMs) || fromMs >= toMs) {
3261
+ return "Query time range must use safe integer milliseconds and be non-empty";
3262
+ }
3263
+ if (toMs - fromMs > this.options.configuration.query.maxRangeMs) {
3264
+ return "Query time range exceeds the configured maximum";
3265
+ }
3266
+ if (!Number.isInteger(limit) || limit <= 0 || limit > this.options.configuration.query.maxRows) {
3267
+ return "Query row limit is outside the configured bounds";
3268
+ }
3269
+ return null;
3270
+ }
3271
+ validateConfiguration() {
3272
+ const configuration = this.options.configuration;
3273
+ if (!configuration.targetId.trim() || !configuration.path.trim()) {
3274
+ throw new Error("SQLite telemetry target id and path are required");
3275
+ }
3276
+ if (!Number.isInteger(configuration.leaseExpiryMs) || configuration.leaseExpiryMs < 1e3) {
3277
+ throw new Error("SQLite telemetry ownership lease expiry must be at least one second");
3278
+ }
3279
+ const queryLimits = Object.values(configuration.query);
3280
+ if (queryLimits.some((limit) => !Number.isSafeInteger(limit) || limit <= 0)) {
3281
+ throw new Error("SQLite telemetry query limits must be positive safe integers");
3282
+ }
3283
+ const retention = configuration.retention;
3284
+ const invalidRetention = retention.rawMs <= 0 || retention.lateArrivalMs < 0 || retention.samplesMs < retention.rawMs + retention.lateArrivalMs;
3285
+ if (invalidRetention) {
3286
+ throw new Error("SQLite telemetry retention values are invalid");
3287
+ }
3288
+ this.validateRollupTiers();
3289
+ }
3290
+ validateRollupTiers() {
3291
+ const retention = this.options.configuration.retention;
3292
+ const tiers = retention.rollups;
3293
+ if (tiers.length === 0) {
3294
+ throw new Error("SQLite telemetry requires at least one rollup retention tier");
3295
+ }
3296
+ for (let index = 0; index < tiers.length; index += 1) {
3297
+ const tier = tiers[index];
3298
+ const previous = tiers[index - 1];
3299
+ if (!tier || tier.resolutionMs <= 0 || tier.retainMs < retention.rawMs) {
3300
+ throw new Error("SQLite telemetry rollup tiers are invalid");
3301
+ }
3302
+ if (previous && (tier.resolutionMs <= previous.resolutionMs || tier.retainMs < previous.retainMs)) {
3303
+ throw new Error("SQLite telemetry rollup tiers must increase by resolution and retention");
3304
+ }
3305
+ }
3306
+ }
3307
+ enforceResponseBytes(result) {
3308
+ if (!result.ok) {
3309
+ return result;
3310
+ }
3311
+ try {
3312
+ const bytes = telemetryUtf8Length(canonicalJson(result.value));
3313
+ if (bytes > this.options.configuration.query.maxResponseBytes) {
3314
+ return {
3315
+ ok: false,
3316
+ error: { kind: "invalid-query", message: "Query result exceeds the configured response byte maximum" }
3317
+ };
3318
+ }
3319
+ return result;
3320
+ } catch (cause) {
3321
+ return {
3322
+ ok: false,
3323
+ error: { kind: "storage-failure", message: `Unable to size telemetry query result: ${this.errorMessage(cause)}` }
3324
+ };
3325
+ }
3326
+ }
3327
+ failStart(error) {
3328
+ this.state = "failed";
3329
+ this.detail = error;
3330
+ return { ok: false, error };
3331
+ }
3332
+ closedAcknowledgement(idempotencyKey) {
3333
+ return {
3334
+ kind: "retryable",
3335
+ idempotencyKey,
3336
+ reason: "SQLite telemetry target is not ready",
3337
+ retryAfterMs: 100,
3338
+ targetStates: [{
3339
+ targetId: this.targetId,
3340
+ delivery: this.options.configuration.delivery,
3341
+ state: "failed",
3342
+ attempts: 0,
3343
+ reason: "target-not-ready"
3344
+ }]
3345
+ };
3346
+ }
3347
+ closeOwnedResources() {
3348
+ if (this.heartbeatHandle !== null) {
3349
+ this.options.clearInterval(this.heartbeatHandle);
3350
+ this.heartbeatHandle = null;
3351
+ }
3352
+ if (this.connection?.open) {
3353
+ try {
3354
+ this.connection.close();
3355
+ } catch {
3356
+ }
3357
+ }
3358
+ this.connection = null;
3359
+ this.repository = null;
3360
+ this.lease?.release();
3361
+ this.lease = null;
3362
+ }
3363
+ requireRepository() {
3364
+ if (this.repository === null) {
3365
+ throw new Error("SQLite telemetry repository is not initialized");
3366
+ }
3367
+ return this.repository;
3368
+ }
3369
+ requireConnection() {
3370
+ if (this.connection === null) {
3371
+ throw new Error("SQLite telemetry connection is not initialized");
3372
+ }
3373
+ return this.connection;
3374
+ }
3375
+ requireTableMap() {
3376
+ if (this.tableMap === null) {
3377
+ throw new Error("SQLite telemetry table map is not initialized");
3378
+ }
3379
+ return this.tableMap;
3380
+ }
3381
+ readCount(row) {
3382
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
3383
+ throw new Error("SQLite count query returned no row");
3384
+ }
3385
+ const count = Reflect.get(row, "count");
3386
+ if (typeof count !== "number") {
3387
+ throw new Error("SQLite count query returned an invalid count");
3388
+ }
3389
+ return count;
3390
+ }
3391
+ readName(row) {
3392
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
3393
+ throw new Error("SQLite schema query returned an invalid row");
3394
+ }
3395
+ const name = Reflect.get(row, "name");
3396
+ if (typeof name !== "string") {
3397
+ throw new Error("SQLite schema query returned an invalid table name");
3398
+ }
3399
+ return name;
3400
+ }
3401
+ errorMessage(cause) {
3402
+ return cause instanceof Error ? cause.message : String(cause);
3403
+ }
3404
+ };
3405
+ // Annotate the CommonJS export names for ESM import in node:
3406
+ 0 && (module.exports = {
3407
+ SQLiteTelemetryTransport
3408
+ });
3409
+ //# sourceMappingURL=index.cjs.map