@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
package/src/touch/spec.ts CHANGED
@@ -1,17 +1,27 @@
1
1
  import { Result } from "better-result";
2
2
  import { dsError } from "../util/ds_error.ts";
3
3
 
4
- export type StreamInterpreterConfig = {
5
- apiVersion: "durable.streams/stream-interpreter/v1";
6
- format?: "durable.streams/state-protocol/v1";
7
- touch?: TouchConfig;
8
- };
9
-
10
- export type StreamInterpreterConfigValidationError = {
11
- kind: "invalid_interpreter";
4
+ export type TouchConfigValidationError = {
5
+ kind: "invalid_touch";
12
6
  message: string;
13
7
  };
14
8
 
9
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
10
+ return !!value && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ function rejectUnknownKeysResult(
14
+ obj: Record<string, unknown>,
15
+ allowed: readonly string[],
16
+ path: string
17
+ ): Result<void, TouchConfigValidationError> {
18
+ const allowedSet = new Set(allowed);
19
+ for (const key of Object.keys(obj)) {
20
+ if (!allowedSet.has(key)) return invalidTouch(`${path}.${key} is not supported`);
21
+ }
22
+ return Result.ok(undefined);
23
+ }
24
+
15
25
  export type TouchConfig = {
16
26
  enabled: boolean;
17
27
  /**
@@ -32,12 +42,12 @@ export type TouchConfig = {
32
42
  *
33
43
  * - coarse: emit coarse table touches only (safe default)
34
44
  * - skipBefore: compute fine touches from `value` only
35
- * - error: interpreter errors (useful for strict debugging)
45
+ * - error: processing errors (useful for strict debugging)
36
46
  */
37
47
  onMissingBefore?: "coarse" | "skipBefore" | "error";
38
48
  /**
39
- * Optional guardrail: when the interpreter backlog (source offsets behind the tail)
40
- * exceeds this threshold, the interpreter will emit coarse table touches only
49
+ * Optional guardrail: when the touch-processing backlog (source offsets behind the tail)
50
+ * exceeds this threshold, the processor will emit coarse table touches only
41
51
  * (fine/template touches are suppressed) to preserve timeliness under overload.
42
52
  *
43
53
  * Default: 5000.
@@ -53,7 +63,7 @@ export type TouchConfig = {
53
63
  */
54
64
  lagRecoverFineTouchesAtSourceOffsets?: number;
55
65
  /**
56
- * Optional guardrail: cap fine/template touches emitted per interpreter batch.
66
+ * Optional guardrail: cap fine/template touches emitted per processing batch.
57
67
  * Table touches are always emitted for correctness.
58
68
  *
59
69
  * Default: 2000.
@@ -79,7 +89,7 @@ export type TouchConfig = {
79
89
  */
80
90
  lagReservedFineTouchBudgetPerBatch?: number;
81
91
  /**
82
- * Memory-only touch journal parameters. Only used when storage="memory".
92
+ * In-memory touch journal parameters.
83
93
  */
84
94
  memory?: {
85
95
  /**
@@ -166,8 +176,8 @@ export type TouchConfig = {
166
176
  };
167
177
  };
168
178
 
169
- function invalidInterpreter<T = never>(message: string): Result<T, StreamInterpreterConfigValidationError> {
170
- return Result.err({ kind: "invalid_interpreter", message });
179
+ function invalidTouch<T = never>(message: string): Result<T, TouchConfigValidationError> {
180
+ return Result.err({ kind: "invalid_touch", message });
171
181
  }
172
182
 
173
183
  function parseNumberField(
@@ -175,9 +185,9 @@ function parseNumberField(
175
185
  defaultValue: number,
176
186
  message: string,
177
187
  predicate: (n: number) => boolean
178
- ): Result<number, StreamInterpreterConfigValidationError> {
188
+ ): Result<number, TouchConfigValidationError> {
179
189
  const n = value === undefined ? defaultValue : Number(value);
180
- if (!Number.isFinite(n) || !predicate(n)) return invalidInterpreter(message);
190
+ if (!Number.isFinite(n) || !predicate(n)) return invalidTouch(message);
181
191
  return Result.ok(n);
182
192
  }
183
193
 
@@ -186,158 +196,178 @@ function parseIntegerField(
186
196
  defaultValue: number,
187
197
  message: string,
188
198
  predicate: (n: number) => boolean
189
- ): Result<number, StreamInterpreterConfigValidationError> {
199
+ ): Result<number, TouchConfigValidationError> {
190
200
  const n = value === undefined ? defaultValue : Number(value);
191
- if (!Number.isFinite(n) || !Number.isInteger(n) || !predicate(n)) return invalidInterpreter(message);
201
+ if (!Number.isFinite(n) || !Number.isInteger(n) || !predicate(n)) return invalidTouch(message);
192
202
  return Result.ok(n);
193
203
  }
194
204
 
195
- function validateTouchConfigResult(raw: any): Result<TouchConfig, StreamInterpreterConfigValidationError> {
196
- if (!raw || typeof raw !== "object") return invalidInterpreter("interpreter.touch must be an object");
205
+ export function validateTouchConfigResult(raw: any, fieldPath = "touch"): Result<TouchConfig, TouchConfigValidationError> {
206
+ if (!isPlainObject(raw)) return invalidTouch(`${fieldPath} must be an object`);
207
+ const topLevelCheck = rejectUnknownKeysResult(
208
+ raw,
209
+ [
210
+ "enabled",
211
+ "coarseIntervalMs",
212
+ "touchCoalesceWindowMs",
213
+ "onMissingBefore",
214
+ "lagDegradeFineTouchesAtSourceOffsets",
215
+ "lagRecoverFineTouchesAtSourceOffsets",
216
+ "fineTouchBudgetPerBatch",
217
+ "fineTokensPerSecond",
218
+ "fineBurstTokens",
219
+ "lagReservedFineTouchBudgetPerBatch",
220
+ "memory",
221
+ "templates",
222
+ ],
223
+ fieldPath
224
+ );
225
+ if (Result.isError(topLevelCheck)) return topLevelCheck;
226
+
197
227
  const enabled = !!raw.enabled;
198
228
  if (!enabled) {
199
229
  return Result.ok({ enabled: false });
200
230
  }
201
231
 
202
- if (raw.storage !== undefined) {
203
- return invalidInterpreter("interpreter.touch.storage is no longer supported; touch always uses the in-memory journal");
204
- }
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");
210
- }
211
-
212
232
  const coarseIntervalMsRes = parseNumberField(
213
233
  raw.coarseIntervalMs,
214
234
  100,
215
- "interpreter.touch.coarseIntervalMs must be > 0",
235
+ `${fieldPath}.coarseIntervalMs must be > 0`,
216
236
  (n) => n > 0
217
237
  );
218
238
  if (Result.isError(coarseIntervalMsRes)) return coarseIntervalMsRes;
219
239
  const touchCoalesceWindowMsRes = parseNumberField(
220
240
  raw.touchCoalesceWindowMs,
221
241
  100,
222
- "interpreter.touch.touchCoalesceWindowMs must be > 0",
242
+ `${fieldPath}.touchCoalesceWindowMs must be > 0`,
223
243
  (n) => n > 0
224
244
  );
225
245
  if (Result.isError(touchCoalesceWindowMsRes)) return touchCoalesceWindowMsRes;
226
246
 
227
247
  const onMissingBefore = raw.onMissingBefore === undefined ? "coarse" : raw.onMissingBefore;
228
248
  if (onMissingBefore !== "coarse" && onMissingBefore !== "skipBefore" && onMissingBefore !== "error") {
229
- return invalidInterpreter("interpreter.touch.onMissingBefore must be coarse|skipBefore|error");
249
+ return invalidTouch(`${fieldPath}.onMissingBefore must be coarse|skipBefore|error`);
230
250
  }
231
251
 
232
- const templates = raw.templates && typeof raw.templates === "object" ? raw.templates : {};
252
+ const templates = raw.templates === undefined ? {} : isPlainObject(raw.templates) ? raw.templates : null;
253
+ if (templates == null) return invalidTouch(`${fieldPath}.templates must be an object`);
254
+ const templatesCheck = rejectUnknownKeysResult(
255
+ templates,
256
+ ["defaultInactivityTtlMs", "lastSeenPersistIntervalMs", "gcIntervalMs", "maxActiveTemplatesPerEntity", "maxActiveTemplatesPerStream", "activationRateLimitPerMinute"],
257
+ `${fieldPath}.templates`
258
+ );
259
+ if (Result.isError(templatesCheck)) return templatesCheck;
233
260
  const defaultInactivityTtlMsRes = parseNumberField(
234
261
  templates.defaultInactivityTtlMs,
235
262
  60 * 60 * 1000,
236
- "interpreter.touch.templates.defaultInactivityTtlMs must be >= 0",
263
+ `${fieldPath}.templates.defaultInactivityTtlMs must be >= 0`,
237
264
  (n) => n >= 0
238
265
  );
239
266
  if (Result.isError(defaultInactivityTtlMsRes)) return defaultInactivityTtlMsRes;
240
267
  const lastSeenPersistIntervalMsRes = parseNumberField(
241
268
  templates.lastSeenPersistIntervalMs,
242
269
  5 * 60 * 1000,
243
- "interpreter.touch.templates.lastSeenPersistIntervalMs must be > 0",
270
+ `${fieldPath}.templates.lastSeenPersistIntervalMs must be > 0`,
244
271
  (n) => n > 0
245
272
  );
246
273
  if (Result.isError(lastSeenPersistIntervalMsRes)) return lastSeenPersistIntervalMsRes;
247
274
  const gcIntervalMsRes = parseNumberField(
248
275
  templates.gcIntervalMs,
249
276
  60_000,
250
- "interpreter.touch.templates.gcIntervalMs must be > 0",
277
+ `${fieldPath}.templates.gcIntervalMs must be > 0`,
251
278
  (n) => n > 0
252
279
  );
253
280
  if (Result.isError(gcIntervalMsRes)) return gcIntervalMsRes;
254
281
  const maxActiveTemplatesPerEntityRes = parseNumberField(
255
282
  templates.maxActiveTemplatesPerEntity,
256
283
  256,
257
- "interpreter.touch.templates.maxActiveTemplatesPerEntity must be > 0",
284
+ `${fieldPath}.templates.maxActiveTemplatesPerEntity must be > 0`,
258
285
  (n) => n > 0
259
286
  );
260
287
  if (Result.isError(maxActiveTemplatesPerEntityRes)) return maxActiveTemplatesPerEntityRes;
261
288
  const maxActiveTemplatesPerStreamRes = parseNumberField(
262
289
  templates.maxActiveTemplatesPerStream,
263
290
  2048,
264
- "interpreter.touch.templates.maxActiveTemplatesPerStream must be > 0",
291
+ `${fieldPath}.templates.maxActiveTemplatesPerStream must be > 0`,
265
292
  (n) => n > 0
266
293
  );
267
294
  if (Result.isError(maxActiveTemplatesPerStreamRes)) return maxActiveTemplatesPerStreamRes;
268
295
  const activationRateLimitPerMinuteRes = parseNumberField(
269
296
  templates.activationRateLimitPerMinute,
270
297
  100,
271
- "interpreter.touch.templates.activationRateLimitPerMinute must be >= 0",
298
+ `${fieldPath}.templates.activationRateLimitPerMinute must be >= 0`,
272
299
  (n) => n >= 0
273
300
  );
274
301
  if (Result.isError(activationRateLimitPerMinuteRes)) return activationRateLimitPerMinuteRes;
275
302
 
276
- if (raw.metrics !== undefined) {
277
- return invalidInterpreter("interpreter.touch.metrics is not supported; live metrics are a global server feature");
278
- }
279
-
280
- const memoryRaw = raw.memory && typeof raw.memory === "object" ? raw.memory : {};
303
+ const memoryRaw = raw.memory === undefined ? {} : isPlainObject(raw.memory) ? raw.memory : null;
304
+ if (memoryRaw == null) return invalidTouch(`${fieldPath}.memory must be an object`);
305
+ const memoryCheck = rejectUnknownKeysResult(
306
+ memoryRaw,
307
+ ["bucketMs", "filterPow2", "k", "pendingMaxKeys", "keyIndexMaxKeys", "hotKeyTtlMs", "hotTemplateTtlMs", "hotMaxKeys", "hotMaxTemplates"],
308
+ `${fieldPath}.memory`
309
+ );
310
+ if (Result.isError(memoryCheck)) return memoryCheck;
281
311
  const bucketMsRes = parseIntegerField(
282
312
  memoryRaw.bucketMs,
283
313
  100,
284
- "interpreter.touch.memory.bucketMs must be an integer > 0",
314
+ `${fieldPath}.memory.bucketMs must be an integer > 0`,
285
315
  (n) => n > 0
286
316
  );
287
317
  if (Result.isError(bucketMsRes)) return bucketMsRes;
288
318
  const filterPow2Res = parseIntegerField(
289
319
  memoryRaw.filterPow2,
290
320
  22,
291
- "interpreter.touch.memory.filterPow2 must be an integer in [10,30]",
321
+ `${fieldPath}.memory.filterPow2 must be an integer in [10,30]`,
292
322
  (n) => n >= 10 && n <= 30
293
323
  );
294
324
  if (Result.isError(filterPow2Res)) return filterPow2Res;
295
325
  const kRes = parseIntegerField(
296
326
  memoryRaw.k,
297
327
  4,
298
- "interpreter.touch.memory.k must be an integer in [1,8]",
328
+ `${fieldPath}.memory.k must be an integer in [1,8]`,
299
329
  (n) => n >= 1 && n <= 8
300
330
  );
301
331
  if (Result.isError(kRes)) return kRes;
302
332
  const pendingMaxKeysRes = parseIntegerField(
303
333
  memoryRaw.pendingMaxKeys,
304
334
  100_000,
305
- "interpreter.touch.memory.pendingMaxKeys must be an integer > 0",
335
+ `${fieldPath}.memory.pendingMaxKeys must be an integer > 0`,
306
336
  (n) => n > 0
307
337
  );
308
338
  if (Result.isError(pendingMaxKeysRes)) return pendingMaxKeysRes;
309
339
  const keyIndexMaxKeysRes = parseIntegerField(
310
340
  memoryRaw.keyIndexMaxKeys,
311
341
  32,
312
- "interpreter.touch.memory.keyIndexMaxKeys must be an integer in [1,1024]",
342
+ `${fieldPath}.memory.keyIndexMaxKeys must be an integer in [1,1024]`,
313
343
  (n) => n >= 1 && n <= 1024
314
344
  );
315
345
  if (Result.isError(keyIndexMaxKeysRes)) return keyIndexMaxKeysRes;
316
346
  const hotKeyTtlMsRes = parseIntegerField(
317
347
  memoryRaw.hotKeyTtlMs,
318
348
  10_000,
319
- "interpreter.touch.memory.hotKeyTtlMs must be an integer > 0",
349
+ `${fieldPath}.memory.hotKeyTtlMs must be an integer > 0`,
320
350
  (n) => n > 0
321
351
  );
322
352
  if (Result.isError(hotKeyTtlMsRes)) return hotKeyTtlMsRes;
323
353
  const hotTemplateTtlMsRes = parseIntegerField(
324
354
  memoryRaw.hotTemplateTtlMs,
325
355
  10_000,
326
- "interpreter.touch.memory.hotTemplateTtlMs must be an integer > 0",
356
+ `${fieldPath}.memory.hotTemplateTtlMs must be an integer > 0`,
327
357
  (n) => n > 0
328
358
  );
329
359
  if (Result.isError(hotTemplateTtlMsRes)) return hotTemplateTtlMsRes;
330
360
  const hotMaxKeysRes = parseIntegerField(
331
361
  memoryRaw.hotMaxKeys,
332
362
  1_000_000,
333
- "interpreter.touch.memory.hotMaxKeys must be an integer > 0",
363
+ `${fieldPath}.memory.hotMaxKeys must be an integer > 0`,
334
364
  (n) => n > 0
335
365
  );
336
366
  if (Result.isError(hotMaxKeysRes)) return hotMaxKeysRes;
337
367
  const hotMaxTemplatesRes = parseIntegerField(
338
368
  memoryRaw.hotMaxTemplates,
339
369
  4096,
340
- "interpreter.touch.memory.hotMaxTemplates must be an integer > 0",
370
+ `${fieldPath}.memory.hotMaxTemplates must be an integer > 0`,
341
371
  (n) => n > 0
342
372
  );
343
373
  if (Result.isError(hotMaxTemplatesRes)) return hotMaxTemplatesRes;
@@ -345,42 +375,42 @@ function validateTouchConfigResult(raw: any): Result<TouchConfig, StreamInterpre
345
375
  const lagDegradeFineTouchesAtSourceOffsetsRes = parseIntegerField(
346
376
  raw.lagDegradeFineTouchesAtSourceOffsets,
347
377
  5000,
348
- "interpreter.touch.lagDegradeFineTouchesAtSourceOffsets must be an integer >= 0",
378
+ `${fieldPath}.lagDegradeFineTouchesAtSourceOffsets must be an integer >= 0`,
349
379
  (n) => n >= 0
350
380
  );
351
381
  if (Result.isError(lagDegradeFineTouchesAtSourceOffsetsRes)) return lagDegradeFineTouchesAtSourceOffsetsRes;
352
382
  const lagRecoverFineTouchesAtSourceOffsetsRes = parseIntegerField(
353
383
  raw.lagRecoverFineTouchesAtSourceOffsets,
354
384
  1000,
355
- "interpreter.touch.lagRecoverFineTouchesAtSourceOffsets must be an integer >= 0",
385
+ `${fieldPath}.lagRecoverFineTouchesAtSourceOffsets must be an integer >= 0`,
356
386
  (n) => n >= 0
357
387
  );
358
388
  if (Result.isError(lagRecoverFineTouchesAtSourceOffsetsRes)) return lagRecoverFineTouchesAtSourceOffsetsRes;
359
389
  const fineTouchBudgetPerBatchRes = parseIntegerField(
360
390
  raw.fineTouchBudgetPerBatch,
361
391
  2000,
362
- "interpreter.touch.fineTouchBudgetPerBatch must be an integer >= 0",
392
+ `${fieldPath}.fineTouchBudgetPerBatch must be an integer >= 0`,
363
393
  (n) => n >= 0
364
394
  );
365
395
  if (Result.isError(fineTouchBudgetPerBatchRes)) return fineTouchBudgetPerBatchRes;
366
396
  const fineTokensPerSecondRes = parseIntegerField(
367
397
  raw.fineTokensPerSecond,
368
398
  200_000,
369
- "interpreter.touch.fineTokensPerSecond must be an integer >= 0",
399
+ `${fieldPath}.fineTokensPerSecond must be an integer >= 0`,
370
400
  (n) => n >= 0
371
401
  );
372
402
  if (Result.isError(fineTokensPerSecondRes)) return fineTokensPerSecondRes;
373
403
  const fineBurstTokensRes = parseIntegerField(
374
404
  raw.fineBurstTokens,
375
405
  400_000,
376
- "interpreter.touch.fineBurstTokens must be an integer >= 0",
406
+ `${fieldPath}.fineBurstTokens must be an integer >= 0`,
377
407
  (n) => n >= 0
378
408
  );
379
409
  if (Result.isError(fineBurstTokensRes)) return fineBurstTokensRes;
380
410
  const lagReservedFineTouchBudgetPerBatchRes = parseIntegerField(
381
411
  raw.lagReservedFineTouchBudgetPerBatch,
382
412
  200,
383
- "interpreter.touch.lagReservedFineTouchBudgetPerBatch must be an integer >= 0",
413
+ `${fieldPath}.lagReservedFineTouchBudgetPerBatch must be an integer >= 0`,
384
414
  (n) => n >= 0
385
415
  );
386
416
  if (Result.isError(lagReservedFineTouchBudgetPerBatchRes)) return lagReservedFineTouchBudgetPerBatchRes;
@@ -418,39 +448,12 @@ function validateTouchConfigResult(raw: any): Result<TouchConfig, StreamInterpre
418
448
  });
419
449
  }
420
450
 
421
- export function validateStreamInterpreterConfigResult(
422
- raw: any
423
- ): Result<StreamInterpreterConfig, StreamInterpreterConfigValidationError> {
424
- if (!raw || typeof raw !== "object") return invalidInterpreter("interpreter must be an object");
425
- if (raw.apiVersion !== "durable.streams/stream-interpreter/v1") {
426
- return invalidInterpreter("invalid interpreter apiVersion");
427
- }
428
- const formatRaw = raw.format === undefined ? undefined : raw.format;
429
- if (formatRaw !== undefined && formatRaw !== "durable.streams/state-protocol/v1") {
430
- return invalidInterpreter("interpreter.format must be durable.streams/state-protocol/v1");
431
- }
432
- if (raw.variants !== undefined) {
433
- return invalidInterpreter("interpreter.variants is not supported (State Protocol is the only supported format)");
434
- }
435
- let touch: TouchConfig | undefined;
436
- if (raw.touch !== undefined) {
437
- const touchRes = validateTouchConfigResult(raw.touch);
438
- if (Result.isError(touchRes)) return invalidInterpreter(touchRes.error.message);
439
- touch = touchRes.value;
440
- }
441
- return Result.ok({
442
- apiVersion: "durable.streams/stream-interpreter/v1",
443
- format: "durable.streams/state-protocol/v1",
444
- touch,
445
- });
446
- }
447
-
448
- export function validateStreamInterpreterConfig(raw: any): StreamInterpreterConfig {
449
- const res = validateStreamInterpreterConfigResult(raw);
451
+ export function validateTouchConfig(raw: any, fieldPath = "touch"): TouchConfig {
452
+ const res = validateTouchConfigResult(raw, fieldPath);
450
453
  if (Result.isError(res)) throw dsError(res.error.message);
451
454
  return res.value;
452
455
  }
453
456
 
454
- export function isTouchEnabled(cfg: StreamInterpreterConfig | undefined): cfg is StreamInterpreterConfig & { touch: TouchConfig } {
455
- return !!cfg?.touch?.enabled;
457
+ export function isTouchEnabled(cfg: TouchConfig | undefined): cfg is TouchConfig & { enabled: true } {
458
+ return !!cfg?.enabled;
456
459
  }
@@ -276,6 +276,10 @@ export class TouchJournal {
276
276
  };
277
277
  }
278
278
 
279
+ getFilterBytes(): number {
280
+ return this.lastSet.byteLength;
281
+ }
282
+
279
283
  touch(keyId: number, sourceOffsetSeq?: bigint): void {
280
284
  if (this.pending.size === 0 && !this.overflow && this.pendingBucketStartMs <= 0) {
281
285
  this.pendingBucketStartMs = Date.now();
@@ -1,9 +1,8 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
1
  import { fileURLToPath } from "node:url";
4
2
  import { Worker } from "node:worker_threads";
5
3
  import { Result } from "better-result";
6
4
  import type { Config } from "../config";
5
+ import { detectHostRuntime } from "../runtime/host_runtime.ts";
7
6
  import type { ProcessRequest, ProcessResult, WorkerMessage } from "./worker_protocol";
8
7
  import { dsError } from "../util/ds_error.ts";
9
8
 
@@ -16,7 +15,7 @@ export type WorkerPoolProcessError = {
16
15
  message: string;
17
16
  };
18
17
 
19
- export class TouchInterpreterWorkerPool {
18
+ export class TouchProcessorWorkerPool {
20
19
  private readonly cfg: Config;
21
20
  private readonly workerCount: number;
22
21
  private readonly workers: Array<{ worker: Worker; busy: boolean; currentId: number | null }> = [];
@@ -102,28 +101,23 @@ export class TouchInterpreterWorkerPool {
102
101
  stream: next.stream,
103
102
  fromOffset: next.fromOffset,
104
103
  toOffset: next.toOffset,
105
- interpreter: next.interpreter,
104
+ profile: next.profile,
106
105
  maxRows: next.maxRows,
107
106
  maxBytes: next.maxBytes,
108
107
  emitFineTouches: next.emitFineTouches,
109
108
  fineTouchBudget: next.fineTouchBudget,
110
109
  fineGranularity: next.fineGranularity,
111
- interpretMode: next.interpretMode,
110
+ processingMode: next.processingMode,
112
111
  filterHotTemplates: next.filterHotTemplates,
113
112
  hotTemplateIds: next.hotTemplateIds,
114
113
  } satisfies ProcessRequest);
115
114
  }
116
115
 
117
116
  private spawnWorker(idx: number, generation: number = this.generation): void {
118
- const workerUrl = new URL("./interpreter_worker.ts", import.meta.url);
119
- let workerSpec = fileURLToPath(workerUrl);
120
- if (!existsSync(workerSpec)) {
121
- const fallback = resolve(process.cwd(), "src/touch/interpreter_worker.ts");
122
- if (existsSync(fallback)) workerSpec = fallback;
123
- }
117
+ const workerSpec = fileURLToPath(new URL("./processor_worker.ts", import.meta.url));
124
118
 
125
119
  const worker = new Worker(workerSpec, {
126
- workerData: { config: this.cfg },
120
+ workerData: { config: this.cfg, hostRuntime: detectHostRuntime() },
127
121
  type: "module",
128
122
  smol: true,
129
123
  } as any);
@@ -160,13 +154,13 @@ export class TouchInterpreterWorkerPool {
160
154
  worker.on("error", (err) => {
161
155
  if (generation !== this.generation) return;
162
156
  // eslint-disable-next-line no-console
163
- console.error(`touch interpreter worker ${idx} error`, err);
157
+ console.error(`touch processor worker ${idx} error`, err);
164
158
  });
165
159
 
166
160
  worker.on("exit", (code) => {
167
161
  if (generation !== this.generation || !this.started) return;
168
162
  // eslint-disable-next-line no-console
169
- console.error(`touch interpreter worker ${idx} exited with code ${code}, respawning`);
163
+ console.error(`touch processor worker ${idx} exited with code ${code}, respawning`);
170
164
  if (slot.currentId != null) {
171
165
  const p = this.pending.get(slot.currentId);
172
166
  if (p) {
@@ -1,4 +1,4 @@
1
- import type { StreamInterpreterConfig } from "./spec.ts";
1
+ import type { StreamProfileSpec } from "../profiles";
2
2
 
3
3
  export type TouchRow = {
4
4
  keyId: number;
@@ -14,13 +14,13 @@ export type ProcessRequest = {
14
14
  stream: string;
15
15
  fromOffset: bigint;
16
16
  toOffset: bigint;
17
- interpreter: StreamInterpreterConfig;
17
+ profile: StreamProfileSpec;
18
18
  maxRows: number;
19
19
  maxBytes: number;
20
20
  emitFineTouches?: boolean;
21
21
  fineTouchBudget?: number | null;
22
22
  fineGranularity?: "key" | "template";
23
- interpretMode?: "full" | "hotTemplatesOnly";
23
+ processingMode?: "full" | "hotTemplatesOnly";
24
24
  filterHotTemplates?: boolean;
25
25
  hotTemplateIds?: string[] | null;
26
26
  };