@prisma/streams-server 0.0.1 → 0.1.1

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 (83) hide show
  1. package/CODE_OF_CONDUCT.md +45 -0
  2. package/CONTRIBUTING.md +68 -0
  3. package/LICENSE +201 -0
  4. package/README.md +39 -2
  5. package/SECURITY.md +33 -0
  6. package/bin/prisma-streams-server +2 -0
  7. package/package.json +29 -34
  8. package/src/app.ts +74 -0
  9. package/src/app_core.ts +1706 -0
  10. package/src/app_local.ts +46 -0
  11. package/src/backpressure.ts +66 -0
  12. package/src/bootstrap.ts +239 -0
  13. package/src/config.ts +251 -0
  14. package/src/db/db.ts +1386 -0
  15. package/src/db/schema.ts +625 -0
  16. package/src/expiry_sweeper.ts +44 -0
  17. package/src/hist.ts +169 -0
  18. package/src/index/binary_fuse.ts +379 -0
  19. package/src/index/indexer.ts +745 -0
  20. package/src/index/run_cache.ts +84 -0
  21. package/src/index/run_format.ts +213 -0
  22. package/src/ingest.ts +655 -0
  23. package/src/lens/lens.ts +501 -0
  24. package/src/manifest.ts +114 -0
  25. package/src/memory.ts +155 -0
  26. package/src/metrics.ts +161 -0
  27. package/src/metrics_emitter.ts +50 -0
  28. package/src/notifier.ts +64 -0
  29. package/src/objectstore/interface.ts +13 -0
  30. package/src/objectstore/mock_r2.ts +269 -0
  31. package/src/objectstore/null.ts +32 -0
  32. package/src/objectstore/r2.ts +128 -0
  33. package/src/offset.ts +70 -0
  34. package/src/reader.ts +454 -0
  35. package/src/runtime/hash.ts +156 -0
  36. package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
  37. package/src/runtime/hash_vendor/NOTICE.md +8 -0
  38. package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
  39. package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
  40. package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
  41. package/src/schema/lens_schema.ts +290 -0
  42. package/src/schema/proof.ts +547 -0
  43. package/src/schema/registry.ts +405 -0
  44. package/src/segment/cache.ts +179 -0
  45. package/src/segment/format.ts +331 -0
  46. package/src/segment/segmenter.ts +326 -0
  47. package/src/segment/segmenter_worker.ts +43 -0
  48. package/src/segment/segmenter_workers.ts +94 -0
  49. package/src/server.ts +326 -0
  50. package/src/sqlite/adapter.ts +164 -0
  51. package/src/stats.ts +205 -0
  52. package/src/touch/engine.ts +41 -0
  53. package/src/touch/interpreter_worker.ts +442 -0
  54. package/src/touch/live_keys.ts +118 -0
  55. package/src/touch/live_metrics.ts +827 -0
  56. package/src/touch/live_templates.ts +619 -0
  57. package/src/touch/manager.ts +1199 -0
  58. package/src/touch/spec.ts +456 -0
  59. package/src/touch/touch_journal.ts +671 -0
  60. package/src/touch/touch_key_id.ts +20 -0
  61. package/src/touch/worker_pool.ts +189 -0
  62. package/src/touch/worker_protocol.ts +56 -0
  63. package/src/types/proper-lockfile.d.ts +1 -0
  64. package/src/uploader.ts +317 -0
  65. package/src/util/base32_crockford.ts +81 -0
  66. package/src/util/bloom256.ts +67 -0
  67. package/src/util/cleanup.ts +22 -0
  68. package/src/util/crc32c.ts +29 -0
  69. package/src/util/ds_error.ts +15 -0
  70. package/src/util/duration.ts +17 -0
  71. package/src/util/endian.ts +53 -0
  72. package/src/util/json_pointer.ts +148 -0
  73. package/src/util/log.ts +25 -0
  74. package/src/util/lru.ts +45 -0
  75. package/src/util/retry.ts +35 -0
  76. package/src/util/siphash.ts +71 -0
  77. package/src/util/stream_paths.ts +31 -0
  78. package/src/util/time.ts +14 -0
  79. package/src/util/yield.ts +3 -0
  80. package/build/index.d.mts +0 -1
  81. package/build/index.d.ts +0 -1
  82. package/build/index.js +0 -0
  83. package/build/index.mjs +0 -1
@@ -0,0 +1,456 @@
1
+ import { Result } from "better-result";
2
+ import { dsError } from "../util/ds_error.ts";
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";
12
+ message: string;
13
+ };
14
+
15
+ export type TouchConfig = {
16
+ enabled: boolean;
17
+ /**
18
+ * Coarse invalidation interval. The server emits at most one table-touch per
19
+ * entity per interval.
20
+ *
21
+ * Default: 100ms.
22
+ */
23
+ coarseIntervalMs?: number;
24
+ /**
25
+ * Fine-touch coalescing window for watch keys.
26
+ *
27
+ * Default: 100ms.
28
+ */
29
+ touchCoalesceWindowMs?: number;
30
+ /**
31
+ * Policy when an update event is missing `oldValue` (before image).
32
+ *
33
+ * - coarse: emit coarse table touches only (safe default)
34
+ * - skipBefore: compute fine touches from `value` only
35
+ * - error: interpreter errors (useful for strict debugging)
36
+ */
37
+ onMissingBefore?: "coarse" | "skipBefore" | "error";
38
+ /**
39
+ * Optional guardrail: when the interpreter backlog (source offsets behind the tail)
40
+ * exceeds this threshold, the interpreter will emit coarse table touches only
41
+ * (fine/template touches are suppressed) to preserve timeliness under overload.
42
+ *
43
+ * Default: 5000.
44
+ */
45
+ lagDegradeFineTouchesAtSourceOffsets?: number;
46
+ /**
47
+ * Hysteresis recovery threshold for lag-based degradation.
48
+ *
49
+ * When fine touches are currently suppressed due to lag, they are re-enabled
50
+ * only after lag falls to this threshold (or lower).
51
+ *
52
+ * Default: 1000.
53
+ */
54
+ lagRecoverFineTouchesAtSourceOffsets?: number;
55
+ /**
56
+ * Optional guardrail: cap fine/template touches emitted per interpreter batch.
57
+ * Table touches are always emitted for correctness.
58
+ *
59
+ * Default: 2000.
60
+ */
61
+ fineTouchBudgetPerBatch?: number;
62
+ /**
63
+ * Fine-touch token bucket refill rate (tokens/sec).
64
+ *
65
+ * Default: 200000.
66
+ */
67
+ fineTokensPerSecond?: number;
68
+ /**
69
+ * Fine-touch token bucket burst capacity (tokens).
70
+ *
71
+ * Default: 400000.
72
+ */
73
+ fineBurstTokens?: number;
74
+ /**
75
+ * When lag guardrails are active, reserve a small fine-touch budget per batch
76
+ * for currently hot keys/templates (premium lane). Set 0 to disable.
77
+ *
78
+ * Default: 200.
79
+ */
80
+ lagReservedFineTouchBudgetPerBatch?: number;
81
+ /**
82
+ * Memory-only touch journal parameters. Only used when storage="memory".
83
+ */
84
+ memory?: {
85
+ /**
86
+ * Bucket duration. Cursor generations advance only on bucket flush.
87
+ *
88
+ * Default: 100ms.
89
+ */
90
+ bucketMs?: number;
91
+ /**
92
+ * Bloom filter size as a power of two (positions). Memory use per stream is
93
+ * `4 * 2^filterPow2` bytes.
94
+ *
95
+ * Default: 22 (16MiB).
96
+ */
97
+ filterPow2?: number;
98
+ /**
99
+ * Hash positions per key.
100
+ *
101
+ * Default: 4.
102
+ */
103
+ k?: number;
104
+ /**
105
+ * Hard cap on unique keys tracked per bucket. If exceeded, the bucket is
106
+ * treated as a broadcast invalidation (wake all waiters) to avoid false negatives.
107
+ *
108
+ * Default: 100000.
109
+ */
110
+ pendingMaxKeys?: number;
111
+ /**
112
+ * Maximum keys per /touch/wait to index per-key. Larger keysets are treated
113
+ * as "broad" and are scanned on each bucket flush.
114
+ *
115
+ * Default: 32.
116
+ */
117
+ keyIndexMaxKeys?: number;
118
+ /**
119
+ * Sliding TTL for "hot" fine keys observed from /touch/wait.
120
+ *
121
+ * Default: 10000ms.
122
+ */
123
+ hotKeyTtlMs?: number;
124
+ /**
125
+ * Sliding TTL for "hot" templates observed from templateIdsUsed.
126
+ *
127
+ * Default: 10000ms.
128
+ */
129
+ hotTemplateTtlMs?: number;
130
+ /**
131
+ * Upper bound for hot fine key tracking per stream.
132
+ *
133
+ * Default: 1000000.
134
+ */
135
+ hotMaxKeys?: number;
136
+ /**
137
+ * Upper bound for hot template tracking per stream.
138
+ *
139
+ * Default: 4096.
140
+ */
141
+ hotMaxTemplates?: number;
142
+ };
143
+ templates?: {
144
+ /**
145
+ * Sliding inactivity TTL for templates, measured since last use.
146
+ * Individual activations may override this TTL.
147
+ *
148
+ * Default: 1 hour.
149
+ */
150
+ defaultInactivityTtlMs?: number;
151
+ /**
152
+ * Persist last-seen timestamps at most once per interval per template.
153
+ *
154
+ * Default: 5 minutes.
155
+ */
156
+ lastSeenPersistIntervalMs?: number;
157
+ /**
158
+ * Template GC interval.
159
+ *
160
+ * Default: 1 minute.
161
+ */
162
+ gcIntervalMs?: number;
163
+ maxActiveTemplatesPerEntity?: number;
164
+ maxActiveTemplatesPerStream?: number;
165
+ activationRateLimitPerMinute?: number;
166
+ };
167
+ };
168
+
169
+ function invalidInterpreter<T = never>(message: string): Result<T, StreamInterpreterConfigValidationError> {
170
+ return Result.err({ kind: "invalid_interpreter", message });
171
+ }
172
+
173
+ function parseNumberField(
174
+ value: any,
175
+ defaultValue: number,
176
+ message: string,
177
+ predicate: (n: number) => boolean
178
+ ): Result<number, StreamInterpreterConfigValidationError> {
179
+ const n = value === undefined ? defaultValue : Number(value);
180
+ if (!Number.isFinite(n) || !predicate(n)) return invalidInterpreter(message);
181
+ return Result.ok(n);
182
+ }
183
+
184
+ function parseIntegerField(
185
+ value: any,
186
+ defaultValue: number,
187
+ message: string,
188
+ predicate: (n: number) => boolean
189
+ ): Result<number, StreamInterpreterConfigValidationError> {
190
+ const n = value === undefined ? defaultValue : Number(value);
191
+ if (!Number.isFinite(n) || !Number.isInteger(n) || !predicate(n)) return invalidInterpreter(message);
192
+ return Result.ok(n);
193
+ }
194
+
195
+ function validateTouchConfigResult(raw: any): Result<TouchConfig, StreamInterpreterConfigValidationError> {
196
+ if (!raw || typeof raw !== "object") return invalidInterpreter("interpreter.touch must be an object");
197
+ const enabled = !!raw.enabled;
198
+ if (!enabled) {
199
+ return Result.ok({ enabled: false });
200
+ }
201
+
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
+ const coarseIntervalMsRes = parseNumberField(
213
+ raw.coarseIntervalMs,
214
+ 100,
215
+ "interpreter.touch.coarseIntervalMs must be > 0",
216
+ (n) => n > 0
217
+ );
218
+ if (Result.isError(coarseIntervalMsRes)) return coarseIntervalMsRes;
219
+ const touchCoalesceWindowMsRes = parseNumberField(
220
+ raw.touchCoalesceWindowMs,
221
+ 100,
222
+ "interpreter.touch.touchCoalesceWindowMs must be > 0",
223
+ (n) => n > 0
224
+ );
225
+ if (Result.isError(touchCoalesceWindowMsRes)) return touchCoalesceWindowMsRes;
226
+
227
+ const onMissingBefore = raw.onMissingBefore === undefined ? "coarse" : raw.onMissingBefore;
228
+ if (onMissingBefore !== "coarse" && onMissingBefore !== "skipBefore" && onMissingBefore !== "error") {
229
+ return invalidInterpreter("interpreter.touch.onMissingBefore must be coarse|skipBefore|error");
230
+ }
231
+
232
+ const templates = raw.templates && typeof raw.templates === "object" ? raw.templates : {};
233
+ const defaultInactivityTtlMsRes = parseNumberField(
234
+ templates.defaultInactivityTtlMs,
235
+ 60 * 60 * 1000,
236
+ "interpreter.touch.templates.defaultInactivityTtlMs must be >= 0",
237
+ (n) => n >= 0
238
+ );
239
+ if (Result.isError(defaultInactivityTtlMsRes)) return defaultInactivityTtlMsRes;
240
+ const lastSeenPersistIntervalMsRes = parseNumberField(
241
+ templates.lastSeenPersistIntervalMs,
242
+ 5 * 60 * 1000,
243
+ "interpreter.touch.templates.lastSeenPersistIntervalMs must be > 0",
244
+ (n) => n > 0
245
+ );
246
+ if (Result.isError(lastSeenPersistIntervalMsRes)) return lastSeenPersistIntervalMsRes;
247
+ const gcIntervalMsRes = parseNumberField(
248
+ templates.gcIntervalMs,
249
+ 60_000,
250
+ "interpreter.touch.templates.gcIntervalMs must be > 0",
251
+ (n) => n > 0
252
+ );
253
+ if (Result.isError(gcIntervalMsRes)) return gcIntervalMsRes;
254
+ const maxActiveTemplatesPerEntityRes = parseNumberField(
255
+ templates.maxActiveTemplatesPerEntity,
256
+ 256,
257
+ "interpreter.touch.templates.maxActiveTemplatesPerEntity must be > 0",
258
+ (n) => n > 0
259
+ );
260
+ if (Result.isError(maxActiveTemplatesPerEntityRes)) return maxActiveTemplatesPerEntityRes;
261
+ const maxActiveTemplatesPerStreamRes = parseNumberField(
262
+ templates.maxActiveTemplatesPerStream,
263
+ 2048,
264
+ "interpreter.touch.templates.maxActiveTemplatesPerStream must be > 0",
265
+ (n) => n > 0
266
+ );
267
+ if (Result.isError(maxActiveTemplatesPerStreamRes)) return maxActiveTemplatesPerStreamRes;
268
+ const activationRateLimitPerMinuteRes = parseNumberField(
269
+ templates.activationRateLimitPerMinute,
270
+ 100,
271
+ "interpreter.touch.templates.activationRateLimitPerMinute must be >= 0",
272
+ (n) => n >= 0
273
+ );
274
+ if (Result.isError(activationRateLimitPerMinuteRes)) return activationRateLimitPerMinuteRes;
275
+
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 : {};
281
+ const bucketMsRes = parseIntegerField(
282
+ memoryRaw.bucketMs,
283
+ 100,
284
+ "interpreter.touch.memory.bucketMs must be an integer > 0",
285
+ (n) => n > 0
286
+ );
287
+ if (Result.isError(bucketMsRes)) return bucketMsRes;
288
+ const filterPow2Res = parseIntegerField(
289
+ memoryRaw.filterPow2,
290
+ 22,
291
+ "interpreter.touch.memory.filterPow2 must be an integer in [10,30]",
292
+ (n) => n >= 10 && n <= 30
293
+ );
294
+ if (Result.isError(filterPow2Res)) return filterPow2Res;
295
+ const kRes = parseIntegerField(
296
+ memoryRaw.k,
297
+ 4,
298
+ "interpreter.touch.memory.k must be an integer in [1,8]",
299
+ (n) => n >= 1 && n <= 8
300
+ );
301
+ if (Result.isError(kRes)) return kRes;
302
+ const pendingMaxKeysRes = parseIntegerField(
303
+ memoryRaw.pendingMaxKeys,
304
+ 100_000,
305
+ "interpreter.touch.memory.pendingMaxKeys must be an integer > 0",
306
+ (n) => n > 0
307
+ );
308
+ if (Result.isError(pendingMaxKeysRes)) return pendingMaxKeysRes;
309
+ const keyIndexMaxKeysRes = parseIntegerField(
310
+ memoryRaw.keyIndexMaxKeys,
311
+ 32,
312
+ "interpreter.touch.memory.keyIndexMaxKeys must be an integer in [1,1024]",
313
+ (n) => n >= 1 && n <= 1024
314
+ );
315
+ if (Result.isError(keyIndexMaxKeysRes)) return keyIndexMaxKeysRes;
316
+ const hotKeyTtlMsRes = parseIntegerField(
317
+ memoryRaw.hotKeyTtlMs,
318
+ 10_000,
319
+ "interpreter.touch.memory.hotKeyTtlMs must be an integer > 0",
320
+ (n) => n > 0
321
+ );
322
+ if (Result.isError(hotKeyTtlMsRes)) return hotKeyTtlMsRes;
323
+ const hotTemplateTtlMsRes = parseIntegerField(
324
+ memoryRaw.hotTemplateTtlMs,
325
+ 10_000,
326
+ "interpreter.touch.memory.hotTemplateTtlMs must be an integer > 0",
327
+ (n) => n > 0
328
+ );
329
+ if (Result.isError(hotTemplateTtlMsRes)) return hotTemplateTtlMsRes;
330
+ const hotMaxKeysRes = parseIntegerField(
331
+ memoryRaw.hotMaxKeys,
332
+ 1_000_000,
333
+ "interpreter.touch.memory.hotMaxKeys must be an integer > 0",
334
+ (n) => n > 0
335
+ );
336
+ if (Result.isError(hotMaxKeysRes)) return hotMaxKeysRes;
337
+ const hotMaxTemplatesRes = parseIntegerField(
338
+ memoryRaw.hotMaxTemplates,
339
+ 4096,
340
+ "interpreter.touch.memory.hotMaxTemplates must be an integer > 0",
341
+ (n) => n > 0
342
+ );
343
+ if (Result.isError(hotMaxTemplatesRes)) return hotMaxTemplatesRes;
344
+
345
+ const lagDegradeFineTouchesAtSourceOffsetsRes = parseIntegerField(
346
+ raw.lagDegradeFineTouchesAtSourceOffsets,
347
+ 5000,
348
+ "interpreter.touch.lagDegradeFineTouchesAtSourceOffsets must be an integer >= 0",
349
+ (n) => n >= 0
350
+ );
351
+ if (Result.isError(lagDegradeFineTouchesAtSourceOffsetsRes)) return lagDegradeFineTouchesAtSourceOffsetsRes;
352
+ const lagRecoverFineTouchesAtSourceOffsetsRes = parseIntegerField(
353
+ raw.lagRecoverFineTouchesAtSourceOffsets,
354
+ 1000,
355
+ "interpreter.touch.lagRecoverFineTouchesAtSourceOffsets must be an integer >= 0",
356
+ (n) => n >= 0
357
+ );
358
+ if (Result.isError(lagRecoverFineTouchesAtSourceOffsetsRes)) return lagRecoverFineTouchesAtSourceOffsetsRes;
359
+ const fineTouchBudgetPerBatchRes = parseIntegerField(
360
+ raw.fineTouchBudgetPerBatch,
361
+ 2000,
362
+ "interpreter.touch.fineTouchBudgetPerBatch must be an integer >= 0",
363
+ (n) => n >= 0
364
+ );
365
+ if (Result.isError(fineTouchBudgetPerBatchRes)) return fineTouchBudgetPerBatchRes;
366
+ const fineTokensPerSecondRes = parseIntegerField(
367
+ raw.fineTokensPerSecond,
368
+ 200_000,
369
+ "interpreter.touch.fineTokensPerSecond must be an integer >= 0",
370
+ (n) => n >= 0
371
+ );
372
+ if (Result.isError(fineTokensPerSecondRes)) return fineTokensPerSecondRes;
373
+ const fineBurstTokensRes = parseIntegerField(
374
+ raw.fineBurstTokens,
375
+ 400_000,
376
+ "interpreter.touch.fineBurstTokens must be an integer >= 0",
377
+ (n) => n >= 0
378
+ );
379
+ if (Result.isError(fineBurstTokensRes)) return fineBurstTokensRes;
380
+ const lagReservedFineTouchBudgetPerBatchRes = parseIntegerField(
381
+ raw.lagReservedFineTouchBudgetPerBatch,
382
+ 200,
383
+ "interpreter.touch.lagReservedFineTouchBudgetPerBatch must be an integer >= 0",
384
+ (n) => n >= 0
385
+ );
386
+ if (Result.isError(lagReservedFineTouchBudgetPerBatchRes)) return lagReservedFineTouchBudgetPerBatchRes;
387
+
388
+ return Result.ok({
389
+ enabled: true,
390
+ coarseIntervalMs: coarseIntervalMsRes.value,
391
+ touchCoalesceWindowMs: touchCoalesceWindowMsRes.value,
392
+ onMissingBefore,
393
+ lagDegradeFineTouchesAtSourceOffsets: lagDegradeFineTouchesAtSourceOffsetsRes.value,
394
+ lagRecoverFineTouchesAtSourceOffsets: lagRecoverFineTouchesAtSourceOffsetsRes.value,
395
+ fineTouchBudgetPerBatch: fineTouchBudgetPerBatchRes.value,
396
+ fineTokensPerSecond: fineTokensPerSecondRes.value,
397
+ fineBurstTokens: fineBurstTokensRes.value,
398
+ lagReservedFineTouchBudgetPerBatch: lagReservedFineTouchBudgetPerBatchRes.value,
399
+ memory: {
400
+ bucketMs: bucketMsRes.value,
401
+ filterPow2: filterPow2Res.value,
402
+ k: kRes.value,
403
+ pendingMaxKeys: pendingMaxKeysRes.value,
404
+ keyIndexMaxKeys: keyIndexMaxKeysRes.value,
405
+ hotKeyTtlMs: hotKeyTtlMsRes.value,
406
+ hotTemplateTtlMs: hotTemplateTtlMsRes.value,
407
+ hotMaxKeys: hotMaxKeysRes.value,
408
+ hotMaxTemplates: hotMaxTemplatesRes.value,
409
+ },
410
+ templates: {
411
+ defaultInactivityTtlMs: defaultInactivityTtlMsRes.value,
412
+ lastSeenPersistIntervalMs: lastSeenPersistIntervalMsRes.value,
413
+ gcIntervalMs: gcIntervalMsRes.value,
414
+ maxActiveTemplatesPerEntity: maxActiveTemplatesPerEntityRes.value,
415
+ maxActiveTemplatesPerStream: maxActiveTemplatesPerStreamRes.value,
416
+ activationRateLimitPerMinute: activationRateLimitPerMinuteRes.value,
417
+ },
418
+ });
419
+ }
420
+
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);
450
+ if (Result.isError(res)) throw dsError(res.error.message);
451
+ return res.value;
452
+ }
453
+
454
+ export function isTouchEnabled(cfg: StreamInterpreterConfig | undefined): cfg is StreamInterpreterConfig & { touch: TouchConfig } {
455
+ return !!cfg?.touch?.enabled;
456
+ }