@prisma/streams-server 0.1.0 → 0.1.2

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.
@@ -1,6 +1,5 @@
1
1
  import type { Config } from "../config";
2
2
  import type { SqliteDurableStore } from "../db/db";
3
- import { STREAM_FLAG_TOUCH } from "../db/db";
4
3
  import type { IngestQueue } from "../ingest";
5
4
  import type { StreamNotifier } from "../notifier";
6
5
  import type { SchemaRegistryStore } from "../schema/registry";
@@ -8,11 +7,9 @@ import { isTouchEnabled } from "./spec";
8
7
  import { TouchInterpreterWorkerPool } from "./worker_pool";
9
8
  import { LruCache } from "../util/lru";
10
9
  import type { BackpressureGate } from "../backpressure";
11
- import { resolveTouchStreamName } from "./naming";
12
10
  import { LiveTemplateRegistry, type TemplateDecl } from "./live_templates";
13
11
  import { LiveMetricsV2 } from "./live_metrics";
14
12
  import type { TouchConfig } from "./spec";
15
- import type { RoutingKeyNotifier } from "./routing_key_notifier";
16
13
  import { TouchJournal } from "./touch_journal";
17
14
  import { Result } from "better-result";
18
15
 
@@ -74,18 +71,12 @@ const HOT_INTEREST_MAX_KEYS = 64;
74
71
 
75
72
  type TouchRecord = {
76
73
  keyId: number;
77
- key?: string;
78
74
  watermark: string;
79
75
  entity: string;
80
76
  kind: "table" | "template";
81
77
  templateId?: string;
82
78
  };
83
79
 
84
- type EnsureTouchStreamError = {
85
- kind: "touch_stream_content_type_mismatch";
86
- message: string;
87
- };
88
-
89
80
  type RestrictedTemplateBucketState = {
90
81
  bucketId: number;
91
82
  templateKeyIds: Set<number>;
@@ -111,22 +102,17 @@ type StreamRuntimeTotals = {
111
102
  export class TouchInterpreterManager {
112
103
  private readonly cfg: Config;
113
104
  private readonly db: SqliteDurableStore;
114
- private readonly ingest: IngestQueue;
115
- private readonly notifier: StreamNotifier;
116
105
  private readonly registry: SchemaRegistryStore;
117
- private readonly backpressure?: BackpressureGate;
118
106
  private readonly pool: TouchInterpreterWorkerPool;
119
107
  private timer: any | null = null;
120
108
  private running = false;
121
109
  private stopping = false;
122
110
  private readonly dirty = new Set<string>();
123
111
  private readonly failures = new FailureTracker(1024);
124
- private readonly lastTrimMs = new LruCache<string, number>(1024);
125
112
  private readonly lastBaseWalGc = new LruCache<string, { atMs: number; through: bigint }>(1024);
126
113
  private readonly templates: LiveTemplateRegistry;
127
114
  private readonly liveMetrics: LiveMetricsV2;
128
115
  private readonly lastTemplateGcMsByStream = new LruCache<string, number>(1024);
129
- private readonly routingKeyNotifier?: RoutingKeyNotifier;
130
116
  private readonly journals = new Map<string, TouchJournal>();
131
117
  private readonly fineLagCoarseOnlyByStream = new Map<string, boolean>();
132
118
  private readonly touchModeByStream = new Map<string, "idle" | "fine" | "restricted" | "coarseOnly">();
@@ -146,26 +132,20 @@ export class TouchInterpreterManager {
146
132
  ingest: IngestQueue,
147
133
  notifier: StreamNotifier,
148
134
  registry: SchemaRegistryStore,
149
- backpressure?: BackpressureGate,
150
- routingKeyNotifier?: RoutingKeyNotifier
135
+ backpressure?: BackpressureGate
151
136
  ) {
152
137
  this.cfg = cfg;
153
138
  this.db = db;
154
- this.ingest = ingest;
155
- this.notifier = notifier;
156
139
  this.registry = registry;
157
- this.backpressure = backpressure;
158
140
  this.pool = new TouchInterpreterWorkerPool(cfg, cfg.interpreterWorkers);
159
141
  this.templates = new LiveTemplateRegistry(db);
160
142
  this.liveMetrics = new LiveMetricsV2(db, ingest, {
161
- routingKeyNotifier,
162
- getTouchJournal: (derivedStream) => {
163
- const j = this.journals.get(derivedStream);
143
+ getTouchJournal: (stream) => {
144
+ const j = this.journals.get(stream);
164
145
  if (!j) return null;
165
146
  return { meta: j.getMeta(), interval: j.snapshotAndResetIntervalStats() };
166
147
  },
167
148
  });
168
- this.routingKeyNotifier = routingKeyNotifier;
169
149
  }
170
150
 
171
151
  start(): void {
@@ -261,33 +241,6 @@ export class TouchInterpreterManager {
261
241
  }
262
242
  }
263
243
 
264
- // Opportunistically enforce touch retention. This keeps internal touch
265
- // WAL bounded even if the touch stream isn't being actively read.
266
- for (const stream of stateByStream.keys()) {
267
- if (this.stopping) break;
268
- const srow = this.db.getStream(stream);
269
- if (!srow || this.db.isDeleted(srow)) continue;
270
- const regRes = this.registry.getRegistryResult(stream);
271
- if (Result.isError(regRes)) {
272
- // eslint-disable-next-line no-console
273
- console.error("touch registry read failed", stream, regRes.error.message);
274
- continue;
275
- }
276
- const reg = regRes.value;
277
- if (!isTouchEnabled(reg.interpreter)) continue;
278
- const derived = resolveTouchStreamName(stream, reg.interpreter.touch);
279
- if ((reg.interpreter.touch.storage ?? "memory") === "sqlite") {
280
- const ensureRes = this.ensureTouchStream(derived);
281
- if (Result.isError(ensureRes)) {
282
- // eslint-disable-next-line no-console
283
- console.error("touch retention stream validation failed", stream, ensureRes.error.message);
284
- continue;
285
- }
286
- const retentionMs = reg.interpreter.touch.retention?.maxAgeMs;
287
- if (retentionMs != null) this.maybeTrimTouchStream(derived, retentionMs);
288
- }
289
- }
290
-
291
244
  // Opportunistically GC base WAL beyond the interpreter checkpoint.
292
245
  //
293
246
  // commitManifest() already GC's on upload, but it can't retroactively GC
@@ -369,7 +322,6 @@ export class TouchInterpreterManager {
369
322
  return;
370
323
  }
371
324
  const touchCfg = reg.interpreter.touch;
372
- const touchStorage = touchCfg.storage ?? "memory";
373
325
  const failProcessing = (message: string): void => {
374
326
  this.failures.recordFailure(stream);
375
327
  this.liveMetrics.recordInterpreterError(stream, touchCfg);
@@ -378,24 +330,19 @@ export class TouchInterpreterManager {
378
330
  };
379
331
 
380
332
  const nowMs = Date.now();
381
- const hotFine = touchStorage === "memory" ? this.getHotFineSnapshot(stream, touchCfg, nowMs) : null;
333
+ const hotFine = this.getHotFineSnapshot(stream, touchCfg, nowMs);
382
334
  const fineWaitersActive = hotFine?.fineWaitersActive ?? 0;
383
335
  const coarseWaitersActive = hotFine?.coarseWaitersActive ?? 0;
384
- const hasAnyWaiters = touchStorage === "memory" ? fineWaitersActive + coarseWaitersActive > 0 : true;
336
+ const hasAnyWaiters = fineWaitersActive + coarseWaitersActive > 0;
385
337
  const hasFineDemand =
386
- touchStorage === "memory"
387
- ? fineWaitersActive > 0 || (hotFine?.broadFineWaitersActive ?? 0) > 0 || (hotFine?.hotKeyCount ?? 0) > 0 || (hotFine?.hotTemplateCount ?? 0) > 0
388
- : true;
338
+ fineWaitersActive > 0 || (hotFine?.broadFineWaitersActive ?? 0) > 0 || (hotFine?.hotKeyCount ?? 0) > 0 || (hotFine?.hotTemplateCount ?? 0) > 0;
389
339
 
390
340
  // Guardrail: when lag/backlog grows too large, temporarily suppress
391
341
  // fine/template touches (coarse table touches are still emitted).
392
342
  const lagAtStart = toOffset >= interpretedThrough ? toOffset - interpretedThrough : 0n;
393
343
  const suppressFineDueToLag = this.computeSuppressFineDueToLag(stream, touchCfg, lagAtStart, hasFineDemand);
394
- if (touchStorage === "memory") {
395
- const derived = resolveTouchStreamName(stream, touchCfg);
396
- const j = this.getOrCreateJournal(derived, touchCfg);
397
- j.setCoalesceMs(this.computeAdaptiveCoalesceMs(touchCfg, lagAtStart, hasAnyWaiters));
398
- }
344
+ const j = this.getOrCreateJournal(stream, touchCfg);
345
+ j.setCoalesceMs(this.computeAdaptiveCoalesceMs(touchCfg, lagAtStart, hasAnyWaiters));
399
346
 
400
347
  const fineBudgetPerBatch = Math.max(0, Math.floor(touchCfg.fineTouchBudgetPerBatch ?? 2000));
401
348
  const lagReservedFineBudgetPerBatch = Math.max(0, Math.floor(touchCfg.lagReservedFineTouchBudgetPerBatch ?? 200));
@@ -480,7 +427,7 @@ export class TouchInterpreterManager {
480
427
  let fineSkippedColdKey = 0;
481
428
  let fineSkippedTemplateBucket = 0;
482
429
 
483
- if (touchStorage === "memory" && hotFine && hotFine.keyFilteringEnabled && fineGranularity !== "template") {
430
+ if (hotFine && hotFine.keyFilteringEnabled && fineGranularity !== "template") {
484
431
  const keyActiveSet = hotFine.hotKeyActiveSet;
485
432
  const keyGraceSet = hotFine.hotKeyGraceSet;
486
433
  const keyCount = (keyActiveSet?.size ?? 0) + (keyGraceSet?.size ?? 0);
@@ -503,76 +450,22 @@ export class TouchInterpreterManager {
503
450
  }
504
451
  }
505
452
 
506
- if (touchStorage === "memory" && fineGranularity === "template" && touches.length > 0) {
453
+ if (fineGranularity === "template" && touches.length > 0) {
507
454
  const coalesced = this.coalesceRestrictedTemplateTouches(stream, touchCfg, touches);
508
455
  touches = coalesced.touches;
509
456
  fineSkippedTemplateBucket = coalesced.dropped;
510
457
  }
511
458
 
512
459
  if (touches.length > 0) {
513
- const derived = res.derivedStream;
514
- if (touchStorage === "sqlite") {
515
- const ensureRes = this.ensureTouchStream(derived);
516
- if (Result.isError(ensureRes)) {
517
- failProcessing(ensureRes.error.message);
518
- return;
519
- }
520
- const encoder = new TextEncoder();
521
- for (const t of touches) {
522
- if (!t.key) {
523
- failProcessing("sqlite touch storage requires routing key strings");
524
- return;
525
- }
526
- }
527
- const rows = touches.map((t) => ({
528
- routingKey: encoder.encode(t.key!),
529
- contentType: "application/json",
530
- payload: encoder.encode(
531
- JSON.stringify({
532
- sourceOffset: t.watermark,
533
- entity: t.entity,
534
- kind: t.kind,
535
- ...(t.kind === "template" && t.templateId ? { templateId: t.templateId } : {}),
536
- })
537
- ),
538
- }));
539
- const appendRes = await this.ingest.appendInternal({
540
- stream: derived,
541
- baseAppendMs: this.db.nowMs(),
542
- rows,
543
- contentType: "application/json",
544
- });
545
- if (Result.isError(appendRes)) {
546
- failProcessing(`touch append failed: ${appendRes.error.kind}`);
547
- return;
548
- }
549
- if (appendRes.value.appendedRows > 0) {
550
- this.notifier.notify(derived, appendRes.value.lastOffset);
551
- if (this.routingKeyNotifier) {
552
- const appended = appendRes.value.appendedRows;
553
- const start = appendRes.value.lastOffset - BigInt(appended) + 1n;
554
- let keys = touches.map((t) => t.key ?? "");
555
- // Be defensive: only notify for the rows we actually appended.
556
- if (keys.length !== appended) keys = keys.slice(Math.max(0, keys.length - appended));
557
- let seq = start;
558
- for (const k of keys) {
559
- if (!k) continue;
560
- this.routingKeyNotifier.notify(derived, k, seq);
561
- seq += 1n;
562
- }
563
- }
564
- }
565
- } else {
566
- const j = this.getOrCreateJournal(derived, touchCfg);
567
- for (const t of touches) {
568
- let sourceOffsetSeq: bigint | undefined;
569
- try {
570
- sourceOffsetSeq = BigInt(t.watermark);
571
- } catch {
572
- sourceOffsetSeq = undefined;
573
- }
574
- j.touch(t.keyId >>> 0, sourceOffsetSeq);
460
+ const j = this.getOrCreateJournal(stream, touchCfg);
461
+ for (const t of touches) {
462
+ let sourceOffsetSeq: bigint | undefined;
463
+ try {
464
+ sourceOffsetSeq = BigInt(t.watermark);
465
+ } catch {
466
+ sourceOffsetSeq = undefined;
575
467
  }
468
+ j.touch(t.keyId >>> 0, sourceOffsetSeq);
576
469
  }
577
470
  }
578
471
 
@@ -649,38 +542,6 @@ export class TouchInterpreterManager {
649
542
  this.failures.recordSuccess(stream);
650
543
  }
651
544
 
652
- private ensureTouchStream(stream: string): Result<void, EnsureTouchStreamError> {
653
- const existing = this.db.getStream(stream);
654
- if (existing) {
655
- if (String(existing.content_type) !== "application/json") {
656
- return Result.err({
657
- kind: "touch_stream_content_type_mismatch",
658
- message: `touch stream content-type mismatch: ${existing.content_type}`,
659
- });
660
- }
661
- if ((existing.stream_flags & STREAM_FLAG_TOUCH) === 0) this.db.addStreamFlags(stream, STREAM_FLAG_TOUCH);
662
- return Result.ok(undefined);
663
- }
664
- this.db.ensureStream(stream, { contentType: "application/json", streamFlags: STREAM_FLAG_TOUCH });
665
- return Result.ok(undefined);
666
- }
667
-
668
- private maybeTrimTouchStream(stream: string, maxAgeMs: number): void {
669
- const now = Date.now();
670
- const last = this.lastTrimMs.get(stream) ?? 0;
671
- // Throttle trims; tick can run frequently.
672
- if (now - last < 10_000) return;
673
- this.lastTrimMs.set(stream, now);
674
-
675
- try {
676
- const res = this.db.trimWalByAge(stream, maxAgeMs);
677
- if (res.trimmedBytes > 0 && this.backpressure) this.backpressure.adjustOnWalTrim(res.trimmedBytes);
678
- } catch (e) {
679
- // eslint-disable-next-line no-console
680
- console.error("touch retention trim failed", stream, e);
681
- }
682
- }
683
-
684
545
  private maybeGcBaseWal(stream: string, uploadedThrough: bigint, interpretedThrough: bigint): void {
685
546
  const gcTargetThrough = interpretedThrough < uploadedThrough ? interpretedThrough : uploadedThrough;
686
547
  if (gcTargetThrough < 0n) return;
@@ -784,8 +645,6 @@ export class TouchInterpreterManager {
784
645
  templateIdsUsed: string[];
785
646
  interestMode: "fine" | "coarse";
786
647
  }): () => void {
787
- if ((args.touchCfg.storage ?? "memory") !== "memory") return () => {};
788
-
789
648
  const nowMs = Date.now();
790
649
  const limits = this.getHotFineLimits(args.touchCfg);
791
650
  const state = this.getOrCreateHotFineState(args.stream);
@@ -878,10 +737,9 @@ export class TouchInterpreterManager {
878
737
  journalTimeoutSweepMsTotal: number;
879
738
  } {
880
739
  const nowMs = Date.now();
881
- const hot = (args.touchCfg.storage ?? "memory") === "memory" ? this.getHotFineSnapshot(args.stream, args.touchCfg, nowMs) : null;
740
+ const hot = this.getHotFineSnapshot(args.stream, args.touchCfg, nowMs);
882
741
  const totals = this.getOrCreateRuntimeTotals(args.stream);
883
- const derived = resolveTouchStreamName(args.stream, args.touchCfg);
884
- const journal = (args.touchCfg.storage ?? "memory") === "memory" ? this.journals.get(derived) ?? null : null;
742
+ const journal = this.journals.get(args.stream) ?? null;
885
743
  const journalTotals = journal?.getTotalStats();
886
744
  return {
887
745
  lagSourceOffsets: this.lagSourceOffsetsByStream.get(args.stream) ?? 0,
@@ -953,8 +811,8 @@ export class TouchInterpreterManager {
953
811
  return Array.from(entities);
954
812
  }
955
813
 
956
- getOrCreateJournal(derivedStream: string, touchCfg: TouchConfig): TouchJournal {
957
- const existing = this.journals.get(derivedStream);
814
+ getOrCreateJournal(stream: string, touchCfg: TouchConfig): TouchJournal {
815
+ const existing = this.journals.get(stream);
958
816
  if (existing) return existing;
959
817
  const mem = touchCfg.memory ?? {};
960
818
  const j = new TouchJournal({
@@ -964,7 +822,7 @@ export class TouchInterpreterManager {
964
822
  pendingMaxKeys: mem.pendingMaxKeys ?? 100_000,
965
823
  keyIndexMaxKeys: mem.keyIndexMaxKeys ?? 32,
966
824
  });
967
- this.journals.set(derivedStream, j);
825
+ this.journals.set(stream, j);
968
826
  return j;
969
827
  }
970
828
 
@@ -978,8 +836,8 @@ export class TouchInterpreterManager {
978
836
  return maxCoalesceMs;
979
837
  }
980
838
 
981
- getJournalIfExists(derivedStream: string): TouchJournal | null {
982
- return this.journals.get(derivedStream) ?? null;
839
+ getJournalIfExists(stream: string): TouchJournal | null {
840
+ return this.journals.get(stream) ?? null;
983
841
  }
984
842
 
985
843
  private computeSuppressFineDueToLag(stream: string, touchCfg: TouchConfig, lagAtStart: bigint, hasFineDemand: boolean): boolean {
@@ -1014,7 +872,7 @@ export class TouchInterpreterManager {
1014
872
  hasActiveWaiters = false;
1015
873
  } else {
1016
874
  const reg = regRes.value;
1017
- if (isTouchEnabled(reg.interpreter) && (reg.interpreter.touch.storage ?? "memory") === "memory") {
875
+ if (isTouchEnabled(reg.interpreter)) {
1018
876
  const snap = this.getHotFineSnapshot(stream, reg.interpreter.touch, nowMs);
1019
877
  hasActiveWaiters = snap.fineWaitersActive + snap.coarseWaitersActive > 0;
1020
878
  }
package/src/touch/spec.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Result } from "better-result";
2
- import { parseDurationMsResult } from "../util/duration.ts";
3
2
  import { dsError } from "../util/ds_error.ts";
4
3
 
5
4
  export type StreamInterpreterConfig = {
@@ -15,22 +14,6 @@ export type StreamInterpreterConfigValidationError = {
15
14
 
16
15
  export type TouchConfig = {
17
16
  enabled: boolean;
18
- /**
19
- * Touch storage backend.
20
- *
21
- * - memory: in-process, lossy (false positives allowed), epoch-scoped journal.
22
- * - sqlite: derived companion stream in SQLite WAL (legacy).
23
- *
24
- * Default: memory.
25
- */
26
- storage?: "memory" | "sqlite";
27
- /**
28
- * Advanced override. When unset, the server exposes touches as a companion of
29
- * the source stream at `/v1/stream/<name>/touch` and uses an internal derived
30
- * stream name.
31
- */
32
- derivedStream?: string;
33
- retention?: { maxAgeMs: number };
34
17
  /**
35
18
  * Coarse invalidation interval. The server emits at most one table-touch per
36
19
  * entity per interval.
@@ -209,72 +192,22 @@ function parseIntegerField(
209
192
  return Result.ok(n);
210
193
  }
211
194
 
212
- function validateRetentionResult(
213
- raw: any
214
- ): Result<{ maxAgeMs: number } | undefined, StreamInterpreterConfigValidationError> {
215
- if (raw == null) return Result.ok(undefined);
216
- if (!raw || typeof raw !== "object") return invalidInterpreter("interpreter.touch.retention must be an object");
217
- const maxAge = raw.maxAge;
218
- const maxAgeMsRaw = raw.maxAgeMs;
219
- if (maxAge !== undefined && maxAgeMsRaw !== undefined) {
220
- return invalidInterpreter("interpreter.touch.retention must specify only one of maxAge|maxAgeMs");
221
- }
222
- let ms: number | null = null;
223
- if (maxAgeMsRaw !== undefined) {
224
- if (typeof maxAgeMsRaw !== "number" || !Number.isFinite(maxAgeMsRaw) || maxAgeMsRaw < 0) {
225
- return invalidInterpreter("interpreter.touch.retention.maxAgeMs must be a non-negative number");
226
- }
227
- ms = maxAgeMsRaw;
228
- } else if (maxAge !== undefined) {
229
- if (typeof maxAge !== "string" || maxAge.trim() === "") {
230
- return invalidInterpreter("interpreter.touch.retention.maxAge must be a non-empty duration string");
231
- }
232
- const durationRes = parseDurationMsResult(maxAge);
233
- if (Result.isError(durationRes)) {
234
- return invalidInterpreter(`interpreter.touch.retention.maxAge ${durationRes.error.message}`);
235
- }
236
- ms = durationRes.value;
237
- if (ms < 0) ms = 0;
238
- } else {
239
- return invalidInterpreter("interpreter.touch.retention must include maxAge or maxAgeMs");
240
- }
241
- return Result.ok({ maxAgeMs: ms });
242
- }
243
-
244
195
  function validateTouchConfigResult(raw: any): Result<TouchConfig, StreamInterpreterConfigValidationError> {
245
196
  if (!raw || typeof raw !== "object") return invalidInterpreter("interpreter.touch must be an object");
246
197
  const enabled = !!raw.enabled;
247
198
  if (!enabled) {
248
- return Result.ok({
249
- enabled: false,
250
- storage: undefined,
251
- derivedStream: undefined,
252
- retention: undefined,
253
- });
199
+ return Result.ok({ enabled: false });
254
200
  }
255
201
 
256
- const storageRaw = raw.storage === undefined ? "memory" : String(raw.storage).trim();
257
- if (storageRaw !== "memory" && storageRaw !== "sqlite") {
258
- return invalidInterpreter("interpreter.touch.storage must be memory|sqlite");
202
+ if (raw.storage !== undefined) {
203
+ return invalidInterpreter("interpreter.touch.storage is no longer supported; touch always uses the in-memory journal");
259
204
  }
260
- const storage = storageRaw as "memory" | "sqlite";
261
-
262
- const derivedStream =
263
- raw.derivedStream === undefined
264
- ? undefined
265
- : typeof raw.derivedStream === "string" && raw.derivedStream.trim() !== ""
266
- ? raw.derivedStream
267
- : null;
268
- if (derivedStream === null) {
269
- return invalidInterpreter("interpreter.touch.derivedStream must be a non-empty string when provided");
205
+ if (raw.derivedStream !== undefined) {
206
+ return invalidInterpreter("interpreter.touch.derivedStream is no longer supported");
207
+ }
208
+ if (raw.retention !== undefined) {
209
+ return invalidInterpreter("interpreter.touch.retention is no longer supported");
270
210
  }
271
-
272
- // Touch companions are intended as short-retention invalidation journals.
273
- // Default to 24h to prevent unbounded growth if retention is omitted.
274
- const retentionRaw = raw.retention;
275
- const retentionRes = retentionRaw === undefined ? Result.ok({ maxAgeMs: 24 * 60 * 60 * 1000 }) : validateRetentionResult(retentionRaw);
276
- if (Result.isError(retentionRes)) return invalidInterpreter(retentionRes.error.message);
277
- const retention = retentionRes.value;
278
211
 
279
212
  const coarseIntervalMsRes = parseNumberField(
280
213
  raw.coarseIntervalMs,
@@ -454,9 +387,6 @@ function validateTouchConfigResult(raw: any): Result<TouchConfig, StreamInterpre
454
387
 
455
388
  return Result.ok({
456
389
  enabled: true,
457
- storage,
458
- derivedStream,
459
- retention,
460
390
  coarseIntervalMs: coarseIntervalMsRes.value,
461
391
  touchCoalesceWindowMs: touchCoalesceWindowMsRes.value,
462
392
  onMissingBefore,
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { Worker } from "node:worker_threads";
5
5
  import { Result } from "better-result";
6
6
  import type { Config } from "../config";
7
+ import { detectHostRuntime } from "../runtime/host_runtime.ts";
7
8
  import type { ProcessRequest, ProcessResult, WorkerMessage } from "./worker_protocol";
8
9
  import { dsError } from "../util/ds_error.ts";
9
10
 
@@ -123,7 +124,7 @@ export class TouchInterpreterWorkerPool {
123
124
  }
124
125
 
125
126
  const worker = new Worker(workerSpec, {
126
- workerData: { config: this.cfg },
127
+ workerData: { config: this.cfg, hostRuntime: detectHostRuntime() },
127
128
  type: "module",
128
129
  smol: true,
129
130
  } as any);
@@ -2,7 +2,6 @@ import type { StreamInterpreterConfig } from "./spec.ts";
2
2
 
3
3
  export type TouchRow = {
4
4
  keyId: number;
5
- key?: string;
6
5
  watermark: string; // source stream offset (base-10 string)
7
6
  entity: string;
8
7
  kind: "table" | "template";
@@ -30,7 +29,6 @@ export type ProcessResult = {
30
29
  type: "result";
31
30
  id: number;
32
31
  stream: string;
33
- derivedStream: string;
34
32
  processedThrough: bigint;
35
33
  touches: TouchRow[];
36
34
  stats: {
@@ -1,13 +0,0 @@
1
- import type { TouchConfig } from "./spec.ts";
2
-
3
- const DEFAULT_TOUCH_SUFFIX = ".__touch";
4
-
5
- export function defaultTouchStreamName(sourceStream: string): string {
6
- return `${sourceStream}${DEFAULT_TOUCH_SUFFIX}`;
7
- }
8
-
9
- export function resolveTouchStreamName(sourceStream: string, touch: TouchConfig): string {
10
- const override = touch.derivedStream;
11
- if (override && override.trim() !== "") return override;
12
- return defaultTouchStreamName(sourceStream);
13
- }