@lunora/do 1.0.0-alpha.37 → 1.0.0-alpha.39

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.
@@ -5,11 +5,11 @@ import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
5
5
  import { e as encodeWire, d as decodeWire } from './wire-codec-CzQc1pvf.mjs';
6
6
  import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-Dy3oFZ26.mjs';
7
7
  import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
8
- import { n as normalizeLogFields, r as resolveTraceAnchor, a as createTracer, c as createMetrics, d as dispatchRootSpan } from './context-telemetry-DWfYDxCS.mjs';
8
+ import { n as normalizeLogFields, r as resolveTraceAnchor, a as createTracer, c as createMetrics, d as dispatchRootSpan } from './context-telemetry-CqcOObBl.mjs';
9
9
  import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
10
10
  import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
11
11
  import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
12
- import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-DVk02KpP.mjs';
12
+ import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-CnZkbXH_.mjs';
13
13
  import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
14
14
  import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
15
15
  import { stableStringify } from './stableStringify-mC40mZts.mjs';
@@ -22,9 +22,10 @@ import { redact, standardRules } from '@visulima/redact';
22
22
  import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } from './security-audit-CucgBice.mjs';
23
23
  import { runReadonlySql } from './MAX_SQL_ROWS-iFAA8FbD.mjs';
24
24
  import { ConflictError } from './ConflictError-CLoq37xH.mjs';
25
+ import { selectExpiredIds } from './selectExpiredIds-C5W29Upb.mjs';
25
26
  import { e as deleteGlobalShapeSnapshotsForConnection, g as readIdempotent, h as writeIdempotent, t as trimIdempotent, r as readClientWatermark, m as migrateClientWatermark, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, f as readGlobalShapeSnapshot, w as writeGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
26
27
  import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
27
- import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
28
+ import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-DTeFiHYS.mjs';
28
29
 
29
30
  const MAX_BATCH_ENTRIES = 500;
30
31
 
@@ -93,12 +94,12 @@ const verifyWsAdminToken = async (secret, token, now = Date.now()) => {
93
94
 
94
95
  const AUDIT_LOG_TABLE = "__lunora_audit__";
95
96
  const AUDIT_LOG_RETENTION = 1e3;
96
- const runSql$4 = (sql, query, ...params) => {
97
+ const runSql$5 = (sql, query, ...params) => {
97
98
  const runner = sql.exec;
98
99
  return runner.call(sql, query, ...params);
99
100
  };
100
101
  const ensureAuditTable = (sql) => {
101
- runSql$4(
102
+ runSql$5(
102
103
  sql,
103
104
  `CREATE TABLE IF NOT EXISTS "${AUDIT_LOG_TABLE}" (
104
105
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -112,7 +113,7 @@ const ensureAuditTable = (sql) => {
112
113
  };
113
114
  const appendAuditEntry = (sql, entry) => {
114
115
  ensureAuditTable(sql);
115
- runSql$4(
116
+ runSql$5(
116
117
  sql,
117
118
  `INSERT INTO "${AUDIT_LOG_TABLE}" (ts, op, "table", id, detail) VALUES (?, ?, ?, ?, ?)`,
118
119
  entry.ts,
@@ -124,13 +125,13 @@ const appendAuditEntry = (sql, entry) => {
124
125
  // eslint-disable-next-line unicorn/no-null -- SQL NULL is the correct value for an op with no associated table/id/detail.
125
126
  entry.detail === void 0 ? null : JSON.stringify(entry.detail)
126
127
  );
127
- runSql$4(sql, `DELETE FROM "${AUDIT_LOG_TABLE}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${AUDIT_LOG_TABLE}")`, AUDIT_LOG_RETENTION);
128
+ runSql$5(sql, `DELETE FROM "${AUDIT_LOG_TABLE}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${AUDIT_LOG_TABLE}")`, AUDIT_LOG_RETENTION);
128
129
  };
129
130
  const readAuditLog = (sql, options = {}) => {
130
131
  ensureAuditTable(sql);
131
132
  const sinceSeq = options.sinceSeq ?? 0;
132
133
  const limit = Math.max(1, Math.min(options.limit ?? AUDIT_LOG_RETENTION, 1e4));
133
- const rows = runSql$4(
134
+ const rows = runSql$5(
134
135
  sql,
135
136
  `SELECT seq, ts, op, "table", id, detail FROM "${AUDIT_LOG_TABLE}" WHERE seq > ? ORDER BY seq DESC LIMIT ?`,
136
137
  sinceSeq,
@@ -187,14 +188,14 @@ const ISSUE_STATE_TABLE = "__lunora_issue_state__";
187
188
  const HASH_QUERY_BATCH = 100;
188
189
  const ISSUE_STATUSES = ["ignored", "open", "resolved"];
189
190
  const ISSUE_SEVERITIES = ["critical", "high", "low", "medium"];
190
- const runSql$3 = (sql, query, ...parameters) => {
191
+ const runSql$4 = (sql, query, ...parameters) => {
191
192
  const runner = sql.exec;
192
193
  return runner.call(sql, query, ...parameters);
193
194
  };
194
195
  const SQL_NULL = null;
195
196
  const orNull$1 = (value) => value ?? SQL_NULL;
196
197
  const ensureIssueStateTable = (sql) => {
197
- runSql$3(
198
+ runSql$4(
198
199
  sql,
199
200
  `CREATE TABLE IF NOT EXISTS "${ISSUE_STATE_TABLE}" (
200
201
  hash TEXT PRIMARY KEY,
@@ -225,7 +226,7 @@ const readIssueStates = (sql, hashes) => {
225
226
  for (let start = 0; start < hashes.length; start += HASH_QUERY_BATCH) {
226
227
  const batch = hashes.slice(start, start + HASH_QUERY_BATCH);
227
228
  const placeholders = batch.map(() => "?").join(", ");
228
- const rows = runSql$3(
229
+ const rows = runSql$4(
229
230
  sql,
230
231
  `SELECT hash, status, assignee, severity, updated_at, updated_by FROM "${ISSUE_STATE_TABLE}" WHERE hash IN (${placeholders})`,
231
232
  ...batch
@@ -244,7 +245,7 @@ const upsertIssueState = (sql, hash, patch, updatedAt, updatedBy) => {
244
245
  const severity = orNull$1(patch.severity);
245
246
  const clearSeverity = patch.severity === null ? 1 : 0;
246
247
  const actor = orNull$1(updatedBy);
247
- runSql$3(
248
+ runSql$4(
248
249
  sql,
249
250
  `INSERT INTO "${ISSUE_STATE_TABLE}" (hash, status, assignee, severity, updated_at, updated_by)
250
251
  VALUES (?, COALESCE(?, 'open'), ?, ?, ?, ?)
@@ -268,7 +269,7 @@ const upsertIssueState = (sql, hash, patch, updatedAt, updatedBy) => {
268
269
  updatedAt,
269
270
  actor
270
271
  );
271
- const [row] = runSql$3(
272
+ const [row] = runSql$4(
272
273
  sql,
273
274
  `SELECT hash, status, assignee, severity, updated_at, updated_by FROM "${ISSUE_STATE_TABLE}" WHERE hash = ?`,
274
275
  hash
@@ -277,7 +278,7 @@ const upsertIssueState = (sql, hash, patch, updatedAt, updatedBy) => {
277
278
  };
278
279
 
279
280
  const DEFAULT_CAPACITY$1 = 256;
280
- const seriesKey = (event) => `${event.kind}\0${event.name}\0${stableStringify(event.attributes ?? {})}`;
281
+ const metricSeriesKey = (event) => `${event.kind}${event.name}${stableStringify(event.attributes ?? {})}`;
281
282
  class MetricBuffer {
282
283
  capacity;
283
284
  series = /* @__PURE__ */ new Map();
@@ -304,7 +305,7 @@ class MetricBuffer {
304
305
  }
305
306
  /** Fold one measurement into its series, creating or updating the aggregate. */
306
307
  push(event) {
307
- const key = seriesKey(event);
308
+ const key = metricSeriesKey(event);
308
309
  const existing = this.series.get(key);
309
310
  if (existing === void 0) {
310
311
  if (this.series.size >= this.capacity) {
@@ -316,6 +317,7 @@ class MetricBuffer {
316
317
  this.series.set(key, {
317
318
  ...event.attributes === void 0 ? {} : { attributes: event.attributes },
318
319
  count: 1,
320
+ ...event.traceId === void 0 ? {} : { exemplarTraceId: event.traceId },
319
321
  firstTs: event.ts,
320
322
  functionPath: event.functionPath,
321
323
  kind: event.kind,
@@ -337,10 +339,152 @@ class MetricBuffer {
337
339
  existing.last = event.value;
338
340
  existing.lastTs = event.ts;
339
341
  existing.functionPath = event.functionPath;
342
+ if (event.traceId !== void 0) {
343
+ existing.exemplarTraceId = event.traceId;
344
+ }
340
345
  this.series.set(key, existing);
341
346
  }
342
347
  }
343
348
 
349
+ const METRIC_HISTORY_TABLE = "__lunora_metric_history";
350
+ const METRIC_HISTORY_BUCKET_MS = 6e4;
351
+ const METRIC_HISTORY_BUCKET_RETENTION = 1440;
352
+ const METRIC_HISTORY_MAX_SERIES = 1e3;
353
+ const METRIC_HISTORY_READ_LIMIT = 5e3;
354
+ const runSql$3 = (sql, query, ...parameters) => {
355
+ const runner = sql.exec;
356
+ return runner.call(sql, query, ...parameters);
357
+ };
358
+ const bucketFloor = (ts) => Math.floor(ts / METRIC_HISTORY_BUCKET_MS) * METRIC_HISTORY_BUCKET_MS;
359
+ const ensureMetricHistoryTable = (sql) => {
360
+ runSql$3(
361
+ sql,
362
+ `CREATE TABLE IF NOT EXISTS "${METRIC_HISTORY_TABLE}" (
363
+ series_key TEXT NOT NULL,
364
+ bucket_ms INTEGER NOT NULL,
365
+ name TEXT NOT NULL,
366
+ kind TEXT NOT NULL,
367
+ attrs TEXT NOT NULL DEFAULT '{}',
368
+ function_path TEXT NOT NULL DEFAULT '',
369
+ shard_key TEXT,
370
+ count INTEGER NOT NULL DEFAULT 0,
371
+ sum REAL NOT NULL DEFAULT 0,
372
+ min REAL NOT NULL DEFAULT 0,
373
+ max REAL NOT NULL DEFAULT 0,
374
+ last REAL NOT NULL DEFAULT 0,
375
+ last_ts REAL NOT NULL DEFAULT 0,
376
+ exemplar_trace TEXT,
377
+ PRIMARY KEY (series_key, bucket_ms)
378
+ )`
379
+ );
380
+ };
381
+ const recordMetricHistory = (sql, event, exemplarTraceId) => {
382
+ ensureMetricHistoryTable(sql);
383
+ const key = metricSeriesKey(event);
384
+ const bucket = bucketFloor(event.ts);
385
+ const bucketExists = runSql$3(sql, `SELECT 1 AS c FROM "${METRIC_HISTORY_TABLE}" WHERE series_key = ? AND bucket_ms = ? LIMIT 1`, key, bucket).toArray().length > 0;
386
+ if (!bucketExists) {
387
+ const seriesTracked = runSql$3(sql, `SELECT 1 AS c FROM "${METRIC_HISTORY_TABLE}" WHERE series_key = ? LIMIT 1`, key).toArray().length > 0;
388
+ if (!seriesTracked) {
389
+ const seriesCountRow = runSql$3(sql, `SELECT COUNT(DISTINCT series_key) AS n FROM "${METRIC_HISTORY_TABLE}"`).one();
390
+ if (seriesCountRow.n >= METRIC_HISTORY_MAX_SERIES) {
391
+ return;
392
+ }
393
+ }
394
+ }
395
+ const exemplar = exemplarTraceId ?? null;
396
+ runSql$3(
397
+ sql,
398
+ `INSERT INTO "${METRIC_HISTORY_TABLE}"
399
+ (series_key, bucket_ms, name, kind, attrs, function_path, shard_key, count, sum, min, max, last, last_ts, exemplar_trace)
400
+ VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
401
+ ON CONFLICT(series_key, bucket_ms) DO UPDATE SET
402
+ count = count + 1,
403
+ sum = sum + excluded.sum,
404
+ min = MIN(min, excluded.min),
405
+ max = MAX(max, excluded.max),
406
+ last = excluded.last,
407
+ last_ts = excluded.last_ts,
408
+ exemplar_trace = CASE WHEN excluded.exemplar_trace IS NULL THEN exemplar_trace ELSE excluded.exemplar_trace END`,
409
+ key,
410
+ bucket,
411
+ event.name,
412
+ event.kind,
413
+ stableStringify(event.attributes ?? {}),
414
+ event.functionPath,
415
+ // eslint-disable-next-line unicorn/no-null -- SQL NULL for the unnamed root DO's absent shard key.
416
+ event.shardKey ?? null,
417
+ event.value,
418
+ event.value,
419
+ event.value,
420
+ event.value,
421
+ event.ts,
422
+ exemplar
423
+ );
424
+ if (!bucketExists) {
425
+ runSql$3(
426
+ sql,
427
+ `DELETE FROM "${METRIC_HISTORY_TABLE}"
428
+ WHERE series_key = ?
429
+ AND bucket_ms <= (
430
+ SELECT MAX(bucket_ms) - ? FROM "${METRIC_HISTORY_TABLE}" WHERE series_key = ?
431
+ )`,
432
+ key,
433
+ METRIC_HISTORY_BUCKET_RETENTION * METRIC_HISTORY_BUCKET_MS,
434
+ key
435
+ );
436
+ }
437
+ };
438
+ const parseAttributes = (raw) => {
439
+ if (raw === "" || raw === "{}") {
440
+ return void 0;
441
+ }
442
+ try {
443
+ const parsed = JSON.parse(raw);
444
+ return parsed !== null && typeof parsed === "object" ? parsed : void 0;
445
+ } catch {
446
+ return void 0;
447
+ }
448
+ };
449
+ const readMetricHistory = (sql, options = {}) => {
450
+ ensureMetricHistoryTable(sql);
451
+ const rows = options.sinceMs === void 0 ? runSql$3(sql, `SELECT * FROM "${METRIC_HISTORY_TABLE}" ORDER BY bucket_ms DESC LIMIT ?`, METRIC_HISTORY_READ_LIMIT).toArray() : runSql$3(
452
+ sql,
453
+ `SELECT * FROM "${METRIC_HISTORY_TABLE}" WHERE bucket_ms >= ? ORDER BY bucket_ms DESC LIMIT ?`,
454
+ options.sinceMs,
455
+ METRIC_HISTORY_READ_LIMIT
456
+ ).toArray();
457
+ const bySeries = /* @__PURE__ */ new Map();
458
+ for (const row of rows) {
459
+ let series = bySeries.get(row.series_key);
460
+ if (series === void 0) {
461
+ const attributes = parseAttributes(row.attrs);
462
+ series = {
463
+ ...attributes === void 0 ? {} : { attributes },
464
+ functionPath: row.function_path,
465
+ kind: row.kind,
466
+ name: row.name,
467
+ points: [],
468
+ ...row.shard_key === null ? {} : { shardKey: row.shard_key }
469
+ };
470
+ bySeries.set(row.series_key, series);
471
+ }
472
+ series.points.push({
473
+ bucketMs: row.bucket_ms,
474
+ count: row.count,
475
+ ...row.exemplar_trace === null ? {} : { exemplarTraceId: row.exemplar_trace },
476
+ last: row.last,
477
+ max: row.max,
478
+ min: row.min,
479
+ sum: row.sum
480
+ });
481
+ }
482
+ for (const series of bySeries.values()) {
483
+ series.points.sort((a, b) => a.bucketMs - b.bucketMs);
484
+ }
485
+ return { series: [...bySeries.values()] };
486
+ };
487
+
344
488
  const QUERY_METRICS_TABLE = "__lunora_metrics_queries";
345
489
  const QUERY_METRICS_MAX_SQL_LEN = 512;
346
490
  const QUERY_METRICS_MAX_STATEMENTS = 500;
@@ -1855,6 +1999,9 @@ const parseRunMigrationArgs = (args) => {
1855
1999
  };
1856
2000
  };
1857
2001
  const SHARD_BULK_DELETE_CAP = MAX_PAGE_SIZE;
2002
+ const TTL_SWEEP_BATCH = 200;
2003
+ const TTL_SWEEP_MAX_BATCHES = 20;
2004
+ const TTL_SWEEP_INTERVAL_MS = 3e4;
1858
2005
  const parseWriteRowArgs = (args) => {
1859
2006
  const { op } = args;
1860
2007
  const table = typeof args["table"] === "string" ? args["table"] : "";
@@ -2461,12 +2608,10 @@ class ShardDO {
2461
2608
  * reason; otherwise the earlier of the two candidate times (never later than
2462
2609
  * `nowMs`, so a source that's already due arms essentially immediately).
2463
2610
  */
2464
- static nextPollAlarmTarget(globalShapesRemaining, nextSourceDueAt, nowMs) {
2611
+ static nextPollAlarmTarget(globalShapesRemaining, nextSourceDueAt, nextTtlDueAt, nowMs) {
2465
2612
  const globalTarget = globalShapesRemaining > 0 ? nowMs + ShardDO.GLOBAL_SHAPE_POLL_INTERVAL_MS : void 0;
2466
- if (globalTarget === void 0) {
2467
- return nextSourceDueAt === void 0 ? void 0 : Math.max(nextSourceDueAt, nowMs);
2468
- }
2469
- return nextSourceDueAt === void 0 ? globalTarget : Math.min(globalTarget, nextSourceDueAt);
2613
+ const candidates = [globalTarget, nextSourceDueAt, nextTtlDueAt].filter((value) => value !== void 0).map((value) => Math.max(value, nowMs));
2614
+ return candidates.length > 0 ? Math.min(...candidates) : void 0;
2470
2615
  }
2471
2616
  state;
2472
2617
  env;
@@ -3147,8 +3292,15 @@ class ShardDO {
3147
3292
  this.recordShapeError("source:poll", error);
3148
3293
  nextSourceDueAt = Date.now() + ShardDO.GLOBAL_SHAPE_POLL_INTERVAL_MS;
3149
3294
  }
3295
+ let nextTtlDueAt;
3296
+ try {
3297
+ nextTtlDueAt = await this.pollTtlSweeps();
3298
+ } catch (error) {
3299
+ this.recordShapeError("ttl:sweep", error);
3300
+ nextTtlDueAt = Date.now() + ShardDO.GLOBAL_SHAPE_POLL_INTERVAL_MS;
3301
+ }
3150
3302
  await this.flushChangedTables();
3151
- const nextAlarmAt = ShardDO.nextPollAlarmTarget(globalShapesRemaining, nextSourceDueAt, Date.now());
3303
+ const nextAlarmAt = ShardDO.nextPollAlarmTarget(globalShapesRemaining, nextSourceDueAt, nextTtlDueAt, Date.now());
3152
3304
  if (nextAlarmAt !== void 0) {
3153
3305
  await this.scheduleGlobalPoll(nextAlarmAt);
3154
3306
  }
@@ -4337,6 +4489,60 @@ class ShardDO {
4337
4489
  scheduleSourcePoll() {
4338
4490
  return this.scheduleGlobalPoll();
4339
4491
  }
4492
+ /**
4493
+ * The resolved TTL policies (`.ttl(field, { after })`) for this DO's schema —
4494
+ * one {@link TtlSweepSpec} per table that declares a TTL. The base `ShardDO`
4495
+ * has no schema, so it returns `[]` and the TTL tier stays dormant. The
4496
+ * codegen subclass overrides it to read each table's `ttlPolicy` (+ its
4497
+ * `.softDelete()` marker) off the imported schema.
4498
+ */
4499
+ // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass reads the imported schema
4500
+ ttlSweeps() {
4501
+ return [];
4502
+ }
4503
+ /**
4504
+ * Sweep every `.ttl()` table once: page the rows past their expiry and remove
4505
+ * each THROUGH the schema-aware writer (`deleteRowThroughWriter`) so companions
4506
+ * / CDC / live subscriptions stay correct and a `.softDelete()` table soft-deletes
4507
+ * instead of physically removing the row. Work is bounded per tick
4508
+ * ({@link TTL_SWEEP_BATCH} × {@link TTL_SWEEP_MAX_BATCHES}) so a large backlog
4509
+ * drains across several alarms without stalling the shard.
4510
+ *
4511
+ * Returns the next-due timestamp (a coarse {@link TTL_SWEEP_INTERVAL_MS}
4512
+ * cadence, so freshly-written rows expire within a bounded window) while any
4513
+ * TTL table exists, or `undefined` when there are none — so a DO with no TTL
4514
+ * table never arms this tier.
4515
+ */
4516
+ async pollTtlSweeps() {
4517
+ const specs = this.ttlSweeps();
4518
+ if (specs.length === 0) {
4519
+ return void 0;
4520
+ }
4521
+ const sql = this.sql;
4522
+ const now = Date.now();
4523
+ for (const spec of specs) {
4524
+ let batches = 0;
4525
+ let hasMore = true;
4526
+ while (hasMore && batches < TTL_SWEEP_MAX_BATCHES) {
4527
+ const page = selectExpiredIds(sql, spec, now, TTL_SWEEP_BATCH);
4528
+ for (const id of page.ids) {
4529
+ await this.deleteRowThroughWriter(spec.table, id);
4530
+ }
4531
+ hasMore = page.hasMore;
4532
+ batches += 1;
4533
+ }
4534
+ }
4535
+ return now + TTL_SWEEP_INTERVAL_MS;
4536
+ }
4537
+ /**
4538
+ * Arm the shared poll alarm for the TTL sweep. Mirrors {@link scheduleSourcePoll};
4539
+ * the codegen subclass calls it once on construction when the schema declares a
4540
+ * `.ttl()` table so the sweep loop starts, after which {@link ShardDO.alarm}
4541
+ * re-arms itself. Idempotent; a no-op when the runtime exposes no `setAlarm`.
4542
+ */
4543
+ scheduleTtlSweep() {
4544
+ return this.scheduleGlobalPoll();
4545
+ }
4340
4546
  /** This DO's shard key (its DO name), or `__root__` for the single-DO default. The `tenantBy` mapper binds it into the source query. */
4341
4547
  currentShardKey() {
4342
4548
  return this.state.id?.name ?? ROOT_SHARD_NAME;
@@ -4595,15 +4801,23 @@ class ShardDO {
4595
4801
  * cross-instance aggregation is still the sink's job.
4596
4802
  */
4597
4803
  recordMetric(event, sink) {
4598
- try {
4599
- this.metricSeries.push(event);
4600
- } catch {
4601
- }
4602
- if (sink?.onMetric) {
4804
+ const exemplarTraceId = this.currentRequestTrace?.traceId;
4805
+ const stamped = exemplarTraceId === void 0 ? event : { ...event, traceId: exemplarTraceId };
4806
+ const bestEffort = (run) => {
4603
4807
  try {
4604
- sink.onMetric(event, { waitUntil: this.state.waitUntil?.bind(this.state) });
4808
+ run();
4605
4809
  } catch {
4606
4810
  }
4811
+ };
4812
+ bestEffort(() => {
4813
+ this.metricSeries.push(stamped);
4814
+ });
4815
+ const rawSql = this.state.storage.sql;
4816
+ bestEffort(() => {
4817
+ recordMetricHistory(rawSql, stamped, exemplarTraceId);
4818
+ });
4819
+ if (sink?.onMetric) {
4820
+ bestEffort(() => sink.onMetric?.(stamped, { waitUntil: this.state.waitUntil?.bind(this.state) }));
4607
4821
  }
4608
4822
  }
4609
4823
  /**
@@ -5809,6 +6023,9 @@ class ShardDO {
5809
6023
  if (functionPath === ADMIN_FUNCTIONS.getMetricSeries) {
5810
6024
  return { series: this.metricSeries.entries() };
5811
6025
  }
6026
+ if (functionPath === ADMIN_FUNCTIONS.getMetricHistory) {
6027
+ return readMetricHistory(this.sql);
6028
+ }
5812
6029
  if (functionPath === ADMIN_FUNCTIONS.getSettings) {
5813
6030
  return buildSettings(this.env);
5814
6031
  }
@@ -2,7 +2,7 @@ import { sql } from 'drizzle-orm';
2
2
  import { matchesStaticWhere } from './AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs';
3
3
  import { encodeAggregateKey, foldAggregateTally, aggregateTableName } from './aggregateTableName-CxNqY1Sl.mjs';
4
4
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
5
- import { D as DOC_COLUMN, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-BCHCWtrD.mjs';
5
+ import { D as DOC_COLUMN, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-CGAgiQUz.mjs';
6
6
  import { param } from './renderSql-D6eUcn2N.mjs';
7
7
  import { sortColumnName, matchesRankStaticWhere, encodePartitionKey, rankTableName } from './RANK_TIEBREAK-CXhdcA1o.mjs';
8
8
  import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
@@ -69,10 +69,19 @@ const createTracer = (deps) => {
69
69
  const spanId = otlpRandomHex(8);
70
70
  const startTs = Date.now();
71
71
  const normalized = normalizeLogFields(attributes);
72
+ const collected = {};
73
+ const spanHandle = {
74
+ setAttribute: (key, value) => {
75
+ Object.assign(collected, normalizeLogFields({ [key]: value }));
76
+ },
77
+ setAttributes: (fields) => {
78
+ Object.assign(collected, normalizeLogFields(fields));
79
+ }
80
+ };
72
81
  let ok = true;
73
82
  let error;
74
83
  try {
75
- return await function_(tracerFor(spanId));
84
+ return await function_(tracerFor(spanId), spanHandle);
76
85
  } catch (error_) {
77
86
  ok = false;
78
87
  error = {
@@ -84,8 +93,9 @@ const createTracer = (deps) => {
84
93
  throw error_;
85
94
  } finally {
86
95
  try {
96
+ const merged = { ...normalized, ...collected };
87
97
  record({
88
- ...normalized === void 0 ? {} : { attributes: normalized },
98
+ ...Object.keys(merged).length === 0 ? {} : { attributes: merged },
89
99
  durationMs: Date.now() - startTs,
90
100
  ...error === void 0 ? {} : { error },
91
101
  functionPath,
@@ -1 +1 @@
1
- export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './context-telemetry-DWfYDxCS.mjs';
1
+ export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './context-telemetry-CqcOObBl.mjs';
@@ -1,6 +1,6 @@
1
1
  import { sql } from 'drizzle-orm';
2
2
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
3
- import { D as DOC_COLUMN, r as rowToDocument, j as jsonPathSql } from './do-sql-BCHCWtrD.mjs';
3
+ import { D as DOC_COLUMN, r as rowToDocument, j as jsonPathSql } from './do-sql-CGAgiQUz.mjs';
4
4
  import { compileWhereSql } from './compileWhereSql-DE6yfRcQ.mjs';
5
5
  import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
6
6
 
@@ -2,6 +2,7 @@ import { sql } from 'drizzle-orm';
2
2
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
3
3
 
4
4
  const DOC_COLUMN = "__doc__";
5
+ const geoTableName = (table, indexName) => `${table}__geo_${indexName}`;
5
6
  const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
6
7
  const jsonPath = (field) => {
7
8
  if (field === "_id" || field === "id") {
@@ -84,4 +85,4 @@ const isFtsAvailable = (sql$1) => {
84
85
  return available;
85
86
  };
86
87
 
87
- export { AGG_KEY as A, DOC_COLUMN as D, AGG_VALUE as a, AGG_COUNT as b, createIndexSql as c, aggUpsertSql as d, qualifiedJsonPathSql as e, isFtsAvailable as i, jsonPathSql as j, quoteIdentifier as q, rowToDocument as r, tableColumns as t };
88
+ export { AGG_KEY as A, DOC_COLUMN as D, AGG_VALUE as a, AGG_COUNT as b, createIndexSql as c, aggUpsertSql as d, qualifiedJsonPathSql as e, geoTableName as g, isFtsAvailable as i, jsonPathSql as j, quoteIdentifier as q, rowToDocument as r, tableColumns as t };
@@ -1,7 +1,7 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  import { sql } from 'drizzle-orm';
3
3
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
4
- import { runExternalSourceTick, materializeExternalRowsIncremental } from './materializeExternalRows-DlQWMlw_.mjs';
4
+ import { runExternalSourceTick, materializeExternalRowsIncremental } from './materializeExternalRows-BtEGs1Fv.mjs';
5
5
 
6
6
  const SOURCE_CURSOR_TABLE = "__lunora_source_cursor";
7
7
  const serializeCursor = (value) => {
@@ -1,5 +1,5 @@
1
1
  import { applyCdcChanges } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
2
- import { s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
2
+ import { s as selectShapeRows } from './ctx-db-shapes-DTeFiHYS.mjs';
3
3
  import { diffExternalSource, projectExternalSourceRow } from './diffExternalSource-CovHfdyo.mjs';
4
4
  import { stableStringify } from './stableStringify-mC40mZts.mjs';
5
5
 
@@ -3,7 +3,7 @@ import { aggregateTableName } from './aggregateTableName-CxNqY1Sl.mjs';
3
3
  import { migrateCdcLog, migrateCdcMeta } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
4
4
  import { m as migrateClientWatermark, a as migrateIdempotency, b as migrateGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
5
5
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
6
- import { D as DOC_COLUMN, j as jsonPathSql, c as createIndexSql, t as tableColumns, i as isFtsAvailable, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-BCHCWtrD.mjs';
6
+ import { D as DOC_COLUMN, j as jsonPathSql, c as createIndexSql, t as tableColumns, i as isFtsAvailable, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT, g as geoTableName } from './do-sql-CGAgiQUz.mjs';
7
7
  import { sortColumnName, rankTableName } from './RANK_TIEBREAK-CXhdcA1o.mjs';
8
8
  import { ftsTableName } from './buildFtsMatch-BLEMawrp.mjs';
9
9
 
@@ -36,6 +36,20 @@ const migrateSearchIndexes = (sql$1, tableName, definition) => {
36
36
  );
37
37
  }
38
38
  };
39
+ const migrateGeoIndexes = (sql$1, tableName, definition) => {
40
+ if (!definition.geoIndexes) {
41
+ return;
42
+ }
43
+ for (const index of definition.geoIndexes) {
44
+ const geoTable = geoTableName(tableName, index.name);
45
+ runDrizzle(
46
+ sql$1,
47
+ sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(geoTable)} (${sql.identifier("__id__")} TEXT PRIMARY KEY, ${sql.identifier("__geohash__")} TEXT NOT NULL, ${sql.identifier("__lat__")} REAL NOT NULL, ${sql.identifier("__lng__")} REAL NOT NULL)`
48
+ );
49
+ const btreeName = `${tableName}__geo_${index.name}__btree`;
50
+ runDrizzle(sql$1, createIndexSql(btreeName, geoTable, sql`${sql.identifier("__geohash__")} ASC, ${sql.identifier("__id__")} ASC`, false));
51
+ }
52
+ };
39
53
  const migrateAggregateIndexes = (sql$1, tableName, definition) => {
40
54
  if (!definition.aggregateIndexes) {
41
55
  return;
@@ -90,6 +104,7 @@ const runShardMigrations = (sql$1, schema, options = {}) => {
90
104
  );
91
105
  migrateSecondaryIndexes(sql$1, tableName, definition);
92
106
  migrateSearchIndexes(sql$1, tableName, definition);
107
+ migrateGeoIndexes(sql$1, tableName, definition);
93
108
  migrateAggregateIndexes(sql$1, tableName, definition);
94
109
  migrateRankIndexes(sql$1, tableName, definition);
95
110
  }
@@ -0,0 +1,18 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
3
+ import { j as jsonPathSql } from './do-sql-CGAgiQUz.mjs';
4
+
5
+ const selectExpiredIds = (sql$1, spec, now, limit) => {
6
+ const cutoff = now - (spec.after ?? 0);
7
+ const conditions = [sql`${jsonPathSql(spec.field)} IS NOT NULL`, sql`${jsonPathSql(spec.field)} < ${cutoff}`];
8
+ if (spec.softDeleteField !== void 0) {
9
+ conditions.push(sql`${jsonPathSql(spec.softDeleteField)} IS NULL`);
10
+ }
11
+ const query = sql`SELECT id FROM ${sql.identifier(spec.table)} WHERE ${sql.join(conditions, sql` AND `)} LIMIT ${sql.raw(String(Math.max(0, Math.floor(limit)) + 1))}`;
12
+ const rows = runDrizzle(sql$1, query).toArray();
13
+ const hasMore = rows.length > limit;
14
+ const ids = rows.slice(0, limit).map((row) => row.id);
15
+ return { hasMore, ids };
16
+ };
17
+
18
+ export { selectExpiredIds };
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-DVk02KpP.mjs';
2
+ import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-CnZkbXH_.mjs';
3
3
 
4
4
  const serveRelationFanout = async (schema, database, functionPath, args) => {
5
5
  const table = typeof args["table"] === "string" ? args["table"] : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.37",
3
+ "version": "1.0.0-alpha.39",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.6",
49
+ "@lunora/errors": "1.0.0-alpha.7",
50
50
  "@lunora/fingerprint": "1.0.0-alpha.3",
51
51
  "@visulima/redact": "3.0.0",
52
52
  "drizzle-orm": "^0.45.2"