@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.
Files changed (85) 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 +1983 -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 +1440 -0
  15. package/src/db/schema.ts +619 -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 +459 -0
  54. package/src/touch/live_keys.ts +118 -0
  55. package/src/touch/live_metrics.ts +858 -0
  56. package/src/touch/live_templates.ts +619 -0
  57. package/src/touch/manager.ts +1341 -0
  58. package/src/touch/naming.ts +13 -0
  59. package/src/touch/routing_key_notifier.ts +275 -0
  60. package/src/touch/spec.ts +526 -0
  61. package/src/touch/touch_journal.ts +671 -0
  62. package/src/touch/touch_key_id.ts +20 -0
  63. package/src/touch/worker_pool.ts +189 -0
  64. package/src/touch/worker_protocol.ts +58 -0
  65. package/src/types/proper-lockfile.d.ts +1 -0
  66. package/src/uploader.ts +317 -0
  67. package/src/util/base32_crockford.ts +81 -0
  68. package/src/util/bloom256.ts +67 -0
  69. package/src/util/cleanup.ts +22 -0
  70. package/src/util/crc32c.ts +29 -0
  71. package/src/util/ds_error.ts +15 -0
  72. package/src/util/duration.ts +17 -0
  73. package/src/util/endian.ts +53 -0
  74. package/src/util/json_pointer.ts +148 -0
  75. package/src/util/log.ts +25 -0
  76. package/src/util/lru.ts +45 -0
  77. package/src/util/retry.ts +35 -0
  78. package/src/util/siphash.ts +71 -0
  79. package/src/util/stream_paths.ts +31 -0
  80. package/src/util/time.ts +14 -0
  81. package/src/util/yield.ts +3 -0
  82. package/build/index.d.mts +0 -1
  83. package/build/index.d.ts +0 -1
  84. package/build/index.js +0 -0
  85. package/build/index.mjs +0 -1
@@ -0,0 +1,13 @@
1
+ import type { TouchConfig } from "./spec.ts";
2
+
3
+ const DEFAULT_TOUCH_SUFFIX = ".__touch";
4
+
5
+ export function defaultTouchStreamName(sourceStream: string): string {
6
+ return `${sourceStream}${DEFAULT_TOUCH_SUFFIX}`;
7
+ }
8
+
9
+ export function resolveTouchStreamName(sourceStream: string, touch: TouchConfig): string {
10
+ const override = touch.derivedStream;
11
+ if (override && override.trim() !== "") return override;
12
+ return defaultTouchStreamName(sourceStream);
13
+ }
@@ -0,0 +1,275 @@
1
+ type Hit = { seq: bigint; key: string };
2
+
3
+ type Waiter = {
4
+ stream: string;
5
+ afterSeq: bigint;
6
+ keys: string[];
7
+ deadlineMs: number;
8
+ heapIndex: number;
9
+ done: boolean;
10
+ cleanup: (hit: Hit | null) => void;
11
+ };
12
+
13
+ type StreamState = {
14
+ byKey: Map<string, Set<Waiter>>;
15
+ activeWaiters: number;
16
+ };
17
+
18
+ export type RoutingKeyNotifierIntervalStats = {
19
+ timeoutsFired: number;
20
+ timeoutSweeps: number;
21
+ timeoutSweepMsSum: number;
22
+ timeoutSweepMsMax: number;
23
+ heapSize: number;
24
+ };
25
+
26
+ /**
27
+ * In-memory wait index keyed by (stream, routingKey).
28
+ *
29
+ * This is used to implement /touch/wait without waking *all* waiters on any
30
+ * new touch row. Instead, only waiters watching a touched key are woken.
31
+ *
32
+ * Note: this is best-effort and process-local (lost on restart). Callers must
33
+ * still do an initial DB scan for missed touches based on sinceTouchOffset.
34
+ */
35
+ export class RoutingKeyNotifier {
36
+ private readonly streams = new Map<string, StreamState>();
37
+
38
+ // Single global deadline heap + timer for waiter expiry.
39
+ private readonly deadlineHeap: Waiter[] = [];
40
+ private timer: any | null = null;
41
+ private scheduledDeadlineMs: number | null = null;
42
+
43
+ // Interval stats (resettable by the metrics system).
44
+ private interval: RoutingKeyNotifierIntervalStats = {
45
+ timeoutsFired: 0,
46
+ timeoutSweeps: 0,
47
+ timeoutSweepMsSum: 0,
48
+ timeoutSweepMsMax: 0,
49
+ heapSize: 0,
50
+ };
51
+
52
+ getActiveWaiters(stream: string): number {
53
+ return this.streams.get(stream)?.activeWaiters ?? 0;
54
+ }
55
+
56
+ snapshotAndResetIntervalStats(): RoutingKeyNotifierIntervalStats {
57
+ const out = { ...this.interval, heapSize: this.deadlineHeap.length };
58
+ this.interval = {
59
+ timeoutsFired: 0,
60
+ timeoutSweeps: 0,
61
+ timeoutSweepMsSum: 0,
62
+ timeoutSweepMsMax: 0,
63
+ heapSize: 0,
64
+ };
65
+ return out;
66
+ }
67
+
68
+ notify(stream: string, key: string, seq: bigint): void {
69
+ const st = this.streams.get(stream);
70
+ if (!st) return;
71
+ const set = st.byKey.get(key);
72
+ if (!set || set.size === 0) return;
73
+
74
+ // Resolve any waiters that are waiting strictly before this seq.
75
+ for (const w of set) {
76
+ if (w.done) continue;
77
+ if (seq > w.afterSeq) w.cleanup({ seq, key });
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Wait for any of `keys` to be notified at an offset > afterSeq.
83
+ *
84
+ * Returns:
85
+ * - `{seq, key}` when touched
86
+ * - `null` on timeout or abort
87
+ */
88
+ waitForAny(args: { stream: string; keys: string[]; afterSeq: bigint; timeoutMs: number; signal?: AbortSignal }): Promise<Hit | null> {
89
+ if (args.keys.length === 0) return Promise.resolve(null);
90
+ if (args.signal?.aborted) return Promise.resolve(null);
91
+ const timeoutMs = Math.max(0, Math.floor(args.timeoutMs));
92
+ if (timeoutMs <= 0) return Promise.resolve(null);
93
+ return new Promise((resolve) => {
94
+ const st = this.streams.get(args.stream) ?? { byKey: new Map<string, Set<Waiter>>(), activeWaiters: 0 };
95
+ const deadlineMs = Date.now() + timeoutMs;
96
+
97
+ const waiter: Waiter = {
98
+ stream: args.stream,
99
+ afterSeq: args.afterSeq,
100
+ keys: args.keys,
101
+ deadlineMs,
102
+ heapIndex: -1,
103
+ done: false,
104
+ cleanup: (hit) => {
105
+ if (waiter.done) return;
106
+ waiter.done = true;
107
+
108
+ // Remove from all key sets.
109
+ const current = this.streams.get(args.stream);
110
+ if (current) {
111
+ for (const k of waiter.keys) {
112
+ const s = current.byKey.get(k);
113
+ if (!s) continue;
114
+ s.delete(waiter);
115
+ if (s.size === 0) current.byKey.delete(k);
116
+ }
117
+ current.activeWaiters = Math.max(0, current.activeWaiters - 1);
118
+ if (current.byKey.size === 0 && current.activeWaiters === 0) this.streams.delete(args.stream);
119
+ }
120
+
121
+ const removedRoot = this.heapRemove(waiter);
122
+ if (args.signal) args.signal.removeEventListener("abort", onAbort);
123
+ if (removedRoot) this.rescheduleTimer();
124
+ resolve(hit);
125
+ },
126
+ };
127
+
128
+ // Register for each key.
129
+ for (const k of args.keys) {
130
+ const set = st.byKey.get(k) ?? new Set<Waiter>();
131
+ set.add(waiter);
132
+ st.byKey.set(k, set);
133
+ }
134
+ st.activeWaiters += 1;
135
+ this.streams.set(args.stream, st);
136
+
137
+ const onAbort = () => waiter.cleanup(null);
138
+ if (args.signal) args.signal.addEventListener("abort", onAbort, { once: true });
139
+
140
+ this.heapPush(waiter);
141
+ this.rescheduleTimer();
142
+ });
143
+ }
144
+
145
+ private rescheduleTimer(): void {
146
+ const next = this.deadlineHeap[0];
147
+ if (!next) {
148
+ if (this.timer) clearTimeout(this.timer);
149
+ this.timer = null;
150
+ this.scheduledDeadlineMs = null;
151
+ return;
152
+ }
153
+
154
+ // Avoid rescheduling unless the heap root changes (insert earlier or remove root).
155
+ if (this.timer && this.scheduledDeadlineMs != null && this.scheduledDeadlineMs === next.deadlineMs) return;
156
+
157
+ if (this.timer) clearTimeout(this.timer);
158
+ this.scheduledDeadlineMs = next.deadlineMs;
159
+ const delayMs = Math.max(0, next.deadlineMs - Date.now());
160
+ this.timer = setTimeout(() => this.expireDueWaiters(), delayMs);
161
+ }
162
+
163
+ private expireDueWaiters(): void {
164
+ this.timer = null;
165
+ this.scheduledDeadlineMs = null;
166
+
167
+ const start = Date.now();
168
+ const now = start;
169
+ let expired = 0;
170
+
171
+ for (;;) {
172
+ const head = this.deadlineHeap[0];
173
+ if (!head) break;
174
+ if (head.deadlineMs > now) break;
175
+
176
+ const w = this.heapPopMin();
177
+ if (!w) break;
178
+ if (w.done) continue;
179
+
180
+ expired += 1;
181
+ w.cleanup(null);
182
+ }
183
+
184
+ if (expired > 0) {
185
+ const sweepMs = Date.now() - start;
186
+ this.interval.timeoutsFired += expired;
187
+ this.interval.timeoutSweeps += 1;
188
+ this.interval.timeoutSweepMsSum += sweepMs;
189
+ this.interval.timeoutSweepMsMax = Math.max(this.interval.timeoutSweepMsMax, sweepMs);
190
+ }
191
+
192
+ this.rescheduleTimer();
193
+ }
194
+
195
+ private heapSwap(i: number, j: number): void {
196
+ const a = this.deadlineHeap[i]!;
197
+ const b = this.deadlineHeap[j]!;
198
+ this.deadlineHeap[i] = b;
199
+ this.deadlineHeap[j] = a;
200
+ a.heapIndex = j;
201
+ b.heapIndex = i;
202
+ }
203
+
204
+ private heapLess(i: number, j: number): boolean {
205
+ const a = this.deadlineHeap[i]!;
206
+ const b = this.deadlineHeap[j]!;
207
+ return a.deadlineMs < b.deadlineMs;
208
+ }
209
+
210
+ private heapSiftUp(i: number): void {
211
+ let idx = i;
212
+ while (idx > 0) {
213
+ const parent = (idx - 1) >> 1;
214
+ if (!this.heapLess(idx, parent)) break;
215
+ this.heapSwap(idx, parent);
216
+ idx = parent;
217
+ }
218
+ }
219
+
220
+ private heapSiftDown(i: number): void {
221
+ let idx = i;
222
+ for (;;) {
223
+ const left = idx * 2 + 1;
224
+ const right = left + 1;
225
+ if (left >= this.deadlineHeap.length) break;
226
+ let smallest = left;
227
+ if (right < this.deadlineHeap.length && this.heapLess(right, left)) smallest = right;
228
+ if (!this.heapLess(smallest, idx)) break;
229
+ this.heapSwap(idx, smallest);
230
+ idx = smallest;
231
+ }
232
+ }
233
+
234
+ private heapPush(w: Waiter): void {
235
+ if (w.heapIndex >= 0) return;
236
+ w.heapIndex = this.deadlineHeap.length;
237
+ this.deadlineHeap.push(w);
238
+ this.heapSiftUp(w.heapIndex);
239
+ }
240
+
241
+ // Returns true if the root was removed (useful to decide whether to reschedule the timer).
242
+ private heapRemove(w: Waiter): boolean {
243
+ const idx = w.heapIndex;
244
+ if (idx < 0) return false;
245
+
246
+ const lastIdx = this.deadlineHeap.length - 1;
247
+ const removedRoot = idx === 0;
248
+ if (idx !== lastIdx) this.heapSwap(idx, lastIdx);
249
+ this.deadlineHeap.pop();
250
+ w.heapIndex = -1;
251
+
252
+ if (idx < this.deadlineHeap.length) {
253
+ // Fix heap invariant for the element swapped into idx.
254
+ this.heapSiftDown(idx);
255
+ this.heapSiftUp(idx);
256
+ }
257
+ return removedRoot;
258
+ }
259
+
260
+ private heapPopMin(): Waiter | null {
261
+ if (this.deadlineHeap.length === 0) return null;
262
+ const w = this.deadlineHeap[0]!;
263
+ const last = this.deadlineHeap.length - 1;
264
+ if (last === 0) {
265
+ this.deadlineHeap.pop();
266
+ w.heapIndex = -1;
267
+ return w;
268
+ }
269
+ this.heapSwap(0, last);
270
+ this.deadlineHeap.pop();
271
+ w.heapIndex = -1;
272
+ this.heapSiftDown(0);
273
+ return w;
274
+ }
275
+ }