@prisma/streams-server 0.1.1 → 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 (91) 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/host_runtime.ts +5 -0
  45. package/src/runtime_memory.ts +200 -0
  46. package/src/runtime_memory_sampler.ts +235 -0
  47. package/src/schema/read_json.ts +43 -0
  48. package/src/schema/registry.ts +563 -59
  49. package/src/search/agg_format.ts +638 -0
  50. package/src/search/aggregate.ts +389 -0
  51. package/src/search/binary/codec.ts +162 -0
  52. package/src/search/binary/docset.ts +67 -0
  53. package/src/search/binary/restart_strings.ts +181 -0
  54. package/src/search/binary/varint.ts +34 -0
  55. package/src/search/bitset.ts +19 -0
  56. package/src/search/col_format.ts +382 -0
  57. package/src/search/col_runtime.ts +59 -0
  58. package/src/search/column_encoding.ts +43 -0
  59. package/src/search/companion_file_cache.ts +319 -0
  60. package/src/search/companion_format.ts +313 -0
  61. package/src/search/companion_manager.ts +1086 -0
  62. package/src/search/companion_plan.ts +218 -0
  63. package/src/search/fts_format.ts +423 -0
  64. package/src/search/fts_runtime.ts +333 -0
  65. package/src/search/query.ts +875 -0
  66. package/src/search/schema.ts +245 -0
  67. package/src/segment/cache.ts +93 -2
  68. package/src/segment/cached_segment.ts +89 -0
  69. package/src/segment/format.ts +108 -36
  70. package/src/segment/segmenter.ts +79 -5
  71. package/src/segment/segmenter_worker.ts +35 -6
  72. package/src/segment/segmenter_workers.ts +42 -12
  73. package/src/server.ts +150 -36
  74. package/src/sqlite/adapter.ts +185 -14
  75. package/src/sqlite/runtime_stats.ts +163 -0
  76. package/src/stats.ts +3 -3
  77. package/src/stream_size_reconciler.ts +100 -0
  78. package/src/touch/canonical_change.ts +7 -0
  79. package/src/touch/live_metrics.ts +94 -64
  80. package/src/touch/live_templates.ts +15 -1
  81. package/src/touch/manager.ts +166 -88
  82. package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
  83. package/src/touch/spec.ts +95 -92
  84. package/src/touch/touch_journal.ts +4 -0
  85. package/src/touch/worker_pool.ts +8 -14
  86. package/src/touch/worker_protocol.ts +3 -3
  87. package/src/uploader.ts +77 -6
  88. package/src/util/bloom256.ts +2 -2
  89. package/src/util/byte_lru.ts +73 -0
  90. package/src/util/lru.ts +8 -0
  91. package/src/util/stream_paths.ts +19 -0
@@ -2,9 +2,9 @@ import type { Config } from "../config";
2
2
  import type { SqliteDurableStore } from "../db/db";
3
3
  import type { IngestQueue } from "../ingest";
4
4
  import type { StreamNotifier } from "../notifier";
5
- import type { SchemaRegistryStore } from "../schema/registry";
6
- import { isTouchEnabled } from "./spec";
7
- import { TouchInterpreterWorkerPool } from "./worker_pool";
5
+ import type { StreamProfileStore } from "../profiles";
6
+ import { listTouchCapableProfileKinds, resolveEnabledTouchCapability, resolveTouchCapability } from "../profiles";
7
+ import { TouchProcessorWorkerPool } from "./worker_pool";
8
8
  import { LruCache } from "../util/lru";
9
9
  import type { BackpressureGate } from "../backpressure";
10
10
  import { LiveTemplateRegistry, type TemplateDecl } from "./live_templates";
@@ -27,12 +27,12 @@ const BASE_WAL_GC_INTERVAL_MS = (() => {
27
27
 
28
28
  const BASE_WAL_GC_CHUNK_OFFSETS = (() => {
29
29
  const raw = process.env.DS_BASE_WAL_GC_CHUNK_OFFSETS;
30
- if (raw == null || raw.trim() === "") return 100_000;
30
+ if (raw == null || raw.trim() === "") return 1_000_000;
31
31
  const n = Number(raw);
32
32
  if (!Number.isFinite(n) || n <= 0) {
33
33
  // eslint-disable-next-line no-console
34
34
  console.error(`invalid DS_BASE_WAL_GC_CHUNK_OFFSETS: ${raw}`);
35
- return 100_000;
35
+ return 1_000_000;
36
36
  }
37
37
  return Math.floor(n);
38
38
  })();
@@ -86,7 +86,7 @@ type StreamRuntimeTotals = {
86
86
  scanRowsTotal: number;
87
87
  scanBatchesTotal: number;
88
88
  scannedButEmitted0BatchesTotal: number;
89
- interpretedThroughDeltaTotal: number;
89
+ processedThroughDeltaTotal: number;
90
90
  touchesEmittedTotal: number;
91
91
  touchesTableTotal: number;
92
92
  touchesTemplateTotal: number;
@@ -99,11 +99,37 @@ type StreamRuntimeTotals = {
99
99
  waitStaleTotal: number;
100
100
  };
101
101
 
102
- export class TouchInterpreterManager {
102
+ export type TouchProcessorManagerMemoryStats = {
103
+ dirtyStreams: number;
104
+ journals: number;
105
+ journalsCreatedTotal: number;
106
+ journalFilterBytesTotal: number;
107
+ fineLagCoarseOnlyStreams: number;
108
+ touchModeStreams: number;
109
+ fineTokenBucketStreams: number;
110
+ hotFineStreams: number;
111
+ lagSourceOffsetStreams: number;
112
+ restrictedTemplateBucketStreams: number;
113
+ runtimeTotalsStreams: number;
114
+ zeroRowBacklogStreakStreams: number;
115
+ templateLastSeenEntries: number;
116
+ templateDirtyLastSeenEntries: number;
117
+ templateRateStateStreams: number;
118
+ liveMetricsCounterStreams: number;
119
+ };
120
+
121
+ export type TouchTopStreamEntry = {
122
+ stream: string;
123
+ journal_filter_bytes: number;
124
+ dirty: boolean;
125
+ touch_mode: "idle" | "fine" | "restricted" | "coarseOnly" | null;
126
+ };
127
+
128
+ export class TouchProcessorManager {
103
129
  private readonly cfg: Config;
104
130
  private readonly db: SqliteDurableStore;
105
- private readonly registry: SchemaRegistryStore;
106
- private readonly pool: TouchInterpreterWorkerPool;
131
+ private readonly profiles: StreamProfileStore;
132
+ private readonly pool: TouchProcessorWorkerPool;
107
133
  private timer: any | null = null;
108
134
  private running = false;
109
135
  private stopping = false;
@@ -122,6 +148,7 @@ export class TouchInterpreterManager {
122
148
  private readonly restrictedTemplateBucketStateByStream = new Map<string, RestrictedTemplateBucketState>();
123
149
  private readonly runtimeTotalsByStream = new Map<string, StreamRuntimeTotals>();
124
150
  private readonly zeroRowBacklogStreakByStream = new Map<string, number>();
151
+ private journalsCreatedTotal = 0;
125
152
  private streamScanCursor = 0;
126
153
  private restartWorkerPoolRequested = false;
127
154
  private lastWorkerPoolRestartAtMs = 0;
@@ -131,28 +158,31 @@ export class TouchInterpreterManager {
131
158
  db: SqliteDurableStore,
132
159
  ingest: IngestQueue,
133
160
  notifier: StreamNotifier,
134
- registry: SchemaRegistryStore,
161
+ profiles: StreamProfileStore,
135
162
  backpressure?: BackpressureGate
136
163
  ) {
137
164
  this.cfg = cfg;
138
165
  this.db = db;
139
- this.registry = registry;
140
- this.pool = new TouchInterpreterWorkerPool(cfg, cfg.interpreterWorkers);
166
+ this.profiles = profiles;
167
+ this.pool = new TouchProcessorWorkerPool(cfg, cfg.touchWorkers);
141
168
  this.templates = new LiveTemplateRegistry(db);
142
- this.liveMetrics = new LiveMetricsV2(db, ingest, {
169
+ this.liveMetrics = new LiveMetricsV2(db, ingest, profiles, {
143
170
  getTouchJournal: (stream) => {
144
171
  const j = this.journals.get(stream);
145
172
  if (!j) return null;
146
173
  return { meta: j.getMeta(), interval: j.snapshotAndResetIntervalStats() };
147
174
  },
175
+ onAppended: ({ lastOffset, stream }) => {
176
+ notifier.notify(stream, lastOffset);
177
+ notifier.notifyDetailsChanged(stream);
178
+ },
148
179
  });
149
180
  }
150
181
 
151
182
  start(): void {
152
183
  if (this.timer) return;
153
184
  this.stopping = false;
154
- this.pool.start();
155
- this.seedInterpretersFromRegistry();
185
+ this.seedTouchStateFromProfiles();
156
186
  const liveMetricsRes = this.liveMetrics.ensureStreamResult();
157
187
  if (Result.isError(liveMetricsRes)) {
158
188
  // eslint-disable-next-line no-console
@@ -160,10 +190,10 @@ export class TouchInterpreterManager {
160
190
  } else {
161
191
  this.liveMetrics.start();
162
192
  }
163
- if (this.cfg.interpreterCheckIntervalMs > 0) {
193
+ if (this.cfg.touchCheckIntervalMs > 0) {
164
194
  this.timer = setInterval(() => {
165
195
  void this.tick();
166
- }, this.cfg.interpreterCheckIntervalMs);
196
+ }, this.cfg.touchCheckIntervalMs);
167
197
  }
168
198
  }
169
199
 
@@ -187,6 +217,46 @@ export class TouchInterpreterManager {
187
217
  this.lastWorkerPoolRestartAtMs = 0;
188
218
  }
189
219
 
220
+ getMemoryStats(): TouchProcessorManagerMemoryStats {
221
+ let journalFilterBytesTotal = 0;
222
+ for (const journal of this.journals.values()) journalFilterBytesTotal += journal.getFilterBytes();
223
+ const templateStats = this.templates.getMemoryStats();
224
+ const liveMetricsStats = this.liveMetrics.getMemoryStats();
225
+ return {
226
+ dirtyStreams: this.dirty.size,
227
+ journals: this.journals.size,
228
+ journalsCreatedTotal: this.journalsCreatedTotal,
229
+ journalFilterBytesTotal,
230
+ fineLagCoarseOnlyStreams: this.fineLagCoarseOnlyByStream.size,
231
+ touchModeStreams: this.touchModeByStream.size,
232
+ fineTokenBucketStreams: this.fineTokenBucketsByStream.size,
233
+ hotFineStreams: this.hotFineByStream.size,
234
+ lagSourceOffsetStreams: this.lagSourceOffsetsByStream.size,
235
+ restrictedTemplateBucketStreams: this.restrictedTemplateBucketStateByStream.size,
236
+ runtimeTotalsStreams: this.runtimeTotalsByStream.size,
237
+ zeroRowBacklogStreakStreams: this.zeroRowBacklogStreakByStream.size,
238
+ templateLastSeenEntries: templateStats.lastSeenEntries,
239
+ templateDirtyLastSeenEntries: templateStats.dirtyLastSeenEntries,
240
+ templateRateStateStreams: templateStats.rateStateStreams,
241
+ liveMetricsCounterStreams: liveMetricsStats.counterStreams,
242
+ };
243
+ }
244
+
245
+ getTopStreams(limit = 5): TouchTopStreamEntry[] {
246
+ const rows: TouchTopStreamEntry[] = [];
247
+ for (const [stream, journal] of this.journals) {
248
+ rows.push({
249
+ stream,
250
+ journal_filter_bytes: journal.getFilterBytes(),
251
+ dirty: this.dirty.has(stream),
252
+ touch_mode: this.touchModeByStream.get(stream) ?? null,
253
+ });
254
+ }
255
+ return rows
256
+ .sort((a, b) => b.journal_filter_bytes - a.journal_filter_bytes || a.stream.localeCompare(b.stream))
257
+ .slice(0, Math.max(0, Math.floor(limit)));
258
+ }
259
+
190
260
  notify(stream: string): void {
191
261
  this.dirty.add(stream);
192
262
  }
@@ -194,14 +264,15 @@ export class TouchInterpreterManager {
194
264
  async tick(): Promise<void> {
195
265
  if (this.stopping) return;
196
266
  if (this.running) return;
197
- if (this.cfg.interpreterWorkers <= 0) return;
267
+ if (this.cfg.touchWorkers <= 0) return;
198
268
  this.running = true;
199
269
  try {
200
270
  const nowMs = Date.now();
201
271
  const dirtyNow = new Set(this.dirty);
202
272
  this.dirty.clear();
203
- const states = this.db.listStreamInterpreters();
273
+ const states = this.db.listStreamTouchStates();
204
274
  if (states.length === 0) return;
275
+ this.pool.start();
205
276
  const stateByStream = new Map(states.map((s) => [s.stream, s]));
206
277
 
207
278
  const ordered: string[] = [];
@@ -209,7 +280,7 @@ export class TouchInterpreterManager {
209
280
  for (const s of stateByStream.keys()) if (!dirtyNow.has(s)) ordered.push(s);
210
281
  const prioritized = this.prioritizeStreamsForProcessing(ordered, nowMs);
211
282
 
212
- const maxConcurrent = Math.max(1, this.cfg.interpreterWorkers);
283
+ const maxConcurrent = Math.max(1, this.cfg.touchWorkers);
213
284
  const tasks: Promise<void>[] = [];
214
285
  if (prioritized.length > 0) {
215
286
  const total = prioritized.length;
@@ -220,10 +291,10 @@ export class TouchInterpreterManager {
220
291
  if (this.failures.shouldSkip(stream)) continue;
221
292
  const st = stateByStream.get(stream);
222
293
  if (!st) continue;
223
- const p = this.processOne(stream, st.interpreted_through).catch((e) => {
294
+ const p = this.processOne(stream, st.processed_through).catch((e) => {
224
295
  this.failures.recordFailure(stream);
225
296
  // eslint-disable-next-line no-console
226
- console.error("touch interpreter failed", stream, e);
297
+ console.error("touch processor failed", stream, e);
227
298
  });
228
299
  tasks.push(p);
229
300
  }
@@ -237,23 +308,23 @@ export class TouchInterpreterManager {
237
308
  this.lastWorkerPoolRestartAtMs = Date.now();
238
309
  } catch (e) {
239
310
  // eslint-disable-next-line no-console
240
- console.error("touch interpreter worker-pool restart failed", e);
311
+ console.error("touch processor worker-pool restart failed", e);
241
312
  }
242
313
  }
243
314
 
244
- // Opportunistically GC base WAL beyond the interpreter checkpoint.
315
+ // Opportunistically GC base WAL beyond the touch-processing checkpoint.
245
316
  //
246
317
  // commitManifest() already GC's on upload, but it can't retroactively GC
247
- // rows that were held back by interpreter lag once the interpreter later
318
+ // rows that were held back by touch-processing lag once the processor later
248
319
  // catches up (unless another upload happens). This loop makes GC progress
249
320
  // deterministic for "catch up after lag" scenarios.
250
321
  for (const stream of stateByStream.keys()) {
251
322
  if (this.stopping) break;
252
323
  const srow = this.db.getStream(stream);
253
324
  if (!srow || this.db.isDeleted(srow)) continue;
254
- const interp = this.db.getStreamInterpreter(stream);
255
- if (!interp) continue;
256
- this.maybeGcBaseWal(stream, srow.uploaded_through, interp.interpreted_through);
325
+ const touchState = this.db.getStreamTouchState(stream);
326
+ if (!touchState) continue;
327
+ this.maybeGcBaseWal(stream, srow.uploaded_through, touchState.processed_through);
257
328
  }
258
329
 
259
330
  // Template retirement GC + last-seen flush (sliding window).
@@ -261,15 +332,16 @@ export class TouchInterpreterManager {
261
332
  let persistIntervalMin = Number.POSITIVE_INFINITY;
262
333
  for (const stream of stateByStream.keys()) {
263
334
  if (this.stopping) break;
264
- const regRes = this.registry.getRegistryResult(stream);
265
- if (Result.isError(regRes)) {
335
+ const profileRes = this.profiles.getProfileResult(stream);
336
+ if (Result.isError(profileRes)) {
266
337
  // eslint-disable-next-line no-console
267
- console.error("touch registry read failed", stream, regRes.error.message);
338
+ console.error("touch profile read failed", stream, profileRes.error.message);
268
339
  continue;
269
340
  }
270
- const reg = regRes.value;
271
- if (!isTouchEnabled(reg.interpreter)) continue;
272
- const touchCfg = reg.interpreter.touch;
341
+ const profile = profileRes.value;
342
+ const enabledTouch = resolveEnabledTouchCapability(profile);
343
+ if (!enabledTouch) continue;
344
+ const touchCfg = enabledTouch.touchCfg;
273
345
  touchCfgByStream.set(stream, touchCfg);
274
346
  const persistInterval = touchCfg.templates?.lastSeenPersistIntervalMs ?? 5 * 60 * 1000;
275
347
  if (persistInterval < persistIntervalMin) persistIntervalMin = persistInterval;
@@ -296,37 +368,38 @@ export class TouchInterpreterManager {
296
368
  }
297
369
  }
298
370
 
299
- private async processOne(stream: string, interpretedThrough: bigint): Promise<void> {
371
+ private async processOne(stream: string, processedThroughAtStart: bigint): Promise<void> {
300
372
  const srow = this.db.getStream(stream);
301
373
  if (!srow || this.db.isDeleted(srow)) {
302
- this.db.deleteStreamInterpreter(stream);
374
+ this.db.deleteStreamTouchState(stream);
303
375
  return;
304
376
  }
305
377
 
306
378
  const next = srow.next_offset;
307
379
  if (next <= 0n) return;
308
- const fromOffset = interpretedThrough + 1n;
380
+ const fromOffset = processedThroughAtStart + 1n;
309
381
  const toOffset = next - 1n;
310
382
  if (fromOffset > toOffset) return;
311
383
 
312
- const regRes = this.registry.getRegistryResult(stream);
313
- if (Result.isError(regRes)) {
384
+ const profileRes = this.profiles.getProfileResult(stream, srow);
385
+ if (Result.isError(profileRes)) {
314
386
  // eslint-disable-next-line no-console
315
- console.error("touch registry read failed", stream, regRes.error.message);
316
- this.db.deleteStreamInterpreter(stream);
387
+ console.error("touch profile read failed", stream, profileRes.error.message);
388
+ this.db.deleteStreamTouchState(stream);
317
389
  return;
318
390
  }
319
- const reg = regRes.value;
320
- if (!isTouchEnabled(reg.interpreter)) {
321
- this.db.deleteStreamInterpreter(stream);
391
+ const profile = profileRes.value;
392
+ const enabledTouch = resolveEnabledTouchCapability(profile);
393
+ if (!enabledTouch) {
394
+ this.db.deleteStreamTouchState(stream);
322
395
  return;
323
396
  }
324
- const touchCfg = reg.interpreter.touch;
397
+ const touchCfg = enabledTouch.touchCfg;
325
398
  const failProcessing = (message: string): void => {
326
399
  this.failures.recordFailure(stream);
327
- this.liveMetrics.recordInterpreterError(stream, touchCfg);
400
+ this.liveMetrics.recordProcessorError(stream, touchCfg);
328
401
  // eslint-disable-next-line no-console
329
- console.error("touch interpreter failed", stream, message);
402
+ console.error("touch processor failed", stream, message);
330
403
  };
331
404
 
332
405
  const nowMs = Date.now();
@@ -339,7 +412,7 @@ export class TouchInterpreterManager {
339
412
 
340
413
  // Guardrail: when lag/backlog grows too large, temporarily suppress
341
414
  // fine/template touches (coarse table touches are still emitted).
342
- const lagAtStart = toOffset >= interpretedThrough ? toOffset - interpretedThrough : 0n;
415
+ const lagAtStart = toOffset >= processedThroughAtStart ? toOffset - processedThroughAtStart : 0n;
343
416
  const suppressFineDueToLag = this.computeSuppressFineDueToLag(stream, touchCfg, lagAtStart, hasFineDemand);
344
417
  const j = this.getOrCreateJournal(stream, touchCfg);
345
418
  j.setCoalesceMs(this.computeAdaptiveCoalesceMs(touchCfg, lagAtStart, hasAnyWaiters));
@@ -375,7 +448,7 @@ export class TouchInterpreterManager {
375
448
  if (fineGranularity !== "template") {
376
449
  this.restrictedTemplateBucketStateByStream.delete(stream);
377
450
  }
378
- const interpretMode: "full" | "hotTemplatesOnly" = fineGranularity === "template" ? "hotTemplatesOnly" : "full";
451
+ const processingMode: "full" | "hotTemplatesOnly" = fineGranularity === "template" ? "hotTemplatesOnly" : "full";
379
452
  const touchMode: "idle" | "fine" | "restricted" | "coarseOnly" = !hasAnyWaiters ? "idle" : emitFineTouches ? (suppressFineDueToLag ? "restricted" : "fine") : "coarseOnly";
380
453
  this.touchModeByStream.set(stream, touchMode);
381
454
 
@@ -383,13 +456,13 @@ export class TouchInterpreterManager {
383
456
  stream,
384
457
  fromOffset,
385
458
  toOffset,
386
- interpreter: reg.interpreter,
387
- maxRows: Math.max(1, this.cfg.interpreterMaxBatchRows),
388
- maxBytes: Math.max(1, this.cfg.interpreterMaxBatchBytes),
459
+ profile,
460
+ maxRows: Math.max(1, this.cfg.touchMaxBatchRows),
461
+ maxBytes: Math.max(1, this.cfg.touchMaxBatchBytes),
389
462
  emitFineTouches,
390
463
  fineTouchBudget: emitFineTouches ? fineBudget : 0,
391
464
  fineGranularity,
392
- interpretMode,
465
+ processingMode,
393
466
  filterHotTemplates: !!(hotFine && hotFine.templateFilteringEnabled),
394
467
  hotTemplateIds: hotFine?.hotTemplateIdsForWorker ?? null,
395
468
  });
@@ -407,7 +480,7 @@ export class TouchInterpreterManager {
407
480
  this.restartWorkerPoolRequested = true;
408
481
  // eslint-disable-next-line no-console
409
482
  console.error(
410
- "touch interpreter produced zero-row batch despite WAL backlog; scheduling worker-pool restart",
483
+ "touch processor produced zero-row batch despite WAL backlog; scheduling worker-pool restart",
411
484
  stream,
412
485
  `from=${fromOffset.toString()}`,
413
486
  `to=${toOffset.toString()}`
@@ -477,7 +550,7 @@ export class TouchInterpreterManager {
477
550
  this.lagSourceOffsetsByStream.set(stream, effectiveLag);
478
551
  const maxSourceTsMs = Number(res.stats.maxSourceTsMs ?? 0);
479
552
  const commitLagMs = maxSourceTsMs > 0 ? Math.max(0, Date.now() - maxSourceTsMs) : undefined;
480
- this.liveMetrics.recordInterpreterBatch({
553
+ this.liveMetrics.recordProcessorBatch({
481
554
  stream,
482
555
  touchCfg,
483
556
  rowsRead: res.stats.rowsRead,
@@ -504,9 +577,13 @@ export class TouchInterpreterManager {
504
577
  broadFineWaitersActive: hotFine?.broadFineWaitersActive ?? 0,
505
578
  scannedButEmitted0: res.stats.rowsRead > 0 && touches.length === 0,
506
579
  noInterestFastForward: false,
507
- interpretedThroughDelta:
508
- res.processedThrough >= interpretedThrough
509
- ? Number((res.processedThrough - interpretedThrough) > BigInt(Number.MAX_SAFE_INTEGER) ? BigInt(Number.MAX_SAFE_INTEGER) : res.processedThrough - interpretedThrough)
580
+ processedThroughDelta:
581
+ res.processedThrough >= processedThroughAtStart
582
+ ? Number(
583
+ (res.processedThrough - processedThroughAtStart) > BigInt(Number.MAX_SAFE_INTEGER)
584
+ ? BigInt(Number.MAX_SAFE_INTEGER)
585
+ : res.processedThrough - processedThroughAtStart
586
+ )
510
587
  : 0,
511
588
  touchesEmittedDelta: touches.length,
512
589
  });
@@ -514,15 +591,19 @@ export class TouchInterpreterManager {
514
591
  // ignore
515
592
  }
516
593
 
517
- const interpretedDelta =
518
- res.processedThrough >= interpretedThrough
519
- ? Number((res.processedThrough - interpretedThrough) > BigInt(Number.MAX_SAFE_INTEGER) ? BigInt(Number.MAX_SAFE_INTEGER) : res.processedThrough - interpretedThrough)
594
+ const processedDelta =
595
+ res.processedThrough >= processedThroughAtStart
596
+ ? Number(
597
+ (res.processedThrough - processedThroughAtStart) > BigInt(Number.MAX_SAFE_INTEGER)
598
+ ? BigInt(Number.MAX_SAFE_INTEGER)
599
+ : res.processedThrough - processedThroughAtStart
600
+ )
520
601
  : 0;
521
602
  const totals = this.getOrCreateRuntimeTotals(stream);
522
603
  totals.scanBatchesTotal += 1;
523
604
  totals.scanRowsTotal += Math.max(0, res.stats.rowsRead);
524
605
  if (res.stats.rowsRead > 0 && touches.length === 0) totals.scannedButEmitted0BatchesTotal += 1;
525
- totals.interpretedThroughDeltaTotal += interpretedDelta;
606
+ totals.processedThroughDeltaTotal += processedDelta;
526
607
  totals.touchesEmittedTotal += touches.length;
527
608
  let tableTouches = 0;
528
609
  let templateTouches = 0;
@@ -537,13 +618,13 @@ export class TouchInterpreterManager {
537
618
  totals.fineTouchesSkippedColdKeyTotal += fineSkippedColdKey;
538
619
  totals.fineTouchesSkippedTemplateBucketTotal += fineSkippedTemplateBucket;
539
620
 
540
- this.db.updateStreamInterpreterThrough(stream, res.processedThrough);
621
+ this.db.updateStreamTouchStateThrough(stream, res.processedThrough);
541
622
  if (res.processedThrough < toOffset) this.dirty.add(stream);
542
623
  this.failures.recordSuccess(stream);
543
624
  }
544
625
 
545
- private maybeGcBaseWal(stream: string, uploadedThrough: bigint, interpretedThrough: bigint): void {
546
- const gcTargetThrough = interpretedThrough < uploadedThrough ? interpretedThrough : uploadedThrough;
626
+ private maybeGcBaseWal(stream: string, uploadedThrough: bigint, processedThrough: bigint): void {
627
+ const gcTargetThrough = processedThrough < uploadedThrough ? processedThrough : uploadedThrough;
547
628
  if (gcTargetThrough < 0n) return;
548
629
 
549
630
  const now = Date.now();
@@ -575,24 +656,20 @@ export class TouchInterpreterManager {
575
656
  }
576
657
  }
577
658
 
578
- private seedInterpretersFromRegistry(): void {
579
- // Bootstrap support: bootstrapFromR2 restores schema registry JSON but does not
580
- // populate stream_interpreters. Seeding here makes interpreters start working
659
+ private seedTouchStateFromProfiles(): void {
660
+ // Bootstrap support: bootstrapFromR2 restores profile state but does not
661
+ // populate stream_touch_state. Seeding here makes touch processing start working
581
662
  // after bootstraps and restarts without requiring a no-op config update.
582
663
  try {
583
- const rows = this.db.db.query(`SELECT stream, schema_json FROM schemas;`).all() as any[];
584
- for (const row of rows) {
585
- const stream = String(row.stream);
586
- const json = String(row.schema_json ?? "");
587
- let raw: any;
588
- try {
589
- raw = JSON.parse(json);
590
- } catch {
591
- continue;
664
+ for (const kind of listTouchCapableProfileKinds()) {
665
+ const streams = this.db.listStreamsByProfile(kind);
666
+ for (const stream of streams) {
667
+ const profileRes = this.profiles.getProfileResult(stream);
668
+ if (Result.isError(profileRes)) continue;
669
+ const touchCapability = resolveTouchCapability(profileRes.value);
670
+ if (!touchCapability) continue;
671
+ touchCapability.syncState({ db: this.db, stream, profile: profileRes.value });
592
672
  }
593
- const enabled = !!raw?.interpreter?.touch?.enabled;
594
- if (enabled) this.db.ensureStreamInterpreter(stream);
595
- else this.db.deleteStreamInterpreter(stream);
596
673
  }
597
674
  } catch {
598
675
  // ignore
@@ -718,7 +795,7 @@ export class TouchInterpreterManager {
718
795
  scanRowsTotal: number;
719
796
  scanBatchesTotal: number;
720
797
  scannedButEmitted0BatchesTotal: number;
721
- interpretedThroughDeltaTotal: number;
798
+ processedThroughDeltaTotal: number;
722
799
  touchesEmittedTotal: number;
723
800
  touchesTableTotal: number;
724
801
  touchesTemplateTotal: number;
@@ -758,7 +835,7 @@ export class TouchInterpreterManager {
758
835
  scanRowsTotal: totals.scanRowsTotal,
759
836
  scanBatchesTotal: totals.scanBatchesTotal,
760
837
  scannedButEmitted0BatchesTotal: totals.scannedButEmitted0BatchesTotal,
761
- interpretedThroughDeltaTotal: totals.interpretedThroughDeltaTotal,
838
+ processedThroughDeltaTotal: totals.processedThroughDeltaTotal,
762
839
  touchesEmittedTotal: totals.touchesEmittedTotal,
763
840
  touchesTableTotal: totals.touchesTableTotal,
764
841
  touchesTemplateTotal: totals.touchesTemplateTotal,
@@ -823,6 +900,7 @@ export class TouchInterpreterManager {
823
900
  keyIndexMaxKeys: mem.keyIndexMaxKeys ?? 32,
824
901
  });
825
902
  this.journals.set(stream, j);
903
+ this.journalsCreatedTotal += 1;
826
904
  return j;
827
905
  }
828
906
 
@@ -867,13 +945,13 @@ export class TouchInterpreterManager {
867
945
  const cold: string[] = [];
868
946
  for (const stream of ordered) {
869
947
  let hasActiveWaiters = false;
870
- const regRes = this.registry.getRegistryResult(stream);
871
- if (Result.isError(regRes)) {
948
+ const profileRes = this.profiles.getProfileResult(stream);
949
+ if (Result.isError(profileRes)) {
872
950
  hasActiveWaiters = false;
873
951
  } else {
874
- const reg = regRes.value;
875
- if (isTouchEnabled(reg.interpreter)) {
876
- const snap = this.getHotFineSnapshot(stream, reg.interpreter.touch, nowMs);
952
+ const enabledTouch = resolveEnabledTouchCapability(profileRes.value);
953
+ if (enabledTouch) {
954
+ const snap = this.getHotFineSnapshot(stream, enabledTouch.touchCfg, nowMs);
877
955
  hasActiveWaiters = snap.fineWaitersActive + snap.coarseWaitersActive > 0;
878
956
  }
879
957
  }
@@ -1140,7 +1218,7 @@ export class TouchInterpreterManager {
1140
1218
  scanRowsTotal: 0,
1141
1219
  scanBatchesTotal: 0,
1142
1220
  scannedButEmitted0BatchesTotal: 0,
1143
- interpretedThroughDeltaTotal: 0,
1221
+ processedThroughDeltaTotal: 0,
1144
1222
  touchesEmittedTotal: 0,
1145
1223
  touchesTableTotal: 0,
1146
1224
  touchesTemplateTotal: 0,
@@ -2,19 +2,23 @@ import { parentPort, workerData } from "node:worker_threads";
2
2
  import { Result } from "better-result";
3
3
  import type { Config } from "../config.ts";
4
4
  import { SqliteDurableStore } from "../db/db.ts";
5
+ import { resolveEnabledTouchCapability } from "../profiles/index.ts";
6
+ import type { HostRuntime } from "../runtime/host_runtime.ts";
7
+ import { setSqliteRuntimeOverride } from "../sqlite/adapter.ts";
5
8
  import { initConsoleLogging } from "../util/log.ts";
6
9
  import type { ProcessRequest } from "./worker_protocol.ts";
7
- import { interpretRecordToChanges } from "./engine.ts";
8
10
  import { encodeTemplateArg, tableKeyIdFor, templateKeyIdFor, watchKeyIdFor, type TemplateEncoding } from "./live_keys.ts";
9
- import { isTouchEnabled } from "./spec.ts";
10
11
 
11
12
  initConsoleLogging();
12
13
 
13
- const data = workerData as { config: Config };
14
+ const data = workerData as { config: Config; hostRuntime?: HostRuntime };
14
15
  const cfg = data.config;
16
+ // Bun worker_threads can miss the Bun globals that the main thread sees.
17
+ // Use the parent host runtime hint before the worker opens SQLite.
18
+ setSqliteRuntimeOverride(data.hostRuntime ?? null);
15
19
  // The main server process initializes/migrates schema; workers should avoid
16
20
  // concurrent migrations on the same sqlite file.
17
- const db = new SqliteDurableStore(cfg.dbPath, { cacheBytes: cfg.sqliteCacheBytes, skipMigrations: true });
21
+ const db = new SqliteDurableStore(cfg.dbPath, { cacheBytes: cfg.workerSqliteCacheBytes, skipMigrations: true });
18
22
 
19
23
  const decoder = new TextDecoder();
20
24
 
@@ -26,12 +30,12 @@ type ActiveTemplate = {
26
30
  activeFromSourceOffset: bigint;
27
31
  };
28
32
 
29
- type InterpreterWorkerError = { kind: "missing_old_value"; message: string };
33
+ type TouchProcessorWorkerError = { kind: "missing_old_value"; message: string };
30
34
 
31
35
  async function handleProcess(msg: ProcessRequest): Promise<void> {
32
- const { stream, fromOffset, toOffset, interpreter, maxRows, maxBytes } = msg;
36
+ const { stream, fromOffset, toOffset, profile, maxRows, maxBytes } = msg;
33
37
  const failProcess = (message: string): void => {
34
- const err = Result.err<never, InterpreterWorkerError>({ kind: "missing_old_value", message });
38
+ const err = Result.err<never, TouchProcessorWorkerError>({ kind: "missing_old_value", message });
35
39
  parentPort?.postMessage({
36
40
  type: "error",
37
41
  id: msg.id,
@@ -39,22 +43,23 @@ async function handleProcess(msg: ProcessRequest): Promise<void> {
39
43
  message: err.error.message,
40
44
  });
41
45
  };
42
- if (!isTouchEnabled(interpreter)) {
46
+ const enabledTouch = resolveEnabledTouchCapability(profile);
47
+ if (!enabledTouch) {
43
48
  parentPort?.postMessage({
44
49
  type: "error",
45
50
  id: msg.id,
46
51
  stream,
47
- message: "touch not enabled for interpreter",
52
+ message: "touch not enabled for profile",
48
53
  });
49
54
  return;
50
55
  }
51
- const touch = interpreter.touch;
56
+ const { capability: touchCapability, touchCfg: touch } = enabledTouch;
52
57
 
53
58
  const fineBudgetRaw = msg.fineTouchBudget ?? touch.fineTouchBudgetPerBatch;
54
59
  const fineBudget = fineBudgetRaw == null ? null : Math.max(0, Math.floor(fineBudgetRaw));
55
60
  const fineGranularity = msg.fineGranularity === "template" ? "template" : "key";
56
- const interpretMode = msg.interpretMode === "hotTemplatesOnly" ? "hotTemplatesOnly" : "full";
57
- const hotTemplatesOnly = fineGranularity === "template" && interpretMode === "hotTemplatesOnly";
61
+ const processingMode = msg.processingMode === "hotTemplatesOnly" ? "hotTemplatesOnly" : "full";
62
+ const hotTemplatesOnly = fineGranularity === "template" && processingMode === "hotTemplatesOnly";
58
63
 
59
64
  const emitFineTouches = msg.emitFineTouches !== false && fineBudget !== 0;
60
65
  let fineBudgetExhausted = fineBudget != null && fineBudget <= 0;
@@ -217,7 +222,7 @@ async function handleProcess(msg: ProcessRequest): Promise<void> {
217
222
  continue;
218
223
  }
219
224
 
220
- const canonical = interpretRecordToChanges(value, interpreter);
225
+ const canonical = touchCapability.deriveCanonicalChanges(value, profile);
221
226
  changes += canonical.length;
222
227
  if (canonical.length === 0) continue;
223
228
  const watermark = offset.toString();
@@ -299,7 +304,7 @@ async function handleProcess(msg: ProcessRequest): Promise<void> {
299
304
  // Policy for missing/insufficient before image:
300
305
  // - coarse: emit no fine touches (table touch already guarantees correctness)
301
306
  // - skipBefore: emit after-only touch
302
- // - error: fail the interpreter batch
307
+ // - error: fail the processing batch
303
308
  const kAfter = compute(afterObj);
304
309
  const kBefore = compute(beforeObj);
305
310