@tungthedev/streams-server 0.2.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.
Files changed (183) hide show
  1. package/CODE_OF_CONDUCT.md +45 -0
  2. package/CONTRIBUTING.md +76 -0
  3. package/LICENSE +201 -0
  4. package/README.md +58 -0
  5. package/SECURITY.md +42 -0
  6. package/bin/prisma-streams-server +2 -0
  7. package/package.json +46 -0
  8. package/src/app.ts +583 -0
  9. package/src/app_core.ts +3144 -0
  10. package/src/app_local.ts +206 -0
  11. package/src/auth.ts +124 -0
  12. package/src/auto_tune.ts +69 -0
  13. package/src/backpressure.ts +66 -0
  14. package/src/bootstrap.ts +613 -0
  15. package/src/compute/demo_entry.ts +415 -0
  16. package/src/compute/demo_site.ts +1242 -0
  17. package/src/compute/entry.ts +19 -0
  18. package/src/compute/package_entry.ts +4 -0
  19. package/src/compute/virtual-modules.d.ts +15 -0
  20. package/src/compute/worker_module_url.ts +9 -0
  21. package/src/concurrency_gate.ts +108 -0
  22. package/src/config.ts +402 -0
  23. package/src/db/bootstrap_store.ts +9 -0
  24. package/src/db/db.ts +2424 -0
  25. package/src/db/schema.ts +925 -0
  26. package/src/db/sqlite_manifest_snapshot.ts +81 -0
  27. package/src/db/sqlite_touch_store.ts +491 -0
  28. package/src/db/sqlite_wal_store.ts +472 -0
  29. package/src/details/full_mode_details.ts +568 -0
  30. package/src/expiry_sweeper.ts +47 -0
  31. package/src/foreground_activity.ts +55 -0
  32. package/src/hist.ts +169 -0
  33. package/src/index/binary_fuse.ts +379 -0
  34. package/src/index/indexer.ts +947 -0
  35. package/src/index/lexicon_file_cache.ts +261 -0
  36. package/src/index/lexicon_format.ts +93 -0
  37. package/src/index/lexicon_indexer.ts +863 -0
  38. package/src/index/run_cache.ts +84 -0
  39. package/src/index/run_format.ts +213 -0
  40. package/src/index/schedule.ts +28 -0
  41. package/src/index/secondary_indexer.ts +901 -0
  42. package/src/index/secondary_schema.ts +105 -0
  43. package/src/ingest.ts +309 -0
  44. package/src/lens/lens.ts +501 -0
  45. package/src/manifest.ts +249 -0
  46. package/src/memory.ts +334 -0
  47. package/src/metrics.ts +147 -0
  48. package/src/metrics_emitter.ts +83 -0
  49. package/src/notifier.ts +180 -0
  50. package/src/objectstore/accounting.ts +151 -0
  51. package/src/objectstore/interface.ts +13 -0
  52. package/src/objectstore/mock_r2.ts +269 -0
  53. package/src/objectstore/null.ts +32 -0
  54. package/src/objectstore/r2.ts +318 -0
  55. package/src/observe/pairing.ts +61 -0
  56. package/src/observe/request.ts +772 -0
  57. package/src/offset.ts +70 -0
  58. package/src/postgres/bootstrap.ts +269 -0
  59. package/src/postgres/companions.ts +197 -0
  60. package/src/postgres/control_restore.ts +109 -0
  61. package/src/postgres/details.ts +189 -0
  62. package/src/postgres/lexicon_index.ts +260 -0
  63. package/src/postgres/routing_index.ts +189 -0
  64. package/src/postgres/rows.ts +132 -0
  65. package/src/postgres/schema.ts +355 -0
  66. package/src/postgres/secondary_index.ts +238 -0
  67. package/src/postgres/segments.ts +900 -0
  68. package/src/postgres/stats.ts +103 -0
  69. package/src/postgres/store.ts +947 -0
  70. package/src/postgres/touch.ts +591 -0
  71. package/src/postgres/types.ts +32 -0
  72. package/src/profiles/evlog/schema.ts +234 -0
  73. package/src/profiles/evlog.ts +473 -0
  74. package/src/profiles/generic.ts +51 -0
  75. package/src/profiles/index.ts +237 -0
  76. package/src/profiles/metrics/block_format.ts +109 -0
  77. package/src/profiles/metrics/normalize.ts +366 -0
  78. package/src/profiles/metrics/schema.ts +319 -0
  79. package/src/profiles/metrics.ts +83 -0
  80. package/src/profiles/otelTraces/normalize.ts +955 -0
  81. package/src/profiles/otelTraces/otlp.ts +1002 -0
  82. package/src/profiles/otelTraces/schema.ts +408 -0
  83. package/src/profiles/otelTraces.ts +390 -0
  84. package/src/profiles/profile.ts +284 -0
  85. package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
  86. package/src/profiles/stateProtocol/changes.ts +24 -0
  87. package/src/profiles/stateProtocol/ingest.ts +115 -0
  88. package/src/profiles/stateProtocol/routes.ts +511 -0
  89. package/src/profiles/stateProtocol/types.ts +6 -0
  90. package/src/profiles/stateProtocol/validation.ts +51 -0
  91. package/src/profiles/stateProtocol.ts +107 -0
  92. package/src/read_filter.ts +468 -0
  93. package/src/reader.ts +2986 -0
  94. package/src/runtime/hash.ts +156 -0
  95. package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
  96. package/src/runtime/hash_vendor/NOTICE.md +8 -0
  97. package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
  98. package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
  99. package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
  100. package/src/runtime/host_runtime.ts +5 -0
  101. package/src/runtime_memory.ts +200 -0
  102. package/src/runtime_memory_sampler.ts +237 -0
  103. package/src/schema/lens_schema.ts +290 -0
  104. package/src/schema/proof.ts +547 -0
  105. package/src/schema/read_json.ts +51 -0
  106. package/src/schema/registry.ts +966 -0
  107. package/src/search/agg_format.ts +638 -0
  108. package/src/search/aggregate.ts +409 -0
  109. package/src/search/binary/codec.ts +162 -0
  110. package/src/search/binary/docset.ts +67 -0
  111. package/src/search/binary/restart_strings.ts +181 -0
  112. package/src/search/binary/varint.ts +34 -0
  113. package/src/search/bitset.ts +19 -0
  114. package/src/search/col_format.ts +382 -0
  115. package/src/search/col_runtime.ts +59 -0
  116. package/src/search/column_encoding.ts +43 -0
  117. package/src/search/companion_file_cache.ts +319 -0
  118. package/src/search/companion_format.ts +327 -0
  119. package/src/search/companion_manager.ts +1305 -0
  120. package/src/search/companion_plan.ts +229 -0
  121. package/src/search/exact_format.ts +281 -0
  122. package/src/search/exact_runtime.ts +55 -0
  123. package/src/search/fts_format.ts +423 -0
  124. package/src/search/fts_runtime.ts +333 -0
  125. package/src/search/query.ts +875 -0
  126. package/src/search/schema.ts +245 -0
  127. package/src/segment/cache.ts +270 -0
  128. package/src/segment/cached_segment.ts +89 -0
  129. package/src/segment/format.ts +403 -0
  130. package/src/segment/segmenter.ts +412 -0
  131. package/src/segment/segmenter_worker.ts +72 -0
  132. package/src/segment/segmenter_workers.ts +130 -0
  133. package/src/server.ts +264 -0
  134. package/src/server_auto_tune.ts +158 -0
  135. package/src/sqlite/adapter.ts +335 -0
  136. package/src/sqlite/runtime_stats.ts +163 -0
  137. package/src/stats.ts +205 -0
  138. package/src/store/append.ts +50 -0
  139. package/src/store/bootstrap_restore_store.ts +71 -0
  140. package/src/store/capabilities.ts +86 -0
  141. package/src/store/full_mode_details_store.ts +71 -0
  142. package/src/store/index_store.ts +104 -0
  143. package/src/store/profile_touch_store.ts +1 -0
  144. package/src/store/rows.ts +144 -0
  145. package/src/store/schema_profile_store.ts +73 -0
  146. package/src/store/schema_publication.ts +6 -0
  147. package/src/store/segment_manifest_store.ts +129 -0
  148. package/src/store/segment_read_store.ts +22 -0
  149. package/src/store/stats_accounting_store.ts +83 -0
  150. package/src/store/touch_store.ts +98 -0
  151. package/src/store/wal_store.ts +21 -0
  152. package/src/stream_size_reconciler.ts +100 -0
  153. package/src/touch/canonical_change.ts +7 -0
  154. package/src/touch/live_keys.ts +158 -0
  155. package/src/touch/live_metrics.ts +841 -0
  156. package/src/touch/live_templates.ts +449 -0
  157. package/src/touch/manager.ts +1292 -0
  158. package/src/touch/process_batch.ts +576 -0
  159. package/src/touch/processor_worker.ts +85 -0
  160. package/src/touch/spec.ts +459 -0
  161. package/src/touch/touch_journal.ts +771 -0
  162. package/src/touch/touch_key_id.ts +20 -0
  163. package/src/touch/worker_pool.ts +191 -0
  164. package/src/touch/worker_protocol.ts +57 -0
  165. package/src/types/proper-lockfile.d.ts +1 -0
  166. package/src/uploader.ts +358 -0
  167. package/src/util/base32_crockford.ts +81 -0
  168. package/src/util/bloom256.ts +67 -0
  169. package/src/util/byte_lru.ts +73 -0
  170. package/src/util/cleanup.ts +22 -0
  171. package/src/util/crc32c.ts +29 -0
  172. package/src/util/ds_error.ts +15 -0
  173. package/src/util/duration.ts +17 -0
  174. package/src/util/endian.ts +53 -0
  175. package/src/util/json_pointer.ts +148 -0
  176. package/src/util/log.ts +25 -0
  177. package/src/util/lru.ts +53 -0
  178. package/src/util/retry.ts +35 -0
  179. package/src/util/siphash.ts +71 -0
  180. package/src/util/stream_paths.ts +50 -0
  181. package/src/util/time.ts +14 -0
  182. package/src/util/yield.ts +3 -0
  183. package/src/util/zstd.ts +24 -0
@@ -0,0 +1,459 @@
1
+ import { Result } from "better-result";
2
+ import { dsError } from "../util/ds_error.ts";
3
+
4
+ export type TouchConfigValidationError = {
5
+ kind: "invalid_touch";
6
+ message: string;
7
+ };
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
+
25
+ export type TouchConfig = {
26
+ enabled: boolean;
27
+ /**
28
+ * Coarse invalidation interval. The server emits at most one table-touch per
29
+ * entity per interval.
30
+ *
31
+ * Default: 100ms.
32
+ */
33
+ coarseIntervalMs?: number;
34
+ /**
35
+ * Fine-touch coalescing window for watch keys.
36
+ *
37
+ * Default: 100ms.
38
+ */
39
+ touchCoalesceWindowMs?: number;
40
+ /**
41
+ * Policy when an update event is missing `old_value` (before image).
42
+ *
43
+ * - coarse: emit coarse table touches only (safe default)
44
+ * - skipBefore: compute fine touches from `value` only
45
+ * - error: processing errors (useful for strict debugging)
46
+ */
47
+ onMissingBefore?: "coarse" | "skipBefore" | "error";
48
+ /**
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
51
+ * (fine/template touches are suppressed) to preserve timeliness under overload.
52
+ *
53
+ * Default: 5000.
54
+ */
55
+ lagDegradeFineTouchesAtSourceOffsets?: number;
56
+ /**
57
+ * Hysteresis recovery threshold for lag-based degradation.
58
+ *
59
+ * When fine touches are currently suppressed due to lag, they are re-enabled
60
+ * only after lag falls to this threshold (or lower).
61
+ *
62
+ * Default: 1000.
63
+ */
64
+ lagRecoverFineTouchesAtSourceOffsets?: number;
65
+ /**
66
+ * Optional guardrail: cap fine/template touches emitted per processing batch.
67
+ * Table touches are always emitted for correctness.
68
+ *
69
+ * Default: 2000.
70
+ */
71
+ fineTouchBudgetPerBatch?: number;
72
+ /**
73
+ * Fine-touch token bucket refill rate (tokens/sec).
74
+ *
75
+ * Default: 200000.
76
+ */
77
+ fineTokensPerSecond?: number;
78
+ /**
79
+ * Fine-touch token bucket burst capacity (tokens).
80
+ *
81
+ * Default: 400000.
82
+ */
83
+ fineBurstTokens?: number;
84
+ /**
85
+ * When lag guardrails are active, reserve a small fine-touch budget per batch
86
+ * for currently hot keys/templates (premium lane). Set 0 to disable.
87
+ *
88
+ * Default: 200.
89
+ */
90
+ lagReservedFineTouchBudgetPerBatch?: number;
91
+ /**
92
+ * In-memory touch journal parameters.
93
+ */
94
+ memory?: {
95
+ /**
96
+ * Bucket duration. Cursor generations advance only on bucket flush.
97
+ *
98
+ * Default: 100ms.
99
+ */
100
+ bucketMs?: number;
101
+ /**
102
+ * Bloom filter size as a power of two (positions). Memory use per stream is
103
+ * `4 * 2^filterPow2` bytes.
104
+ *
105
+ * Default: 22 (16MiB).
106
+ */
107
+ filterPow2?: number;
108
+ /**
109
+ * Hash positions per key.
110
+ *
111
+ * Default: 4.
112
+ */
113
+ k?: number;
114
+ /**
115
+ * Hard cap on unique keys tracked per bucket. If exceeded, the bucket is
116
+ * treated as a broadcast invalidation (wake all waiters) to avoid false negatives.
117
+ *
118
+ * Default: 100000.
119
+ */
120
+ pendingMaxKeys?: number;
121
+ /**
122
+ * Maximum keys per /touch/wait to index per-key. Larger keysets are treated
123
+ * as "broad" and are scanned on each bucket flush.
124
+ *
125
+ * Default: 32.
126
+ */
127
+ keyIndexMaxKeys?: number;
128
+ /**
129
+ * Sliding TTL for "hot" fine keys observed from /touch/wait.
130
+ *
131
+ * Default: 10000ms.
132
+ */
133
+ hotKeyTtlMs?: number;
134
+ /**
135
+ * Sliding TTL for "hot" templates observed from templateIdsUsed.
136
+ *
137
+ * Default: 10000ms.
138
+ */
139
+ hotTemplateTtlMs?: number;
140
+ /**
141
+ * Upper bound for hot fine key tracking per stream.
142
+ *
143
+ * Default: 1000000.
144
+ */
145
+ hotMaxKeys?: number;
146
+ /**
147
+ * Upper bound for hot template tracking per stream.
148
+ *
149
+ * Default: 4096.
150
+ */
151
+ hotMaxTemplates?: number;
152
+ };
153
+ templates?: {
154
+ /**
155
+ * Sliding inactivity TTL for templates, measured since last use.
156
+ * Individual activations may override this TTL.
157
+ *
158
+ * Default: 1 hour.
159
+ */
160
+ defaultInactivityTtlMs?: number;
161
+ /**
162
+ * Persist last-seen timestamps at most once per interval per template.
163
+ *
164
+ * Default: 5 minutes.
165
+ */
166
+ lastSeenPersistIntervalMs?: number;
167
+ /**
168
+ * Template GC interval.
169
+ *
170
+ * Default: 1 minute.
171
+ */
172
+ gcIntervalMs?: number;
173
+ maxActiveTemplatesPerEntity?: number;
174
+ maxActiveTemplatesPerStream?: number;
175
+ activationRateLimitPerMinute?: number;
176
+ };
177
+ };
178
+
179
+ function invalidTouch<T = never>(message: string): Result<T, TouchConfigValidationError> {
180
+ return Result.err({ kind: "invalid_touch", message });
181
+ }
182
+
183
+ function parseNumberField(
184
+ value: any,
185
+ defaultValue: number,
186
+ message: string,
187
+ predicate: (n: number) => boolean
188
+ ): Result<number, TouchConfigValidationError> {
189
+ const n = value === undefined ? defaultValue : Number(value);
190
+ if (!Number.isFinite(n) || !predicate(n)) return invalidTouch(message);
191
+ return Result.ok(n);
192
+ }
193
+
194
+ function parseIntegerField(
195
+ value: any,
196
+ defaultValue: number,
197
+ message: string,
198
+ predicate: (n: number) => boolean
199
+ ): Result<number, TouchConfigValidationError> {
200
+ const n = value === undefined ? defaultValue : Number(value);
201
+ if (!Number.isFinite(n) || !Number.isInteger(n) || !predicate(n)) return invalidTouch(message);
202
+ return Result.ok(n);
203
+ }
204
+
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
+
227
+ const enabled = !!raw.enabled;
228
+ if (!enabled) {
229
+ return Result.ok({ enabled: false });
230
+ }
231
+
232
+ const coarseIntervalMsRes = parseNumberField(
233
+ raw.coarseIntervalMs,
234
+ 100,
235
+ `${fieldPath}.coarseIntervalMs must be > 0`,
236
+ (n) => n > 0
237
+ );
238
+ if (Result.isError(coarseIntervalMsRes)) return coarseIntervalMsRes;
239
+ const touchCoalesceWindowMsRes = parseNumberField(
240
+ raw.touchCoalesceWindowMs,
241
+ 100,
242
+ `${fieldPath}.touchCoalesceWindowMs must be > 0`,
243
+ (n) => n > 0
244
+ );
245
+ if (Result.isError(touchCoalesceWindowMsRes)) return touchCoalesceWindowMsRes;
246
+
247
+ const onMissingBefore = raw.onMissingBefore === undefined ? "coarse" : raw.onMissingBefore;
248
+ if (onMissingBefore !== "coarse" && onMissingBefore !== "skipBefore" && onMissingBefore !== "error") {
249
+ return invalidTouch(`${fieldPath}.onMissingBefore must be coarse|skipBefore|error`);
250
+ }
251
+
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;
260
+ const defaultInactivityTtlMsRes = parseNumberField(
261
+ templates.defaultInactivityTtlMs,
262
+ 60 * 60 * 1000,
263
+ `${fieldPath}.templates.defaultInactivityTtlMs must be >= 0`,
264
+ (n) => n >= 0
265
+ );
266
+ if (Result.isError(defaultInactivityTtlMsRes)) return defaultInactivityTtlMsRes;
267
+ const lastSeenPersistIntervalMsRes = parseNumberField(
268
+ templates.lastSeenPersistIntervalMs,
269
+ 5 * 60 * 1000,
270
+ `${fieldPath}.templates.lastSeenPersistIntervalMs must be > 0`,
271
+ (n) => n > 0
272
+ );
273
+ if (Result.isError(lastSeenPersistIntervalMsRes)) return lastSeenPersistIntervalMsRes;
274
+ const gcIntervalMsRes = parseNumberField(
275
+ templates.gcIntervalMs,
276
+ 60_000,
277
+ `${fieldPath}.templates.gcIntervalMs must be > 0`,
278
+ (n) => n > 0
279
+ );
280
+ if (Result.isError(gcIntervalMsRes)) return gcIntervalMsRes;
281
+ const maxActiveTemplatesPerEntityRes = parseNumberField(
282
+ templates.maxActiveTemplatesPerEntity,
283
+ 256,
284
+ `${fieldPath}.templates.maxActiveTemplatesPerEntity must be > 0`,
285
+ (n) => n > 0
286
+ );
287
+ if (Result.isError(maxActiveTemplatesPerEntityRes)) return maxActiveTemplatesPerEntityRes;
288
+ const maxActiveTemplatesPerStreamRes = parseNumberField(
289
+ templates.maxActiveTemplatesPerStream,
290
+ 2048,
291
+ `${fieldPath}.templates.maxActiveTemplatesPerStream must be > 0`,
292
+ (n) => n > 0
293
+ );
294
+ if (Result.isError(maxActiveTemplatesPerStreamRes)) return maxActiveTemplatesPerStreamRes;
295
+ const activationRateLimitPerMinuteRes = parseNumberField(
296
+ templates.activationRateLimitPerMinute,
297
+ 100,
298
+ `${fieldPath}.templates.activationRateLimitPerMinute must be >= 0`,
299
+ (n) => n >= 0
300
+ );
301
+ if (Result.isError(activationRateLimitPerMinuteRes)) return activationRateLimitPerMinuteRes;
302
+
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;
311
+ const bucketMsRes = parseIntegerField(
312
+ memoryRaw.bucketMs,
313
+ 100,
314
+ `${fieldPath}.memory.bucketMs must be an integer > 0`,
315
+ (n) => n > 0
316
+ );
317
+ if (Result.isError(bucketMsRes)) return bucketMsRes;
318
+ const filterPow2Res = parseIntegerField(
319
+ memoryRaw.filterPow2,
320
+ 22,
321
+ `${fieldPath}.memory.filterPow2 must be an integer in [10,30]`,
322
+ (n) => n >= 10 && n <= 30
323
+ );
324
+ if (Result.isError(filterPow2Res)) return filterPow2Res;
325
+ const kRes = parseIntegerField(
326
+ memoryRaw.k,
327
+ 4,
328
+ `${fieldPath}.memory.k must be an integer in [1,8]`,
329
+ (n) => n >= 1 && n <= 8
330
+ );
331
+ if (Result.isError(kRes)) return kRes;
332
+ const pendingMaxKeysRes = parseIntegerField(
333
+ memoryRaw.pendingMaxKeys,
334
+ 100_000,
335
+ `${fieldPath}.memory.pendingMaxKeys must be an integer > 0`,
336
+ (n) => n > 0
337
+ );
338
+ if (Result.isError(pendingMaxKeysRes)) return pendingMaxKeysRes;
339
+ const keyIndexMaxKeysRes = parseIntegerField(
340
+ memoryRaw.keyIndexMaxKeys,
341
+ 32,
342
+ `${fieldPath}.memory.keyIndexMaxKeys must be an integer in [1,1024]`,
343
+ (n) => n >= 1 && n <= 1024
344
+ );
345
+ if (Result.isError(keyIndexMaxKeysRes)) return keyIndexMaxKeysRes;
346
+ const hotKeyTtlMsRes = parseIntegerField(
347
+ memoryRaw.hotKeyTtlMs,
348
+ 10_000,
349
+ `${fieldPath}.memory.hotKeyTtlMs must be an integer > 0`,
350
+ (n) => n > 0
351
+ );
352
+ if (Result.isError(hotKeyTtlMsRes)) return hotKeyTtlMsRes;
353
+ const hotTemplateTtlMsRes = parseIntegerField(
354
+ memoryRaw.hotTemplateTtlMs,
355
+ 10_000,
356
+ `${fieldPath}.memory.hotTemplateTtlMs must be an integer > 0`,
357
+ (n) => n > 0
358
+ );
359
+ if (Result.isError(hotTemplateTtlMsRes)) return hotTemplateTtlMsRes;
360
+ const hotMaxKeysRes = parseIntegerField(
361
+ memoryRaw.hotMaxKeys,
362
+ 1_000_000,
363
+ `${fieldPath}.memory.hotMaxKeys must be an integer > 0`,
364
+ (n) => n > 0
365
+ );
366
+ if (Result.isError(hotMaxKeysRes)) return hotMaxKeysRes;
367
+ const hotMaxTemplatesRes = parseIntegerField(
368
+ memoryRaw.hotMaxTemplates,
369
+ 4096,
370
+ `${fieldPath}.memory.hotMaxTemplates must be an integer > 0`,
371
+ (n) => n > 0
372
+ );
373
+ if (Result.isError(hotMaxTemplatesRes)) return hotMaxTemplatesRes;
374
+
375
+ const lagDegradeFineTouchesAtSourceOffsetsRes = parseIntegerField(
376
+ raw.lagDegradeFineTouchesAtSourceOffsets,
377
+ 5000,
378
+ `${fieldPath}.lagDegradeFineTouchesAtSourceOffsets must be an integer >= 0`,
379
+ (n) => n >= 0
380
+ );
381
+ if (Result.isError(lagDegradeFineTouchesAtSourceOffsetsRes)) return lagDegradeFineTouchesAtSourceOffsetsRes;
382
+ const lagRecoverFineTouchesAtSourceOffsetsRes = parseIntegerField(
383
+ raw.lagRecoverFineTouchesAtSourceOffsets,
384
+ 1000,
385
+ `${fieldPath}.lagRecoverFineTouchesAtSourceOffsets must be an integer >= 0`,
386
+ (n) => n >= 0
387
+ );
388
+ if (Result.isError(lagRecoverFineTouchesAtSourceOffsetsRes)) return lagRecoverFineTouchesAtSourceOffsetsRes;
389
+ const fineTouchBudgetPerBatchRes = parseIntegerField(
390
+ raw.fineTouchBudgetPerBatch,
391
+ 2000,
392
+ `${fieldPath}.fineTouchBudgetPerBatch must be an integer >= 0`,
393
+ (n) => n >= 0
394
+ );
395
+ if (Result.isError(fineTouchBudgetPerBatchRes)) return fineTouchBudgetPerBatchRes;
396
+ const fineTokensPerSecondRes = parseIntegerField(
397
+ raw.fineTokensPerSecond,
398
+ 200_000,
399
+ `${fieldPath}.fineTokensPerSecond must be an integer >= 0`,
400
+ (n) => n >= 0
401
+ );
402
+ if (Result.isError(fineTokensPerSecondRes)) return fineTokensPerSecondRes;
403
+ const fineBurstTokensRes = parseIntegerField(
404
+ raw.fineBurstTokens,
405
+ 400_000,
406
+ `${fieldPath}.fineBurstTokens must be an integer >= 0`,
407
+ (n) => n >= 0
408
+ );
409
+ if (Result.isError(fineBurstTokensRes)) return fineBurstTokensRes;
410
+ const lagReservedFineTouchBudgetPerBatchRes = parseIntegerField(
411
+ raw.lagReservedFineTouchBudgetPerBatch,
412
+ 200,
413
+ `${fieldPath}.lagReservedFineTouchBudgetPerBatch must be an integer >= 0`,
414
+ (n) => n >= 0
415
+ );
416
+ if (Result.isError(lagReservedFineTouchBudgetPerBatchRes)) return lagReservedFineTouchBudgetPerBatchRes;
417
+
418
+ return Result.ok({
419
+ enabled: true,
420
+ coarseIntervalMs: coarseIntervalMsRes.value,
421
+ touchCoalesceWindowMs: touchCoalesceWindowMsRes.value,
422
+ onMissingBefore,
423
+ lagDegradeFineTouchesAtSourceOffsets: lagDegradeFineTouchesAtSourceOffsetsRes.value,
424
+ lagRecoverFineTouchesAtSourceOffsets: lagRecoverFineTouchesAtSourceOffsetsRes.value,
425
+ fineTouchBudgetPerBatch: fineTouchBudgetPerBatchRes.value,
426
+ fineTokensPerSecond: fineTokensPerSecondRes.value,
427
+ fineBurstTokens: fineBurstTokensRes.value,
428
+ lagReservedFineTouchBudgetPerBatch: lagReservedFineTouchBudgetPerBatchRes.value,
429
+ memory: {
430
+ bucketMs: bucketMsRes.value,
431
+ filterPow2: filterPow2Res.value,
432
+ k: kRes.value,
433
+ pendingMaxKeys: pendingMaxKeysRes.value,
434
+ keyIndexMaxKeys: keyIndexMaxKeysRes.value,
435
+ hotKeyTtlMs: hotKeyTtlMsRes.value,
436
+ hotTemplateTtlMs: hotTemplateTtlMsRes.value,
437
+ hotMaxKeys: hotMaxKeysRes.value,
438
+ hotMaxTemplates: hotMaxTemplatesRes.value,
439
+ },
440
+ templates: {
441
+ defaultInactivityTtlMs: defaultInactivityTtlMsRes.value,
442
+ lastSeenPersistIntervalMs: lastSeenPersistIntervalMsRes.value,
443
+ gcIntervalMs: gcIntervalMsRes.value,
444
+ maxActiveTemplatesPerEntity: maxActiveTemplatesPerEntityRes.value,
445
+ maxActiveTemplatesPerStream: maxActiveTemplatesPerStreamRes.value,
446
+ activationRateLimitPerMinute: activationRateLimitPerMinuteRes.value,
447
+ },
448
+ });
449
+ }
450
+
451
+ export function validateTouchConfig(raw: any, fieldPath = "touch"): TouchConfig {
452
+ const res = validateTouchConfigResult(raw, fieldPath);
453
+ if (Result.isError(res)) throw dsError(res.error.message);
454
+ return res.value;
455
+ }
456
+
457
+ export function isTouchEnabled(cfg: TouchConfig | undefined): cfg is TouchConfig & { enabled: true } {
458
+ return !!cfg?.enabled;
459
+ }