@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,613 @@
1
+ import { mkdirSync, rmSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import type { Config } from "./config";
4
+ import { createSqliteBootstrapRestoreStore } from "./db/bootstrap_store";
5
+ import type { ObjectStore } from "./objectstore/interface";
6
+ import type { BootstrapRestoreStore } from "./store/bootstrap_restore_store";
7
+ import { zstdDecompressSync } from "./util/zstd";
8
+ import {
9
+ localSegmentPath,
10
+ schemaObjectKey,
11
+ segmentObjectKey,
12
+ streamHash16Hex,
13
+ } from "./util/stream_paths";
14
+ import { retry } from "./util/retry";
15
+ import { dsError } from "./util/ds_error.ts";
16
+ import { resolveTouchCapability, type StreamProfileSpec } from "./profiles";
17
+
18
+ type Manifest = Record<string, any>;
19
+
20
+ export async function bootstrapFromR2(
21
+ cfg: Config,
22
+ store: ObjectStore,
23
+ opts: { clearLocal?: boolean } = {},
24
+ ): Promise<void> {
25
+ if (opts.clearLocal !== false) {
26
+ try {
27
+ rmSync(cfg.dbPath, { force: true });
28
+ } catch {
29
+ // ignore
30
+ }
31
+ try {
32
+ rmSync(`${cfg.rootDir}/local`, { recursive: true, force: true });
33
+ } catch {
34
+ // ignore
35
+ }
36
+ try {
37
+ rmSync(`${cfg.rootDir}/cache`, { recursive: true, force: true });
38
+ } catch {
39
+ // ignore
40
+ }
41
+ }
42
+
43
+ mkdirSync(cfg.rootDir, { recursive: true });
44
+
45
+ const db = createSqliteBootstrapRestoreStore(cfg.dbPath, {
46
+ cacheBytes: cfg.sqliteCacheBytes,
47
+ });
48
+ try {
49
+ await bootstrapObjectStoreIntoRestoreStore(cfg, store, db);
50
+ } finally {
51
+ await db.close();
52
+ }
53
+ }
54
+
55
+ export async function bootstrapObjectStoreIntoRestoreStore(
56
+ cfg: Config,
57
+ store: ObjectStore,
58
+ db: BootstrapRestoreStore,
59
+ ): Promise<void> {
60
+ const retryOpts = {
61
+ retries: cfg.objectStoreRetries,
62
+ baseDelayMs: cfg.objectStoreBaseDelayMs,
63
+ maxDelayMs: cfg.objectStoreMaxDelayMs,
64
+ timeoutMs: cfg.objectStoreTimeoutMs,
65
+ };
66
+ const keys = await retry(() => store.list("streams/"), retryOpts);
67
+ const manifestKeys = keys.filter((k) => k.endsWith("/manifest.json"));
68
+ for (const mkey of manifestKeys) {
69
+ const mbytes = await retry(async () => {
70
+ const data = await store.get(mkey);
71
+ if (!data) throw dsError(`missing manifest ${mkey}`);
72
+ return data;
73
+ }, retryOpts);
74
+ const manifest = JSON.parse(new TextDecoder().decode(mbytes)) as Manifest;
75
+ const stream = String(manifest.name ?? "");
76
+ if (!stream) continue;
77
+
78
+ const shash = streamHash16Hex(stream);
79
+ const nowMs = db.nowMs();
80
+
81
+ const createdAtMs = parseIsoMs(manifest.created_at) ?? nowMs;
82
+ const expiresAtMs = parseIsoMs(manifest.expires_at);
83
+ const epoch = typeof manifest.epoch === "number" ? manifest.epoch : 0;
84
+ const nextOffsetNum =
85
+ typeof manifest.next_offset === "number" ? manifest.next_offset : 0;
86
+ const nextOffset = BigInt(nextOffsetNum);
87
+ const logicalSizeBytes =
88
+ parseManifestBigInt(manifest.logical_size_bytes) ?? 0n;
89
+
90
+ const contentType =
91
+ typeof manifest.content_type === "string"
92
+ ? manifest.content_type
93
+ : "application/octet-stream";
94
+ const profile =
95
+ typeof manifest.profile === "string" && manifest.profile !== ""
96
+ ? manifest.profile
97
+ : "generic";
98
+ const profileJson =
99
+ manifest.profile_json && typeof manifest.profile_json === "object"
100
+ ? manifest.profile_json
101
+ : null;
102
+ const streamSeq =
103
+ typeof manifest.stream_seq === "string" ? manifest.stream_seq : null;
104
+ const closed = typeof manifest.closed === "number" ? manifest.closed : 0;
105
+ const closedProducerId =
106
+ typeof manifest.closed_producer_id === "string"
107
+ ? manifest.closed_producer_id
108
+ : null;
109
+ const closedProducerEpoch =
110
+ typeof manifest.closed_producer_epoch === "number"
111
+ ? manifest.closed_producer_epoch
112
+ : null;
113
+ const closedProducerSeq =
114
+ typeof manifest.closed_producer_seq === "number"
115
+ ? manifest.closed_producer_seq
116
+ : null;
117
+ const ttlSeconds =
118
+ typeof manifest.ttl_seconds === "number" ? manifest.ttl_seconds : null;
119
+ const streamFlags =
120
+ typeof manifest.stream_flags === "number" ? manifest.stream_flags : 0;
121
+
122
+ const segmentOffsetsBytes = decodeZstdBase64(
123
+ manifest.segment_offsets ?? "",
124
+ );
125
+ const segmentBlocksBytes = decodeZstdBase64(manifest.segment_blocks ?? "");
126
+ const segmentLastTsBytes = decodeZstdBase64(manifest.segment_last_ts ?? "");
127
+ const segmentOffsets = decodeU64LeArray(segmentOffsetsBytes);
128
+ const segmentBlocks = decodeU32LeArray(segmentBlocksBytes);
129
+ const segmentLastTs = decodeU64LeArray(segmentLastTsBytes);
130
+ const segmentCount =
131
+ typeof manifest.segment_count === "number"
132
+ ? manifest.segment_count
133
+ : segmentOffsets.length;
134
+
135
+ if (
136
+ segmentOffsets.length !== segmentCount ||
137
+ segmentBlocks.length !== segmentCount ||
138
+ segmentLastTs.length !== segmentCount
139
+ ) {
140
+ throw dsError(`manifest array length mismatch for ${stream}`);
141
+ }
142
+
143
+ const lastEndOffset =
144
+ segmentCount > 0 ? segmentOffsets[segmentCount - 1] - 1n : -1n;
145
+ const uploadedPrefix =
146
+ typeof manifest.uploaded_through === "number"
147
+ ? manifest.uploaded_through
148
+ : segmentCount;
149
+ const uploadedThrough =
150
+ uploadedPrefix > 0 && uploadedPrefix <= segmentOffsets.length
151
+ ? segmentOffsets[uploadedPrefix - 1] - 1n
152
+ : -1n;
153
+ const lastAppendMs =
154
+ segmentCount > 0 ? segmentLastTs[segmentCount - 1] / 1_000_000n : nowMs;
155
+
156
+ const manifestHead = await retry(async () => {
157
+ const head = await store.head(mkey);
158
+ if (!head) throw dsError(`missing manifest head ${mkey}`);
159
+ return head;
160
+ }, retryOpts);
161
+ const restoredSegments: Array<{
162
+ row: {
163
+ segmentId: string;
164
+ stream: string;
165
+ segmentIndex: number;
166
+ startOffset: bigint;
167
+ endOffset: bigint;
168
+ blockCount: number;
169
+ lastAppendMs: bigint;
170
+ payloadBytes: bigint;
171
+ sizeBytes: number;
172
+ localPath: string;
173
+ };
174
+ etag: string;
175
+ }> = [];
176
+ for (let i = 0; i < segmentCount; i++) {
177
+ const startOffset = i === 0 ? 0n : segmentOffsets[i - 1];
178
+ const endOffset = segmentOffsets[i] - 1n;
179
+ const lastTsMs = segmentLastTs[i] / 1_000_000n;
180
+ const localPath = localSegmentPath(cfg.rootDir, shash, i);
181
+ const segmentId = `${shash}-${i}-${startOffset.toString()}-${endOffset.toString()}`;
182
+ mkdirSync(dirname(localPath), { recursive: true });
183
+ const objectKey = segmentObjectKey(shash, i);
184
+ const head = await retry(async () => {
185
+ const h = await store.head(objectKey);
186
+ if (!h) throw dsError(`missing segment ${objectKey}`);
187
+ return h;
188
+ }, retryOpts);
189
+ if (!head) throw dsError(`missing segment ${objectKey}`);
190
+ restoredSegments.push({
191
+ row: {
192
+ segmentId,
193
+ stream,
194
+ segmentIndex: i,
195
+ startOffset,
196
+ endOffset,
197
+ blockCount: segmentBlocks[i],
198
+ lastAppendMs: lastTsMs,
199
+ payloadBytes: 0n,
200
+ sizeBytes: head.size,
201
+ localPath,
202
+ },
203
+ etag: head.etag,
204
+ });
205
+ }
206
+ const schemaKey = schemaObjectKey(shash);
207
+ const schemaBytes = await retry(async () => {
208
+ const data = await store.get(schemaKey);
209
+ if (!data) return null;
210
+ return data;
211
+ }, retryOpts);
212
+
213
+ let restoreStarted = false;
214
+ try {
215
+ await db.beginRestoreStream?.(stream);
216
+ restoreStarted = true;
217
+
218
+ await db.restoreStreamRow({
219
+ stream,
220
+ created_at_ms: createdAtMs,
221
+ updated_at_ms: nowMs,
222
+ content_type: contentType,
223
+ profile,
224
+ stream_seq: streamSeq,
225
+ closed,
226
+ closed_producer_id: closedProducerId,
227
+ closed_producer_epoch: closedProducerEpoch,
228
+ closed_producer_seq: closedProducerSeq,
229
+ ttl_seconds: ttlSeconds,
230
+ epoch,
231
+ next_offset: nextOffset,
232
+ sealed_through: lastEndOffset,
233
+ uploaded_through: uploadedThrough,
234
+ uploaded_segment_count: uploadedPrefix,
235
+ pending_rows: 0n,
236
+ pending_bytes: 0n,
237
+ logical_size_bytes: logicalSizeBytes,
238
+ wal_rows: 0n,
239
+ wal_bytes: 0n,
240
+ last_append_ms: lastAppendMs,
241
+ last_segment_cut_ms: lastAppendMs,
242
+ segment_in_progress: 0,
243
+ expires_at_ms: expiresAtMs,
244
+ stream_flags: streamFlags,
245
+ });
246
+ if (profileJson) {
247
+ await db.upsertStreamProfile(stream, JSON.stringify(profileJson));
248
+ const profile = profileJson as StreamProfileSpec;
249
+ const touchCapability = resolveTouchCapability(profile);
250
+ if (touchCapability)
251
+ await touchCapability.syncState({ db: db.touch, stream, profile });
252
+ else await db.touch.deleteStreamTouchState(stream);
253
+ } else {
254
+ await db.deleteStreamProfile(stream);
255
+ await db.touch.deleteStreamTouchState(stream);
256
+ }
257
+
258
+ await db.upsertSegmentMeta(
259
+ stream,
260
+ segmentCount,
261
+ segmentOffsetsBytes,
262
+ segmentBlocksBytes,
263
+ segmentLastTsBytes,
264
+ );
265
+
266
+ await db.upsertManifestRow(
267
+ stream,
268
+ Number(manifest.generation ?? 0),
269
+ Number(manifest.generation ?? 0),
270
+ nowMs,
271
+ manifestHead?.etag ?? null,
272
+ manifestHead?.size ?? null,
273
+ );
274
+
275
+ for (const segment of restoredSegments) {
276
+ await db.createSegmentRow(segment.row);
277
+ await db.markSegmentUploaded(
278
+ segment.row.segmentId,
279
+ segment.etag,
280
+ nowMs,
281
+ );
282
+ }
283
+
284
+ const indexSecretB64 =
285
+ typeof manifest.index_secret === "string" ? manifest.index_secret : "";
286
+ if (indexSecretB64) {
287
+ const secret = new Uint8Array(Buffer.from(indexSecretB64, "base64"));
288
+ const indexedThrough =
289
+ typeof manifest.indexed_through === "number"
290
+ ? manifest.indexed_through
291
+ : 0;
292
+ await db.upsertIndexState(stream, secret, indexedThrough);
293
+ }
294
+
295
+ const activeRuns = Array.isArray(manifest.active_runs)
296
+ ? manifest.active_runs
297
+ : [];
298
+ const retiredRuns = Array.isArray(manifest.retired_runs)
299
+ ? manifest.retired_runs
300
+ : [];
301
+ for (const r of activeRuns) {
302
+ await db.insertIndexRun({
303
+ run_id: String(r.run_id),
304
+ stream,
305
+ level: Number(r.level),
306
+ start_segment: Number(r.start_segment),
307
+ end_segment: Number(r.end_segment),
308
+ object_key: String(r.object_key),
309
+ size_bytes: Number(r.size_bytes ?? 0),
310
+ filter_len: Number(r.filter_len ?? 0),
311
+ record_count: Number(r.record_count ?? 0),
312
+ });
313
+ }
314
+ for (const r of retiredRuns) {
315
+ const runId = String(r.run_id);
316
+ await db.insertIndexRun({
317
+ run_id: runId,
318
+ stream,
319
+ level: Number(r.level),
320
+ start_segment: Number(r.start_segment),
321
+ end_segment: Number(r.end_segment),
322
+ object_key: String(r.object_key),
323
+ size_bytes: Number(r.size_bytes ?? 0),
324
+ filter_len: Number(r.filter_len ?? 0),
325
+ record_count: Number(r.record_count ?? 0),
326
+ });
327
+ const retiredGen =
328
+ typeof r.retired_gen === "number"
329
+ ? r.retired_gen
330
+ : Number(manifest.generation ?? 0);
331
+ const retiredAtUnix =
332
+ typeof r.retired_at_unix === "number"
333
+ ? r.retired_at_unix
334
+ : Math.floor(Number(nowMs) / 1000);
335
+ await db.retireIndexRuns(
336
+ [runId],
337
+ retiredGen,
338
+ BigInt(retiredAtUnix) * 1000n,
339
+ );
340
+ }
341
+
342
+ const secondaryIndexes =
343
+ manifest.secondary_indexes &&
344
+ typeof manifest.secondary_indexes === "object"
345
+ ? manifest.secondary_indexes
346
+ : {};
347
+ for (const [indexName, rawState] of Object.entries(secondaryIndexes)) {
348
+ if (!rawState || typeof rawState !== "object") continue;
349
+ const indexSecretB64 =
350
+ typeof (rawState as any).index_secret === "string"
351
+ ? (rawState as any).index_secret
352
+ : "";
353
+ if (!indexSecretB64) continue;
354
+ const secret = new Uint8Array(Buffer.from(indexSecretB64, "base64"));
355
+ const configHash =
356
+ typeof (rawState as any).config_hash === "string"
357
+ ? (rawState as any).config_hash
358
+ : "";
359
+ const indexedThrough =
360
+ typeof (rawState as any).indexed_through === "number"
361
+ ? Number((rawState as any).indexed_through)
362
+ : 0;
363
+ await db.upsertSecondaryIndexState(
364
+ stream,
365
+ indexName,
366
+ secret,
367
+ configHash,
368
+ indexedThrough,
369
+ );
370
+
371
+ const activeSecondaryRuns = Array.isArray((rawState as any).active_runs)
372
+ ? (rawState as any).active_runs
373
+ : [];
374
+ const retiredSecondaryRuns = Array.isArray(
375
+ (rawState as any).retired_runs,
376
+ )
377
+ ? (rawState as any).retired_runs
378
+ : [];
379
+ for (const run of activeSecondaryRuns) {
380
+ await db.insertSecondaryIndexRun({
381
+ run_id: String(run.run_id),
382
+ stream,
383
+ index_name: indexName,
384
+ level: Number(run.level),
385
+ start_segment: Number(run.start_segment),
386
+ end_segment: Number(run.end_segment),
387
+ object_key: String(run.object_key),
388
+ size_bytes: Number(run.size_bytes ?? 0),
389
+ filter_len: Number(run.filter_len ?? 0),
390
+ record_count: Number(run.record_count ?? 0),
391
+ });
392
+ }
393
+ for (const run of retiredSecondaryRuns) {
394
+ const runId = String(run.run_id);
395
+ await db.insertSecondaryIndexRun({
396
+ run_id: runId,
397
+ stream,
398
+ index_name: indexName,
399
+ level: Number(run.level),
400
+ start_segment: Number(run.start_segment),
401
+ end_segment: Number(run.end_segment),
402
+ object_key: String(run.object_key),
403
+ size_bytes: Number(run.size_bytes ?? 0),
404
+ filter_len: Number(run.filter_len ?? 0),
405
+ record_count: Number(run.record_count ?? 0),
406
+ });
407
+ const retiredGen =
408
+ typeof run.retired_gen === "number"
409
+ ? run.retired_gen
410
+ : Number(manifest.generation ?? 0);
411
+ const retiredAtUnix =
412
+ typeof run.retired_at_unix === "number"
413
+ ? run.retired_at_unix
414
+ : Math.floor(Number(nowMs) / 1000);
415
+ await db.retireSecondaryIndexRuns(
416
+ [runId],
417
+ retiredGen,
418
+ BigInt(retiredAtUnix) * 1000n,
419
+ );
420
+ }
421
+ }
422
+
423
+ const lexiconIndexes = Array.isArray(manifest.lexicon_indexes)
424
+ ? manifest.lexicon_indexes
425
+ : [];
426
+ for (const rawState of lexiconIndexes) {
427
+ if (!rawState || typeof rawState !== "object") continue;
428
+ const sourceKind =
429
+ typeof (rawState as any).source_kind === "string"
430
+ ? (rawState as any).source_kind
431
+ : "";
432
+ if (sourceKind === "") continue;
433
+ const sourceName =
434
+ typeof (rawState as any).source_name === "string"
435
+ ? (rawState as any).source_name
436
+ : "";
437
+ const indexedThrough =
438
+ typeof (rawState as any).indexed_through === "number"
439
+ ? Number((rawState as any).indexed_through)
440
+ : 0;
441
+ await db.upsertLexiconIndexState(
442
+ stream,
443
+ sourceKind,
444
+ sourceName,
445
+ indexedThrough,
446
+ );
447
+
448
+ const activeLexiconRuns = Array.isArray((rawState as any).active_runs)
449
+ ? (rawState as any).active_runs
450
+ : [];
451
+ const retiredLexiconRuns = Array.isArray((rawState as any).retired_runs)
452
+ ? (rawState as any).retired_runs
453
+ : [];
454
+ for (const run of activeLexiconRuns) {
455
+ await db.insertLexiconIndexRun({
456
+ run_id: String(run.run_id),
457
+ stream,
458
+ source_kind: sourceKind,
459
+ source_name: sourceName,
460
+ level: Number(run.level),
461
+ start_segment: Number(run.start_segment),
462
+ end_segment: Number(run.end_segment),
463
+ object_key: String(run.object_key),
464
+ size_bytes: Number(run.size_bytes ?? 0),
465
+ record_count: Number(run.record_count ?? 0),
466
+ });
467
+ }
468
+ for (const run of retiredLexiconRuns) {
469
+ const runId = String(run.run_id);
470
+ await db.insertLexiconIndexRun({
471
+ run_id: runId,
472
+ stream,
473
+ source_kind: sourceKind,
474
+ source_name: sourceName,
475
+ level: Number(run.level),
476
+ start_segment: Number(run.start_segment),
477
+ end_segment: Number(run.end_segment),
478
+ object_key: String(run.object_key),
479
+ size_bytes: Number(run.size_bytes ?? 0),
480
+ record_count: Number(run.record_count ?? 0),
481
+ });
482
+ const retiredGen =
483
+ typeof run.retired_gen === "number"
484
+ ? run.retired_gen
485
+ : Number(manifest.generation ?? 0);
486
+ const retiredAtUnix =
487
+ typeof run.retired_at_unix === "number"
488
+ ? run.retired_at_unix
489
+ : Math.floor(Number(nowMs) / 1000);
490
+ await db.retireLexiconIndexRuns(
491
+ [runId],
492
+ retiredGen,
493
+ BigInt(retiredAtUnix) * 1000n,
494
+ );
495
+ }
496
+ }
497
+
498
+ const searchCompanions =
499
+ manifest.search_companions &&
500
+ typeof manifest.search_companions === "object"
501
+ ? manifest.search_companions
502
+ : null;
503
+ if (searchCompanions) {
504
+ const generation =
505
+ typeof searchCompanions.generation === "number"
506
+ ? searchCompanions.generation
507
+ : 0;
508
+ const planHash =
509
+ typeof searchCompanions.plan_hash === "string"
510
+ ? searchCompanions.plan_hash
511
+ : "";
512
+ const planJson =
513
+ searchCompanions.plan_json &&
514
+ typeof searchCompanions.plan_json === "object"
515
+ ? JSON.stringify(searchCompanions.plan_json)
516
+ : JSON.stringify({ families: {}, summary: {} });
517
+ if (generation > 0 && planHash) {
518
+ await db.upsertSearchCompanionPlan(
519
+ stream,
520
+ generation,
521
+ planHash,
522
+ planJson,
523
+ );
524
+ }
525
+ const segments = Array.isArray(searchCompanions.segments)
526
+ ? searchCompanions.segments
527
+ : [];
528
+ for (const segment of segments) {
529
+ if (!segment || typeof segment !== "object") continue;
530
+ if (
531
+ typeof (segment as any).segment_index !== "number" ||
532
+ typeof (segment as any).object_key !== "string" ||
533
+ typeof (segment as any).plan_generation !== "number"
534
+ ) {
535
+ continue;
536
+ }
537
+ const sections = Array.isArray((segment as any).sections)
538
+ ? (segment as any).sections
539
+ : [];
540
+ await db.upsertSearchSegmentCompanion(
541
+ stream,
542
+ Number((segment as any).segment_index),
543
+ String((segment as any).object_key),
544
+ Number((segment as any).plan_generation),
545
+ JSON.stringify(sections),
546
+ JSON.stringify((segment as any).section_sizes ?? {}),
547
+ Number((segment as any).size_bytes ?? 0),
548
+ parseManifestBigInt((segment as any).primary_timestamp_min_ms),
549
+ parseManifestBigInt((segment as any).primary_timestamp_max_ms),
550
+ );
551
+ }
552
+ }
553
+
554
+ if (schemaBytes) {
555
+ await db.upsertSchemaRegistry(
556
+ stream,
557
+ new TextDecoder().decode(schemaBytes),
558
+ );
559
+ await db.setSchemaUploadedSizeBytes(stream, schemaBytes.byteLength);
560
+ }
561
+ await db.commitRestoreStream?.(stream);
562
+ } catch (error) {
563
+ if (restoreStarted)
564
+ await Promise.resolve(db.rollbackRestoreStream?.(stream)).catch(
565
+ () => {},
566
+ );
567
+ throw error;
568
+ }
569
+ }
570
+ }
571
+
572
+ function parseManifestBigInt(value: unknown): bigint | null {
573
+ if (typeof value === "bigint") return value;
574
+ if (typeof value === "number" && Number.isFinite(value))
575
+ return BigInt(Math.trunc(value));
576
+ if (typeof value === "string" && /^-?[0-9]+$/.test(value))
577
+ return BigInt(value);
578
+ return null;
579
+ }
580
+
581
+ function decodeZstdBase64(value: string): Uint8Array {
582
+ if (!value) return new Uint8Array(0);
583
+ const raw = Buffer.from(value, "base64");
584
+ if (raw.byteLength === 0) return new Uint8Array(0);
585
+ return new Uint8Array(zstdDecompressSync(raw));
586
+ }
587
+
588
+ function decodeU64LeArray(bytes: Uint8Array): bigint[] {
589
+ if (bytes.byteLength === 0) return [];
590
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
591
+ const out: bigint[] = [];
592
+ for (let off = 0; off + 8 <= bytes.byteLength; off += 8) {
593
+ out.push(dv.getBigUint64(off, true));
594
+ }
595
+ return out;
596
+ }
597
+
598
+ function decodeU32LeArray(bytes: Uint8Array): number[] {
599
+ if (bytes.byteLength === 0) return [];
600
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
601
+ const out: number[] = [];
602
+ for (let off = 0; off + 4 <= bytes.byteLength; off += 4) {
603
+ out.push(dv.getUint32(off, true));
604
+ }
605
+ return out;
606
+ }
607
+
608
+ function parseIsoMs(value: any): bigint | null {
609
+ if (!value || typeof value !== "string") return null;
610
+ const ms = Date.parse(value);
611
+ if (!Number.isFinite(ms)) return null;
612
+ return BigInt(ms);
613
+ }