@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.
- package/CONTRIBUTING.md +8 -0
- package/package.json +2 -1
- package/src/app.ts +290 -17
- package/src/app_core.ts +1833 -698
- package/src/app_local.ts +144 -4
- package/src/auto_tune.ts +62 -0
- package/src/bootstrap.ts +159 -1
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +116 -14
- package/src/db/db.ts +1201 -131
- package/src/db/schema.ts +308 -8
- package/src/foreground_activity.ts +55 -0
- package/src/index/indexer.ts +254 -124
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +789 -0
- package/src/index/secondary_indexer.ts +824 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +10 -12
- package/src/manifest.ts +143 -8
- package/src/memory.ts +183 -8
- package/src/metrics.ts +15 -29
- package/src/metrics_emitter.ts +26 -3
- package/src/notifier.ts +121 -5
- package/src/objectstore/accounting.ts +92 -0
- package/src/objectstore/mock_r2.ts +1 -1
- package/src/objectstore/r2.ts +17 -1
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +299 -0
- package/src/profiles/generic.ts +47 -0
- package/src/profiles/index.ts +205 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +85 -0
- package/src/profiles/profile.ts +225 -0
- package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
- package/src/profiles/stateProtocol/routes.ts +389 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +100 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2151 -164
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +235 -0
- package/src/schema/read_json.ts +43 -0
- package/src/schema/registry.ts +563 -59
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +389 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +313 -0
- package/src/search/companion_manager.ts +1086 -0
- package/src/search/companion_plan.ts +218 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +93 -2
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +108 -36
- package/src/segment/segmenter.ts +79 -5
- package/src/segment/segmenter_worker.ts +35 -6
- package/src/segment/segmenter_workers.ts +42 -12
- package/src/server.ts +150 -36
- package/src/sqlite/adapter.ts +185 -14
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +3 -3
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_metrics.ts +94 -64
- package/src/touch/live_templates.ts +15 -1
- package/src/touch/manager.ts +166 -88
- package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
- package/src/touch/spec.ts +95 -92
- package/src/touch/touch_journal.ts +4 -0
- package/src/touch/worker_pool.ts +8 -14
- package/src/touch/worker_protocol.ts +3 -3
- package/src/uploader.ts +77 -6
- package/src/util/bloom256.ts +2 -2
- package/src/util/byte_lru.ts +73 -0
- package/src/util/lru.ts +8 -0
- 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
|
|
5
|
-
|
|
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:
|
|
45
|
+
* - error: processing errors (useful for strict debugging)
|
|
36
46
|
*/
|
|
37
47
|
onMissingBefore?: "coarse" | "skipBefore" | "error";
|
|
38
48
|
/**
|
|
39
|
-
* Optional guardrail: when the
|
|
40
|
-
* exceeds this threshold, the
|
|
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
|
|
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
|
-
*
|
|
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
|
|
170
|
-
return Result.err({ kind: "
|
|
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,
|
|
188
|
+
): Result<number, TouchConfigValidationError> {
|
|
179
189
|
const n = value === undefined ? defaultValue : Number(value);
|
|
180
|
-
if (!Number.isFinite(n) || !predicate(n)) return
|
|
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,
|
|
199
|
+
): Result<number, TouchConfigValidationError> {
|
|
190
200
|
const n = value === undefined ? defaultValue : Number(value);
|
|
191
|
-
if (!Number.isFinite(n) || !Number.isInteger(n) || !predicate(n)) return
|
|
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,
|
|
196
|
-
if (!raw
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
249
|
+
return invalidTouch(`${fieldPath}.onMissingBefore must be coarse|skipBefore|error`);
|
|
230
250
|
}
|
|
231
251
|
|
|
232
|
-
const templates = 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
422
|
-
raw
|
|
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:
|
|
455
|
-
return !!cfg?.
|
|
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();
|
package/src/touch/worker_pool.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
processingMode?: "full" | "hotTemplatesOnly";
|
|
24
24
|
filterHotTemplates?: boolean;
|
|
25
25
|
hotTemplateIds?: string[] | null;
|
|
26
26
|
};
|