@prisma/streams-server 0.0.1 → 0.1.0
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.
- package/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +68 -0
- package/LICENSE +201 -0
- package/README.md +39 -2
- package/SECURITY.md +33 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +29 -34
- package/src/app.ts +74 -0
- package/src/app_core.ts +1983 -0
- package/src/app_local.ts +46 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +239 -0
- package/src/config.ts +251 -0
- package/src/db/db.ts +1440 -0
- package/src/db/schema.ts +619 -0
- package/src/expiry_sweeper.ts +44 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +745 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/ingest.ts +655 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +114 -0
- package/src/memory.ts +155 -0
- package/src/metrics.ts +161 -0
- package/src/metrics_emitter.ts +50 -0
- package/src/notifier.ts +64 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +128 -0
- package/src/offset.ts +70 -0
- package/src/reader.ts +454 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/registry.ts +405 -0
- package/src/segment/cache.ts +179 -0
- package/src/segment/format.ts +331 -0
- package/src/segment/segmenter.ts +326 -0
- package/src/segment/segmenter_worker.ts +43 -0
- package/src/segment/segmenter_workers.ts +94 -0
- package/src/server.ts +326 -0
- package/src/sqlite/adapter.ts +164 -0
- package/src/stats.ts +205 -0
- package/src/touch/engine.ts +41 -0
- package/src/touch/interpreter_worker.ts +459 -0
- package/src/touch/live_keys.ts +118 -0
- package/src/touch/live_metrics.ts +858 -0
- package/src/touch/live_templates.ts +619 -0
- package/src/touch/manager.ts +1341 -0
- package/src/touch/naming.ts +13 -0
- package/src/touch/routing_key_notifier.ts +275 -0
- package/src/touch/spec.ts +526 -0
- package/src/touch/touch_journal.ts +671 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +189 -0
- package/src/touch/worker_protocol.ts +58 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +317 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +45 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +31 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/build/index.d.mts +0 -1
- package/build/index.d.ts +0 -1
- package/build/index.js +0 -0
- package/build/index.mjs +0 -1
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import { parseDurationMsResult } from "../util/duration.ts";
|
|
3
|
+
import { dsError } from "../util/ds_error.ts";
|
|
4
|
+
|
|
5
|
+
export type StreamInterpreterConfig = {
|
|
6
|
+
apiVersion: "durable.streams/stream-interpreter/v1";
|
|
7
|
+
format?: "durable.streams/state-protocol/v1";
|
|
8
|
+
touch?: TouchConfig;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type StreamInterpreterConfigValidationError = {
|
|
12
|
+
kind: "invalid_interpreter";
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type TouchConfig = {
|
|
17
|
+
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
|
+
/**
|
|
35
|
+
* Coarse invalidation interval. The server emits at most one table-touch per
|
|
36
|
+
* entity per interval.
|
|
37
|
+
*
|
|
38
|
+
* Default: 100ms.
|
|
39
|
+
*/
|
|
40
|
+
coarseIntervalMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Fine-touch coalescing window for watch keys.
|
|
43
|
+
*
|
|
44
|
+
* Default: 100ms.
|
|
45
|
+
*/
|
|
46
|
+
touchCoalesceWindowMs?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Policy when an update event is missing `oldValue` (before image).
|
|
49
|
+
*
|
|
50
|
+
* - coarse: emit coarse table touches only (safe default)
|
|
51
|
+
* - skipBefore: compute fine touches from `value` only
|
|
52
|
+
* - error: interpreter errors (useful for strict debugging)
|
|
53
|
+
*/
|
|
54
|
+
onMissingBefore?: "coarse" | "skipBefore" | "error";
|
|
55
|
+
/**
|
|
56
|
+
* Optional guardrail: when the interpreter backlog (source offsets behind the tail)
|
|
57
|
+
* exceeds this threshold, the interpreter will emit coarse table touches only
|
|
58
|
+
* (fine/template touches are suppressed) to preserve timeliness under overload.
|
|
59
|
+
*
|
|
60
|
+
* Default: 5000.
|
|
61
|
+
*/
|
|
62
|
+
lagDegradeFineTouchesAtSourceOffsets?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Hysteresis recovery threshold for lag-based degradation.
|
|
65
|
+
*
|
|
66
|
+
* When fine touches are currently suppressed due to lag, they are re-enabled
|
|
67
|
+
* only after lag falls to this threshold (or lower).
|
|
68
|
+
*
|
|
69
|
+
* Default: 1000.
|
|
70
|
+
*/
|
|
71
|
+
lagRecoverFineTouchesAtSourceOffsets?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Optional guardrail: cap fine/template touches emitted per interpreter batch.
|
|
74
|
+
* Table touches are always emitted for correctness.
|
|
75
|
+
*
|
|
76
|
+
* Default: 2000.
|
|
77
|
+
*/
|
|
78
|
+
fineTouchBudgetPerBatch?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Fine-touch token bucket refill rate (tokens/sec).
|
|
81
|
+
*
|
|
82
|
+
* Default: 200000.
|
|
83
|
+
*/
|
|
84
|
+
fineTokensPerSecond?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Fine-touch token bucket burst capacity (tokens).
|
|
87
|
+
*
|
|
88
|
+
* Default: 400000.
|
|
89
|
+
*/
|
|
90
|
+
fineBurstTokens?: number;
|
|
91
|
+
/**
|
|
92
|
+
* When lag guardrails are active, reserve a small fine-touch budget per batch
|
|
93
|
+
* for currently hot keys/templates (premium lane). Set 0 to disable.
|
|
94
|
+
*
|
|
95
|
+
* Default: 200.
|
|
96
|
+
*/
|
|
97
|
+
lagReservedFineTouchBudgetPerBatch?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Memory-only touch journal parameters. Only used when storage="memory".
|
|
100
|
+
*/
|
|
101
|
+
memory?: {
|
|
102
|
+
/**
|
|
103
|
+
* Bucket duration. Cursor generations advance only on bucket flush.
|
|
104
|
+
*
|
|
105
|
+
* Default: 100ms.
|
|
106
|
+
*/
|
|
107
|
+
bucketMs?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Bloom filter size as a power of two (positions). Memory use per stream is
|
|
110
|
+
* `4 * 2^filterPow2` bytes.
|
|
111
|
+
*
|
|
112
|
+
* Default: 22 (16MiB).
|
|
113
|
+
*/
|
|
114
|
+
filterPow2?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Hash positions per key.
|
|
117
|
+
*
|
|
118
|
+
* Default: 4.
|
|
119
|
+
*/
|
|
120
|
+
k?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Hard cap on unique keys tracked per bucket. If exceeded, the bucket is
|
|
123
|
+
* treated as a broadcast invalidation (wake all waiters) to avoid false negatives.
|
|
124
|
+
*
|
|
125
|
+
* Default: 100000.
|
|
126
|
+
*/
|
|
127
|
+
pendingMaxKeys?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Maximum keys per /touch/wait to index per-key. Larger keysets are treated
|
|
130
|
+
* as "broad" and are scanned on each bucket flush.
|
|
131
|
+
*
|
|
132
|
+
* Default: 32.
|
|
133
|
+
*/
|
|
134
|
+
keyIndexMaxKeys?: number;
|
|
135
|
+
/**
|
|
136
|
+
* Sliding TTL for "hot" fine keys observed from /touch/wait.
|
|
137
|
+
*
|
|
138
|
+
* Default: 10000ms.
|
|
139
|
+
*/
|
|
140
|
+
hotKeyTtlMs?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Sliding TTL for "hot" templates observed from templateIdsUsed.
|
|
143
|
+
*
|
|
144
|
+
* Default: 10000ms.
|
|
145
|
+
*/
|
|
146
|
+
hotTemplateTtlMs?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Upper bound for hot fine key tracking per stream.
|
|
149
|
+
*
|
|
150
|
+
* Default: 1000000.
|
|
151
|
+
*/
|
|
152
|
+
hotMaxKeys?: number;
|
|
153
|
+
/**
|
|
154
|
+
* Upper bound for hot template tracking per stream.
|
|
155
|
+
*
|
|
156
|
+
* Default: 4096.
|
|
157
|
+
*/
|
|
158
|
+
hotMaxTemplates?: number;
|
|
159
|
+
};
|
|
160
|
+
templates?: {
|
|
161
|
+
/**
|
|
162
|
+
* Sliding inactivity TTL for templates, measured since last use.
|
|
163
|
+
* Individual activations may override this TTL.
|
|
164
|
+
*
|
|
165
|
+
* Default: 1 hour.
|
|
166
|
+
*/
|
|
167
|
+
defaultInactivityTtlMs?: number;
|
|
168
|
+
/**
|
|
169
|
+
* Persist last-seen timestamps at most once per interval per template.
|
|
170
|
+
*
|
|
171
|
+
* Default: 5 minutes.
|
|
172
|
+
*/
|
|
173
|
+
lastSeenPersistIntervalMs?: number;
|
|
174
|
+
/**
|
|
175
|
+
* Template GC interval.
|
|
176
|
+
*
|
|
177
|
+
* Default: 1 minute.
|
|
178
|
+
*/
|
|
179
|
+
gcIntervalMs?: number;
|
|
180
|
+
maxActiveTemplatesPerEntity?: number;
|
|
181
|
+
maxActiveTemplatesPerStream?: number;
|
|
182
|
+
activationRateLimitPerMinute?: number;
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function invalidInterpreter<T = never>(message: string): Result<T, StreamInterpreterConfigValidationError> {
|
|
187
|
+
return Result.err({ kind: "invalid_interpreter", message });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseNumberField(
|
|
191
|
+
value: any,
|
|
192
|
+
defaultValue: number,
|
|
193
|
+
message: string,
|
|
194
|
+
predicate: (n: number) => boolean
|
|
195
|
+
): Result<number, StreamInterpreterConfigValidationError> {
|
|
196
|
+
const n = value === undefined ? defaultValue : Number(value);
|
|
197
|
+
if (!Number.isFinite(n) || !predicate(n)) return invalidInterpreter(message);
|
|
198
|
+
return Result.ok(n);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseIntegerField(
|
|
202
|
+
value: any,
|
|
203
|
+
defaultValue: number,
|
|
204
|
+
message: string,
|
|
205
|
+
predicate: (n: number) => boolean
|
|
206
|
+
): Result<number, StreamInterpreterConfigValidationError> {
|
|
207
|
+
const n = value === undefined ? defaultValue : Number(value);
|
|
208
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || !predicate(n)) return invalidInterpreter(message);
|
|
209
|
+
return Result.ok(n);
|
|
210
|
+
}
|
|
211
|
+
|
|
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
|
+
function validateTouchConfigResult(raw: any): Result<TouchConfig, StreamInterpreterConfigValidationError> {
|
|
245
|
+
if (!raw || typeof raw !== "object") return invalidInterpreter("interpreter.touch must be an object");
|
|
246
|
+
const enabled = !!raw.enabled;
|
|
247
|
+
if (!enabled) {
|
|
248
|
+
return Result.ok({
|
|
249
|
+
enabled: false,
|
|
250
|
+
storage: undefined,
|
|
251
|
+
derivedStream: undefined,
|
|
252
|
+
retention: undefined,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
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");
|
|
259
|
+
}
|
|
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");
|
|
270
|
+
}
|
|
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
|
+
|
|
279
|
+
const coarseIntervalMsRes = parseNumberField(
|
|
280
|
+
raw.coarseIntervalMs,
|
|
281
|
+
100,
|
|
282
|
+
"interpreter.touch.coarseIntervalMs must be > 0",
|
|
283
|
+
(n) => n > 0
|
|
284
|
+
);
|
|
285
|
+
if (Result.isError(coarseIntervalMsRes)) return coarseIntervalMsRes;
|
|
286
|
+
const touchCoalesceWindowMsRes = parseNumberField(
|
|
287
|
+
raw.touchCoalesceWindowMs,
|
|
288
|
+
100,
|
|
289
|
+
"interpreter.touch.touchCoalesceWindowMs must be > 0",
|
|
290
|
+
(n) => n > 0
|
|
291
|
+
);
|
|
292
|
+
if (Result.isError(touchCoalesceWindowMsRes)) return touchCoalesceWindowMsRes;
|
|
293
|
+
|
|
294
|
+
const onMissingBefore = raw.onMissingBefore === undefined ? "coarse" : raw.onMissingBefore;
|
|
295
|
+
if (onMissingBefore !== "coarse" && onMissingBefore !== "skipBefore" && onMissingBefore !== "error") {
|
|
296
|
+
return invalidInterpreter("interpreter.touch.onMissingBefore must be coarse|skipBefore|error");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const templates = raw.templates && typeof raw.templates === "object" ? raw.templates : {};
|
|
300
|
+
const defaultInactivityTtlMsRes = parseNumberField(
|
|
301
|
+
templates.defaultInactivityTtlMs,
|
|
302
|
+
60 * 60 * 1000,
|
|
303
|
+
"interpreter.touch.templates.defaultInactivityTtlMs must be >= 0",
|
|
304
|
+
(n) => n >= 0
|
|
305
|
+
);
|
|
306
|
+
if (Result.isError(defaultInactivityTtlMsRes)) return defaultInactivityTtlMsRes;
|
|
307
|
+
const lastSeenPersistIntervalMsRes = parseNumberField(
|
|
308
|
+
templates.lastSeenPersistIntervalMs,
|
|
309
|
+
5 * 60 * 1000,
|
|
310
|
+
"interpreter.touch.templates.lastSeenPersistIntervalMs must be > 0",
|
|
311
|
+
(n) => n > 0
|
|
312
|
+
);
|
|
313
|
+
if (Result.isError(lastSeenPersistIntervalMsRes)) return lastSeenPersistIntervalMsRes;
|
|
314
|
+
const gcIntervalMsRes = parseNumberField(
|
|
315
|
+
templates.gcIntervalMs,
|
|
316
|
+
60_000,
|
|
317
|
+
"interpreter.touch.templates.gcIntervalMs must be > 0",
|
|
318
|
+
(n) => n > 0
|
|
319
|
+
);
|
|
320
|
+
if (Result.isError(gcIntervalMsRes)) return gcIntervalMsRes;
|
|
321
|
+
const maxActiveTemplatesPerEntityRes = parseNumberField(
|
|
322
|
+
templates.maxActiveTemplatesPerEntity,
|
|
323
|
+
256,
|
|
324
|
+
"interpreter.touch.templates.maxActiveTemplatesPerEntity must be > 0",
|
|
325
|
+
(n) => n > 0
|
|
326
|
+
);
|
|
327
|
+
if (Result.isError(maxActiveTemplatesPerEntityRes)) return maxActiveTemplatesPerEntityRes;
|
|
328
|
+
const maxActiveTemplatesPerStreamRes = parseNumberField(
|
|
329
|
+
templates.maxActiveTemplatesPerStream,
|
|
330
|
+
2048,
|
|
331
|
+
"interpreter.touch.templates.maxActiveTemplatesPerStream must be > 0",
|
|
332
|
+
(n) => n > 0
|
|
333
|
+
);
|
|
334
|
+
if (Result.isError(maxActiveTemplatesPerStreamRes)) return maxActiveTemplatesPerStreamRes;
|
|
335
|
+
const activationRateLimitPerMinuteRes = parseNumberField(
|
|
336
|
+
templates.activationRateLimitPerMinute,
|
|
337
|
+
100,
|
|
338
|
+
"interpreter.touch.templates.activationRateLimitPerMinute must be >= 0",
|
|
339
|
+
(n) => n >= 0
|
|
340
|
+
);
|
|
341
|
+
if (Result.isError(activationRateLimitPerMinuteRes)) return activationRateLimitPerMinuteRes;
|
|
342
|
+
|
|
343
|
+
if (raw.metrics !== undefined) {
|
|
344
|
+
return invalidInterpreter("interpreter.touch.metrics is not supported; live metrics are a global server feature");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const memoryRaw = raw.memory && typeof raw.memory === "object" ? raw.memory : {};
|
|
348
|
+
const bucketMsRes = parseIntegerField(
|
|
349
|
+
memoryRaw.bucketMs,
|
|
350
|
+
100,
|
|
351
|
+
"interpreter.touch.memory.bucketMs must be an integer > 0",
|
|
352
|
+
(n) => n > 0
|
|
353
|
+
);
|
|
354
|
+
if (Result.isError(bucketMsRes)) return bucketMsRes;
|
|
355
|
+
const filterPow2Res = parseIntegerField(
|
|
356
|
+
memoryRaw.filterPow2,
|
|
357
|
+
22,
|
|
358
|
+
"interpreter.touch.memory.filterPow2 must be an integer in [10,30]",
|
|
359
|
+
(n) => n >= 10 && n <= 30
|
|
360
|
+
);
|
|
361
|
+
if (Result.isError(filterPow2Res)) return filterPow2Res;
|
|
362
|
+
const kRes = parseIntegerField(
|
|
363
|
+
memoryRaw.k,
|
|
364
|
+
4,
|
|
365
|
+
"interpreter.touch.memory.k must be an integer in [1,8]",
|
|
366
|
+
(n) => n >= 1 && n <= 8
|
|
367
|
+
);
|
|
368
|
+
if (Result.isError(kRes)) return kRes;
|
|
369
|
+
const pendingMaxKeysRes = parseIntegerField(
|
|
370
|
+
memoryRaw.pendingMaxKeys,
|
|
371
|
+
100_000,
|
|
372
|
+
"interpreter.touch.memory.pendingMaxKeys must be an integer > 0",
|
|
373
|
+
(n) => n > 0
|
|
374
|
+
);
|
|
375
|
+
if (Result.isError(pendingMaxKeysRes)) return pendingMaxKeysRes;
|
|
376
|
+
const keyIndexMaxKeysRes = parseIntegerField(
|
|
377
|
+
memoryRaw.keyIndexMaxKeys,
|
|
378
|
+
32,
|
|
379
|
+
"interpreter.touch.memory.keyIndexMaxKeys must be an integer in [1,1024]",
|
|
380
|
+
(n) => n >= 1 && n <= 1024
|
|
381
|
+
);
|
|
382
|
+
if (Result.isError(keyIndexMaxKeysRes)) return keyIndexMaxKeysRes;
|
|
383
|
+
const hotKeyTtlMsRes = parseIntegerField(
|
|
384
|
+
memoryRaw.hotKeyTtlMs,
|
|
385
|
+
10_000,
|
|
386
|
+
"interpreter.touch.memory.hotKeyTtlMs must be an integer > 0",
|
|
387
|
+
(n) => n > 0
|
|
388
|
+
);
|
|
389
|
+
if (Result.isError(hotKeyTtlMsRes)) return hotKeyTtlMsRes;
|
|
390
|
+
const hotTemplateTtlMsRes = parseIntegerField(
|
|
391
|
+
memoryRaw.hotTemplateTtlMs,
|
|
392
|
+
10_000,
|
|
393
|
+
"interpreter.touch.memory.hotTemplateTtlMs must be an integer > 0",
|
|
394
|
+
(n) => n > 0
|
|
395
|
+
);
|
|
396
|
+
if (Result.isError(hotTemplateTtlMsRes)) return hotTemplateTtlMsRes;
|
|
397
|
+
const hotMaxKeysRes = parseIntegerField(
|
|
398
|
+
memoryRaw.hotMaxKeys,
|
|
399
|
+
1_000_000,
|
|
400
|
+
"interpreter.touch.memory.hotMaxKeys must be an integer > 0",
|
|
401
|
+
(n) => n > 0
|
|
402
|
+
);
|
|
403
|
+
if (Result.isError(hotMaxKeysRes)) return hotMaxKeysRes;
|
|
404
|
+
const hotMaxTemplatesRes = parseIntegerField(
|
|
405
|
+
memoryRaw.hotMaxTemplates,
|
|
406
|
+
4096,
|
|
407
|
+
"interpreter.touch.memory.hotMaxTemplates must be an integer > 0",
|
|
408
|
+
(n) => n > 0
|
|
409
|
+
);
|
|
410
|
+
if (Result.isError(hotMaxTemplatesRes)) return hotMaxTemplatesRes;
|
|
411
|
+
|
|
412
|
+
const lagDegradeFineTouchesAtSourceOffsetsRes = parseIntegerField(
|
|
413
|
+
raw.lagDegradeFineTouchesAtSourceOffsets,
|
|
414
|
+
5000,
|
|
415
|
+
"interpreter.touch.lagDegradeFineTouchesAtSourceOffsets must be an integer >= 0",
|
|
416
|
+
(n) => n >= 0
|
|
417
|
+
);
|
|
418
|
+
if (Result.isError(lagDegradeFineTouchesAtSourceOffsetsRes)) return lagDegradeFineTouchesAtSourceOffsetsRes;
|
|
419
|
+
const lagRecoverFineTouchesAtSourceOffsetsRes = parseIntegerField(
|
|
420
|
+
raw.lagRecoverFineTouchesAtSourceOffsets,
|
|
421
|
+
1000,
|
|
422
|
+
"interpreter.touch.lagRecoverFineTouchesAtSourceOffsets must be an integer >= 0",
|
|
423
|
+
(n) => n >= 0
|
|
424
|
+
);
|
|
425
|
+
if (Result.isError(lagRecoverFineTouchesAtSourceOffsetsRes)) return lagRecoverFineTouchesAtSourceOffsetsRes;
|
|
426
|
+
const fineTouchBudgetPerBatchRes = parseIntegerField(
|
|
427
|
+
raw.fineTouchBudgetPerBatch,
|
|
428
|
+
2000,
|
|
429
|
+
"interpreter.touch.fineTouchBudgetPerBatch must be an integer >= 0",
|
|
430
|
+
(n) => n >= 0
|
|
431
|
+
);
|
|
432
|
+
if (Result.isError(fineTouchBudgetPerBatchRes)) return fineTouchBudgetPerBatchRes;
|
|
433
|
+
const fineTokensPerSecondRes = parseIntegerField(
|
|
434
|
+
raw.fineTokensPerSecond,
|
|
435
|
+
200_000,
|
|
436
|
+
"interpreter.touch.fineTokensPerSecond must be an integer >= 0",
|
|
437
|
+
(n) => n >= 0
|
|
438
|
+
);
|
|
439
|
+
if (Result.isError(fineTokensPerSecondRes)) return fineTokensPerSecondRes;
|
|
440
|
+
const fineBurstTokensRes = parseIntegerField(
|
|
441
|
+
raw.fineBurstTokens,
|
|
442
|
+
400_000,
|
|
443
|
+
"interpreter.touch.fineBurstTokens must be an integer >= 0",
|
|
444
|
+
(n) => n >= 0
|
|
445
|
+
);
|
|
446
|
+
if (Result.isError(fineBurstTokensRes)) return fineBurstTokensRes;
|
|
447
|
+
const lagReservedFineTouchBudgetPerBatchRes = parseIntegerField(
|
|
448
|
+
raw.lagReservedFineTouchBudgetPerBatch,
|
|
449
|
+
200,
|
|
450
|
+
"interpreter.touch.lagReservedFineTouchBudgetPerBatch must be an integer >= 0",
|
|
451
|
+
(n) => n >= 0
|
|
452
|
+
);
|
|
453
|
+
if (Result.isError(lagReservedFineTouchBudgetPerBatchRes)) return lagReservedFineTouchBudgetPerBatchRes;
|
|
454
|
+
|
|
455
|
+
return Result.ok({
|
|
456
|
+
enabled: true,
|
|
457
|
+
storage,
|
|
458
|
+
derivedStream,
|
|
459
|
+
retention,
|
|
460
|
+
coarseIntervalMs: coarseIntervalMsRes.value,
|
|
461
|
+
touchCoalesceWindowMs: touchCoalesceWindowMsRes.value,
|
|
462
|
+
onMissingBefore,
|
|
463
|
+
lagDegradeFineTouchesAtSourceOffsets: lagDegradeFineTouchesAtSourceOffsetsRes.value,
|
|
464
|
+
lagRecoverFineTouchesAtSourceOffsets: lagRecoverFineTouchesAtSourceOffsetsRes.value,
|
|
465
|
+
fineTouchBudgetPerBatch: fineTouchBudgetPerBatchRes.value,
|
|
466
|
+
fineTokensPerSecond: fineTokensPerSecondRes.value,
|
|
467
|
+
fineBurstTokens: fineBurstTokensRes.value,
|
|
468
|
+
lagReservedFineTouchBudgetPerBatch: lagReservedFineTouchBudgetPerBatchRes.value,
|
|
469
|
+
memory: {
|
|
470
|
+
bucketMs: bucketMsRes.value,
|
|
471
|
+
filterPow2: filterPow2Res.value,
|
|
472
|
+
k: kRes.value,
|
|
473
|
+
pendingMaxKeys: pendingMaxKeysRes.value,
|
|
474
|
+
keyIndexMaxKeys: keyIndexMaxKeysRes.value,
|
|
475
|
+
hotKeyTtlMs: hotKeyTtlMsRes.value,
|
|
476
|
+
hotTemplateTtlMs: hotTemplateTtlMsRes.value,
|
|
477
|
+
hotMaxKeys: hotMaxKeysRes.value,
|
|
478
|
+
hotMaxTemplates: hotMaxTemplatesRes.value,
|
|
479
|
+
},
|
|
480
|
+
templates: {
|
|
481
|
+
defaultInactivityTtlMs: defaultInactivityTtlMsRes.value,
|
|
482
|
+
lastSeenPersistIntervalMs: lastSeenPersistIntervalMsRes.value,
|
|
483
|
+
gcIntervalMs: gcIntervalMsRes.value,
|
|
484
|
+
maxActiveTemplatesPerEntity: maxActiveTemplatesPerEntityRes.value,
|
|
485
|
+
maxActiveTemplatesPerStream: maxActiveTemplatesPerStreamRes.value,
|
|
486
|
+
activationRateLimitPerMinute: activationRateLimitPerMinuteRes.value,
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function validateStreamInterpreterConfigResult(
|
|
492
|
+
raw: any
|
|
493
|
+
): Result<StreamInterpreterConfig, StreamInterpreterConfigValidationError> {
|
|
494
|
+
if (!raw || typeof raw !== "object") return invalidInterpreter("interpreter must be an object");
|
|
495
|
+
if (raw.apiVersion !== "durable.streams/stream-interpreter/v1") {
|
|
496
|
+
return invalidInterpreter("invalid interpreter apiVersion");
|
|
497
|
+
}
|
|
498
|
+
const formatRaw = raw.format === undefined ? undefined : raw.format;
|
|
499
|
+
if (formatRaw !== undefined && formatRaw !== "durable.streams/state-protocol/v1") {
|
|
500
|
+
return invalidInterpreter("interpreter.format must be durable.streams/state-protocol/v1");
|
|
501
|
+
}
|
|
502
|
+
if (raw.variants !== undefined) {
|
|
503
|
+
return invalidInterpreter("interpreter.variants is not supported (State Protocol is the only supported format)");
|
|
504
|
+
}
|
|
505
|
+
let touch: TouchConfig | undefined;
|
|
506
|
+
if (raw.touch !== undefined) {
|
|
507
|
+
const touchRes = validateTouchConfigResult(raw.touch);
|
|
508
|
+
if (Result.isError(touchRes)) return invalidInterpreter(touchRes.error.message);
|
|
509
|
+
touch = touchRes.value;
|
|
510
|
+
}
|
|
511
|
+
return Result.ok({
|
|
512
|
+
apiVersion: "durable.streams/stream-interpreter/v1",
|
|
513
|
+
format: "durable.streams/state-protocol/v1",
|
|
514
|
+
touch,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function validateStreamInterpreterConfig(raw: any): StreamInterpreterConfig {
|
|
519
|
+
const res = validateStreamInterpreterConfigResult(raw);
|
|
520
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
521
|
+
return res.value;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function isTouchEnabled(cfg: StreamInterpreterConfig | undefined): cfg is StreamInterpreterConfig & { touch: TouchConfig } {
|
|
525
|
+
return !!cfg?.touch?.enabled;
|
|
526
|
+
}
|