@prisma/streams-server 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CONTRIBUTING.md +8 -0
  2. package/package.json +2 -1
  3. package/src/app.ts +290 -17
  4. package/src/app_core.ts +1833 -698
  5. package/src/app_local.ts +144 -4
  6. package/src/auto_tune.ts +62 -0
  7. package/src/bootstrap.ts +159 -1
  8. package/src/concurrency_gate.ts +108 -0
  9. package/src/config.ts +116 -14
  10. package/src/db/db.ts +1201 -131
  11. package/src/db/schema.ts +308 -8
  12. package/src/foreground_activity.ts +55 -0
  13. package/src/index/indexer.ts +254 -124
  14. package/src/index/lexicon_file_cache.ts +261 -0
  15. package/src/index/lexicon_format.ts +93 -0
  16. package/src/index/lexicon_indexer.ts +789 -0
  17. package/src/index/secondary_indexer.ts +824 -0
  18. package/src/index/secondary_schema.ts +105 -0
  19. package/src/ingest.ts +10 -12
  20. package/src/manifest.ts +143 -8
  21. package/src/memory.ts +183 -8
  22. package/src/metrics.ts +15 -29
  23. package/src/metrics_emitter.ts +26 -3
  24. package/src/notifier.ts +121 -5
  25. package/src/objectstore/accounting.ts +92 -0
  26. package/src/objectstore/mock_r2.ts +1 -1
  27. package/src/objectstore/r2.ts +17 -1
  28. package/src/profiles/evlog/schema.ts +234 -0
  29. package/src/profiles/evlog.ts +299 -0
  30. package/src/profiles/generic.ts +47 -0
  31. package/src/profiles/index.ts +205 -0
  32. package/src/profiles/metrics/block_format.ts +109 -0
  33. package/src/profiles/metrics/normalize.ts +366 -0
  34. package/src/profiles/metrics/schema.ts +319 -0
  35. package/src/profiles/metrics.ts +85 -0
  36. package/src/profiles/profile.ts +225 -0
  37. package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
  38. package/src/profiles/stateProtocol/routes.ts +389 -0
  39. package/src/profiles/stateProtocol/types.ts +6 -0
  40. package/src/profiles/stateProtocol/validation.ts +51 -0
  41. package/src/profiles/stateProtocol.ts +100 -0
  42. package/src/read_filter.ts +468 -0
  43. package/src/reader.ts +2151 -164
  44. package/src/runtime_memory.ts +200 -0
  45. package/src/runtime_memory_sampler.ts +235 -0
  46. package/src/schema/read_json.ts +43 -0
  47. package/src/schema/registry.ts +563 -59
  48. package/src/search/agg_format.ts +638 -0
  49. package/src/search/aggregate.ts +389 -0
  50. package/src/search/binary/codec.ts +162 -0
  51. package/src/search/binary/docset.ts +67 -0
  52. package/src/search/binary/restart_strings.ts +181 -0
  53. package/src/search/binary/varint.ts +34 -0
  54. package/src/search/bitset.ts +19 -0
  55. package/src/search/col_format.ts +382 -0
  56. package/src/search/col_runtime.ts +59 -0
  57. package/src/search/column_encoding.ts +43 -0
  58. package/src/search/companion_file_cache.ts +319 -0
  59. package/src/search/companion_format.ts +313 -0
  60. package/src/search/companion_manager.ts +1086 -0
  61. package/src/search/companion_plan.ts +218 -0
  62. package/src/search/fts_format.ts +423 -0
  63. package/src/search/fts_runtime.ts +333 -0
  64. package/src/search/query.ts +875 -0
  65. package/src/search/schema.ts +245 -0
  66. package/src/segment/cache.ts +93 -2
  67. package/src/segment/cached_segment.ts +89 -0
  68. package/src/segment/format.ts +108 -36
  69. package/src/segment/segmenter.ts +79 -5
  70. package/src/segment/segmenter_worker.ts +31 -5
  71. package/src/segment/segmenter_workers.ts +40 -12
  72. package/src/server.ts +150 -36
  73. package/src/sqlite/adapter.ts +155 -8
  74. package/src/sqlite/runtime_stats.ts +163 -0
  75. package/src/stats.ts +3 -3
  76. package/src/stream_size_reconciler.ts +100 -0
  77. package/src/touch/canonical_change.ts +7 -0
  78. package/src/touch/live_metrics.ts +94 -64
  79. package/src/touch/live_templates.ts +15 -1
  80. package/src/touch/manager.ts +166 -88
  81. package/src/touch/{interpreter_worker.ts → processor_worker.ts} +13 -13
  82. package/src/touch/spec.ts +95 -92
  83. package/src/touch/touch_journal.ts +4 -0
  84. package/src/touch/worker_pool.ts +6 -13
  85. package/src/touch/worker_protocol.ts +3 -3
  86. package/src/uploader.ts +77 -6
  87. package/src/util/bloom256.ts +2 -2
  88. package/src/util/byte_lru.ts +73 -0
  89. package/src/util/lru.ts +8 -0
  90. package/src/util/stream_paths.ts +19 -0
@@ -1,5 +1,7 @@
1
1
  import type { IngestQueue } from "../ingest";
2
2
  import type { SqliteDurableStore } from "../db/db";
3
+ import type { StreamProfileStore } from "../profiles";
4
+ import { resolveEnabledTouchCapability } from "../profiles";
3
5
  import { STREAM_FLAG_TOUCH } from "../db/db";
4
6
  import { encodeOffset } from "../offset";
5
7
  import type { TouchConfig } from "./spec";
@@ -126,7 +128,7 @@ type StreamCounters = {
126
128
  latencySumMs: number;
127
129
  latencyHist: LatencyHistogram;
128
130
  };
129
- interpreter: {
131
+ processor: {
130
132
  eventsIn: number;
131
133
  changesOut: number;
132
134
  errors: number;
@@ -134,7 +136,7 @@ type StreamCounters = {
134
136
  scannedBatches: number;
135
137
  scannedButEmitted0Batches: number;
136
138
  noInterestFastForwardBatches: number;
137
- interpretedThroughDelta: number;
139
+ processedThroughDelta: number;
138
140
  touchesEmittedDelta: number;
139
141
  commitLagSamples: number;
140
142
  commitLagMsSum: number;
@@ -142,6 +144,10 @@ type StreamCounters = {
142
144
  };
143
145
  };
144
146
 
147
+ export type LiveMetricsMemoryStats = {
148
+ counterStreams: number;
149
+ };
150
+
145
151
  function defaultCounters(touchCfg: TouchConfig): StreamCounters {
146
152
  return {
147
153
  touch: {
@@ -179,7 +185,7 @@ function defaultCounters(touchCfg: TouchConfig): StreamCounters {
179
185
  },
180
186
  templates: { activated: 0, retired: 0, evicted: 0, activationDenied: 0 },
181
187
  wait: { calls: 0, keysWatchedTotal: 0, touched: 0, timeout: 0, stale: 0, latencySumMs: 0, latencyHist: makeLatencyHistogram() },
182
- interpreter: {
188
+ processor: {
183
189
  eventsIn: 0,
184
190
  changesOut: 0,
185
191
  errors: 0,
@@ -187,7 +193,7 @@ function defaultCounters(touchCfg: TouchConfig): StreamCounters {
187
193
  scannedBatches: 0,
188
194
  scannedButEmitted0Batches: 0,
189
195
  noInterestFastForwardBatches: 0,
190
- interpretedThroughDelta: 0,
196
+ processedThroughDelta: 0,
191
197
  touchesEmittedDelta: 0,
192
198
  commitLagSamples: 0,
193
199
  commitLagMsSum: 0,
@@ -199,6 +205,7 @@ function defaultCounters(touchCfg: TouchConfig): StreamCounters {
199
205
  export class LiveMetricsV2 {
200
206
  private readonly db: SqliteDurableStore;
201
207
  private readonly ingest: IngestQueue;
208
+ private readonly profiles: StreamProfileStore;
202
209
  private readonly metricsStream: string;
203
210
  private readonly enabled: boolean;
204
211
  private readonly intervalMs: number;
@@ -206,6 +213,10 @@ export class LiveMetricsV2 {
206
213
  private readonly snapshotChunkSize: number;
207
214
  private readonly retentionMs: number;
208
215
  private readonly getTouchJournal?: (stream: string) => { meta: TouchJournalMeta; interval: TouchJournalIntervalStats } | null;
216
+ private readonly onAppended?: (args: {
217
+ lastOffset: bigint;
218
+ stream: string;
219
+ }) => void;
209
220
  private timer: any | null = null;
210
221
  private snapshotTimer: any | null = null;
211
222
  private retentionTimer: any | null = null;
@@ -224,6 +235,7 @@ export class LiveMetricsV2 {
224
235
  constructor(
225
236
  db: SqliteDurableStore,
226
237
  ingest: IngestQueue,
238
+ profiles: StreamProfileStore,
227
239
  opts?: {
228
240
  enabled?: boolean;
229
241
  stream?: string;
@@ -232,10 +244,12 @@ export class LiveMetricsV2 {
232
244
  snapshotChunkSize?: number;
233
245
  retentionMs?: number;
234
246
  getTouchJournal?: (stream: string) => { meta: TouchJournalMeta; interval: TouchJournalIntervalStats } | null;
247
+ onAppended?: (args: { lastOffset: bigint; stream: string }) => void;
235
248
  }
236
249
  ) {
237
250
  this.db = db;
238
251
  this.ingest = ingest;
252
+ this.profiles = profiles;
239
253
  this.enabled = opts?.enabled !== false;
240
254
  this.metricsStream = opts?.stream ?? "live.metrics";
241
255
  this.intervalMs = opts?.intervalMs ?? 1000;
@@ -243,6 +257,7 @@ export class LiveMetricsV2 {
243
257
  this.snapshotChunkSize = opts?.snapshotChunkSize ?? 200;
244
258
  this.retentionMs = opts?.retentionMs ?? 7 * 24 * 60 * 60 * 1000;
245
259
  this.getTouchJournal = opts?.getTouchJournal;
260
+ this.onAppended = opts?.onAppended;
246
261
  }
247
262
 
248
263
  start(): void {
@@ -329,12 +344,16 @@ export class LiveMetricsV2 {
329
344
  return c;
330
345
  }
331
346
 
332
- recordInterpreterError(stream: string, touchCfg: TouchConfig): void {
347
+ getMemoryStats(): LiveMetricsMemoryStats {
348
+ return { counterStreams: this.counters.size };
349
+ }
350
+
351
+ recordProcessorError(stream: string, touchCfg: TouchConfig): void {
333
352
  const c = this.get(stream, touchCfg);
334
- c.interpreter.errors += 1;
353
+ c.processor.errors += 1;
335
354
  }
336
355
 
337
- recordInterpreterBatch(args: {
356
+ recordProcessorBatch(args: {
338
357
  stream: string;
339
358
  touchCfg: TouchConfig;
340
359
  rowsRead: number;
@@ -361,7 +380,7 @@ export class LiveMetricsV2 {
361
380
  fineTouchesSuppressedDueToBudget?: boolean;
362
381
  scannedButEmitted0?: boolean;
363
382
  noInterestFastForward?: boolean;
364
- interpretedThroughDelta?: number;
383
+ processedThroughDelta?: number;
365
384
  touchesEmittedDelta?: number;
366
385
  }): void {
367
386
  const c = this.get(args.stream, args.touchCfg);
@@ -377,18 +396,18 @@ export class LiveMetricsV2 {
377
396
  c.touch.fineWaitersActive = Math.max(c.touch.fineWaitersActive, Math.max(0, Math.floor(args.fineWaitersActive ?? 0)));
378
397
  c.touch.coarseWaitersActive = Math.max(c.touch.coarseWaitersActive, Math.max(0, Math.floor(args.coarseWaitersActive ?? 0)));
379
398
  c.touch.broadFineWaitersActive = Math.max(c.touch.broadFineWaitersActive, Math.max(0, Math.floor(args.broadFineWaitersActive ?? 0)));
380
- c.interpreter.eventsIn += Math.max(0, args.rowsRead);
381
- c.interpreter.changesOut += Math.max(0, args.changes);
382
- c.interpreter.lagSourceOffsets = Math.max(c.interpreter.lagSourceOffsets, Math.max(0, args.lagSourceOffsets));
383
- c.interpreter.scannedBatches += 1;
384
- if (args.scannedButEmitted0) c.interpreter.scannedButEmitted0Batches += 1;
385
- if (args.noInterestFastForward) c.interpreter.noInterestFastForwardBatches += 1;
386
- c.interpreter.interpretedThroughDelta += Math.max(0, Math.floor(args.interpretedThroughDelta ?? 0));
387
- c.interpreter.touchesEmittedDelta += Math.max(0, Math.floor(args.touchesEmittedDelta ?? 0));
399
+ c.processor.eventsIn += Math.max(0, args.rowsRead);
400
+ c.processor.changesOut += Math.max(0, args.changes);
401
+ c.processor.lagSourceOffsets = Math.max(c.processor.lagSourceOffsets, Math.max(0, args.lagSourceOffsets));
402
+ c.processor.scannedBatches += 1;
403
+ if (args.scannedButEmitted0) c.processor.scannedButEmitted0Batches += 1;
404
+ if (args.noInterestFastForward) c.processor.noInterestFastForwardBatches += 1;
405
+ c.processor.processedThroughDelta += Math.max(0, Math.floor(args.processedThroughDelta ?? 0));
406
+ c.processor.touchesEmittedDelta += Math.max(0, Math.floor(args.touchesEmittedDelta ?? 0));
388
407
  if (args.commitLagMs != null && Number.isFinite(args.commitLagMs) && args.commitLagMs >= 0) {
389
- c.interpreter.commitLagSamples += 1;
390
- c.interpreter.commitLagMsSum += args.commitLagMs;
391
- c.interpreter.commitLagHist.record(args.commitLagMs);
408
+ c.processor.commitLagSamples += 1;
409
+ c.processor.commitLagMsSum += args.commitLagMs;
410
+ c.processor.commitLagHist.record(args.commitLagMs);
392
411
  }
393
412
  c.touch.fineTouchesDroppedDueToBudget += Math.max(0, args.fineTouchesDroppedDueToBudget ?? 0);
394
413
  c.touch.fineTouchesSkippedColdTemplate += Math.max(0, args.fineTouchesSkippedColdTemplate ?? 0);
@@ -458,12 +477,18 @@ export class LiveMetricsV2 {
458
477
  }
459
478
 
460
479
  try {
461
- await this.ingest.appendInternal({
480
+ const appendRes = await this.ingest.appendInternal({
462
481
  stream: this.metricsStream,
463
482
  baseAppendMs: BigInt(Date.now()),
464
483
  rows,
465
484
  contentType: "application/json",
466
485
  });
486
+ if (!Result.isError(appendRes)) {
487
+ this.onAppended?.({
488
+ lastOffset: appendRes.value.lastOffset,
489
+ stream: this.metricsStream,
490
+ });
491
+ }
467
492
  } catch {
468
493
  // best-effort
469
494
  }
@@ -483,7 +508,7 @@ export class LiveMetricsV2 {
483
508
  return v > max ? Number.MAX_SAFE_INTEGER : Number(v);
484
509
  };
485
510
 
486
- const states = this.db.listStreamInterpreters();
511
+ const states = this.db.listStreamTouchStates();
487
512
  if (states.length === 0) return;
488
513
 
489
514
  const rows: Array<{ routingKey: Uint8Array | null; contentType: string; payload: Uint8Array }> = [];
@@ -501,16 +526,9 @@ export class LiveMetricsV2 {
501
526
  if (!regRow) continue;
502
527
 
503
528
  const touchCfg = ((): TouchConfig | null => {
504
- try {
505
- const row = this.db.getSchemaRegistry(stream);
506
- if (!row) return null;
507
- const raw = JSON.parse(row.registry_json);
508
- const cfg = raw?.interpreter?.touch;
509
- if (!cfg || !cfg.enabled) return null;
510
- return cfg as TouchConfig;
511
- } catch {
512
- return null;
513
- }
529
+ const profileRes = this.profiles.getProfileResult(stream, regRow);
530
+ if (Result.isError(profileRes)) return null;
531
+ return resolveEnabledTouchCapability(profileRes.value)?.touchCfg ?? null;
514
532
  })();
515
533
  if (!touchCfg) continue;
516
534
 
@@ -518,9 +536,9 @@ export class LiveMetricsV2 {
518
536
  const journal = this.getTouchJournal?.(stream) ?? null;
519
537
  const waitActive = journal?.meta.activeWaiters ?? 0;
520
538
  const tailSeq = regRow.next_offset > 0n ? regRow.next_offset - 1n : -1n;
521
- const interpretedThrough = st.interpreted_through;
522
- const gcThrough = interpretedThrough < regRow.uploaded_through ? interpretedThrough : regRow.uploaded_through;
523
- const backlog = tailSeq >= interpretedThrough ? tailSeq - interpretedThrough : 0n;
539
+ const processedThrough = st.processed_through;
540
+ const gcThrough = processedThrough < regRow.uploaded_through ? processedThrough : regRow.uploaded_through;
541
+ const backlog = tailSeq >= processedThrough ? tailSeq - processedThrough : 0n;
524
542
  const backlogNum = backlog > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(backlog);
525
543
  let walOldestOffset: string | null = null;
526
544
  try {
@@ -603,27 +621,27 @@ export class LiveMetricsV2 {
603
621
  notifyWakeMsMax: journal?.interval.notifyWakeMsMax ?? 0,
604
622
  timeoutHeapSize: journal?.interval.heapSize ?? 0,
605
623
  },
606
- interpreter: {
607
- eventsIn: c.interpreter.eventsIn,
608
- changesOut: c.interpreter.changesOut,
609
- errors: c.interpreter.errors,
610
- lagSourceOffsets: c.interpreter.lagSourceOffsets,
611
- scannedBatches: c.interpreter.scannedBatches,
612
- scannedButEmitted0Batches: c.interpreter.scannedButEmitted0Batches,
613
- noInterestFastForwardBatches: c.interpreter.noInterestFastForwardBatches,
614
- interpretedThroughDelta: c.interpreter.interpretedThroughDelta,
615
- touchesEmittedDelta: c.interpreter.touchesEmittedDelta,
616
- commitLagMsAvg: c.interpreter.commitLagSamples > 0 ? c.interpreter.commitLagMsSum / c.interpreter.commitLagSamples : 0,
617
- commitLagMsP50: c.interpreter.commitLagHist.p50(),
618
- commitLagMsP95: c.interpreter.commitLagHist.p95(),
619
- commitLagMsP99: c.interpreter.commitLagHist.p99(),
624
+ processor: {
625
+ eventsIn: c.processor.eventsIn,
626
+ changesOut: c.processor.changesOut,
627
+ errors: c.processor.errors,
628
+ lagSourceOffsets: c.processor.lagSourceOffsets,
629
+ scannedBatches: c.processor.scannedBatches,
630
+ scannedButEmitted0Batches: c.processor.scannedButEmitted0Batches,
631
+ noInterestFastForwardBatches: c.processor.noInterestFastForwardBatches,
632
+ processedThroughDelta: c.processor.processedThroughDelta,
633
+ touchesEmittedDelta: c.processor.touchesEmittedDelta,
634
+ commitLagMsAvg: c.processor.commitLagSamples > 0 ? c.processor.commitLagMsSum / c.processor.commitLagSamples : 0,
635
+ commitLagMsP50: c.processor.commitLagHist.p50(),
636
+ commitLagMsP95: c.processor.commitLagHist.p95(),
637
+ commitLagMsP99: c.processor.commitLagHist.p99(),
620
638
  },
621
639
  base: {
622
640
  tailOffset: encodeOffset(regRow.epoch, tailSeq),
623
641
  nextOffset: encodeOffset(regRow.epoch, regRow.next_offset),
624
642
  sealedThrough: encodeOffset(regRow.epoch, regRow.sealed_through),
625
643
  uploadedThrough: encodeOffset(regRow.epoch, regRow.uploaded_through),
626
- interpretedThrough: encodeOffset(regRow.epoch, interpretedThrough),
644
+ processedThrough: encodeOffset(regRow.epoch, processedThrough),
627
645
  gcThrough: encodeOffset(regRow.epoch, gcThrough),
628
646
  walOldestOffset,
629
647
  walRetainedRows: clampBigInt(regRow.wal_rows),
@@ -683,18 +701,18 @@ export class LiveMetricsV2 {
683
701
  c.wait.stale = 0;
684
702
  c.wait.latencySumMs = 0;
685
703
  c.wait.latencyHist.reset();
686
- c.interpreter.eventsIn = 0;
687
- c.interpreter.changesOut = 0;
688
- c.interpreter.errors = 0;
689
- c.interpreter.lagSourceOffsets = 0;
690
- c.interpreter.scannedBatches = 0;
691
- c.interpreter.scannedButEmitted0Batches = 0;
692
- c.interpreter.noInterestFastForwardBatches = 0;
693
- c.interpreter.interpretedThroughDelta = 0;
694
- c.interpreter.touchesEmittedDelta = 0;
695
- c.interpreter.commitLagSamples = 0;
696
- c.interpreter.commitLagMsSum = 0;
697
- c.interpreter.commitLagHist.reset();
704
+ c.processor.eventsIn = 0;
705
+ c.processor.changesOut = 0;
706
+ c.processor.errors = 0;
707
+ c.processor.lagSourceOffsets = 0;
708
+ c.processor.scannedBatches = 0;
709
+ c.processor.scannedButEmitted0Batches = 0;
710
+ c.processor.noInterestFastForwardBatches = 0;
711
+ c.processor.processedThroughDelta = 0;
712
+ c.processor.touchesEmittedDelta = 0;
713
+ c.processor.commitLagSamples = 0;
714
+ c.processor.commitLagMsSum = 0;
715
+ c.processor.commitLagHist.reset();
698
716
  c.gc.baseWalGcCalls = 0;
699
717
  c.gc.baseWalGcDeletedRows = 0;
700
718
  c.gc.baseWalGcDeletedBytes = 0;
@@ -704,12 +722,18 @@ export class LiveMetricsV2 {
704
722
 
705
723
  if (rows.length === 0) return;
706
724
  try {
707
- await this.ingest.appendInternal({
725
+ const appendRes = await this.ingest.appendInternal({
708
726
  stream: this.metricsStream,
709
727
  baseAppendMs: BigInt(nowMs),
710
728
  rows,
711
729
  contentType: "application/json",
712
730
  });
731
+ if (!Result.isError(appendRes)) {
732
+ this.onAppended?.({
733
+ lastOffset: appendRes.value.lastOffset,
734
+ stream: this.metricsStream,
735
+ });
736
+ }
713
737
  } catch {
714
738
  // best-effort
715
739
  }
@@ -718,7 +742,7 @@ export class LiveMetricsV2 {
718
742
  private async emitSnapshots(): Promise<void> {
719
743
  if (!this.enabled) return;
720
744
  const nowMs = Date.now();
721
- const streams = this.db.listStreamInterpreters().map((r) => r.stream);
745
+ const streams = this.db.listStreamTouchStates().map((r) => r.stream);
722
746
  if (streams.length === 0) return;
723
747
 
724
748
  const encoder = new TextEncoder();
@@ -814,12 +838,18 @@ export class LiveMetricsV2 {
814
838
 
815
839
  if (rows.length === 0) return;
816
840
  try {
817
- await this.ingest.appendInternal({
841
+ const appendRes = await this.ingest.appendInternal({
818
842
  stream: this.metricsStream,
819
843
  baseAppendMs: BigInt(nowMs),
820
844
  rows,
821
845
  contentType: "application/json",
822
846
  });
847
+ if (!Result.isError(appendRes)) {
848
+ this.onAppended?.({
849
+ lastOffset: appendRes.value.lastOffset,
850
+ stream: this.metricsStream,
851
+ });
852
+ }
823
853
  } catch {
824
854
  // best-effort
825
855
  }
@@ -66,6 +66,12 @@ export type TemplateLifecycleEvent =
66
66
 
67
67
  type RateState = { tokens: number; lastRefillMs: number };
68
68
 
69
+ export type LiveTemplateRegistryMemoryStats = {
70
+ lastSeenEntries: number;
71
+ dirtyLastSeenEntries: number;
72
+ rateStateStreams: number;
73
+ };
74
+
69
75
  function nowIso(ms: number): string {
70
76
  return new Date(ms).toISOString();
71
77
  }
@@ -105,6 +111,14 @@ export class LiveTemplateRegistry {
105
111
  return `${stream}\n${templateId}`;
106
112
  }
107
113
 
114
+ getMemoryStats(): LiveTemplateRegistryMemoryStats {
115
+ return {
116
+ lastSeenEntries: this.lastSeenMem.size,
117
+ dirtyLastSeenEntries: this.dirtyLastSeen.size,
118
+ rateStateStreams: this.rate.size,
119
+ };
120
+ }
121
+
108
122
  private allowActivation(stream: string, nowMs: number, limitPerMinute: number): boolean {
109
123
  if (limitPerMinute <= 0) return true;
110
124
  const ratePerMs = limitPerMinute / 60_000;
@@ -163,7 +177,7 @@ export class LiveTemplateRegistry {
163
177
  *
164
178
  * `baseStreamNextOffset` is used to set `active_from_source_offset` so we do
165
179
  * not backfill fine touches for history when a template is activated while
166
- * the interpreter is behind.
180
+ * touch processing is behind.
167
181
  */
168
182
  activate(args: {
169
183
  stream: string;