@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.
- package/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/SECURITY.md +42 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +46 -0
- package/src/app.ts +583 -0
- package/src/app_core.ts +3144 -0
- package/src/app_local.ts +206 -0
- package/src/auth.ts +124 -0
- package/src/auto_tune.ts +69 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +613 -0
- package/src/compute/demo_entry.ts +415 -0
- package/src/compute/demo_site.ts +1242 -0
- package/src/compute/entry.ts +19 -0
- package/src/compute/package_entry.ts +4 -0
- package/src/compute/virtual-modules.d.ts +15 -0
- package/src/compute/worker_module_url.ts +9 -0
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +402 -0
- package/src/db/bootstrap_store.ts +9 -0
- package/src/db/db.ts +2424 -0
- package/src/db/schema.ts +925 -0
- package/src/db/sqlite_manifest_snapshot.ts +81 -0
- package/src/db/sqlite_touch_store.ts +491 -0
- package/src/db/sqlite_wal_store.ts +472 -0
- package/src/details/full_mode_details.ts +568 -0
- package/src/expiry_sweeper.ts +47 -0
- package/src/foreground_activity.ts +55 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +947 -0
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +863 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/index/schedule.ts +28 -0
- package/src/index/secondary_indexer.ts +901 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +309 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +249 -0
- package/src/memory.ts +334 -0
- package/src/metrics.ts +147 -0
- package/src/metrics_emitter.ts +83 -0
- package/src/notifier.ts +180 -0
- package/src/objectstore/accounting.ts +151 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +318 -0
- package/src/observe/pairing.ts +61 -0
- package/src/observe/request.ts +772 -0
- package/src/offset.ts +70 -0
- package/src/postgres/bootstrap.ts +269 -0
- package/src/postgres/companions.ts +197 -0
- package/src/postgres/control_restore.ts +109 -0
- package/src/postgres/details.ts +189 -0
- package/src/postgres/lexicon_index.ts +260 -0
- package/src/postgres/routing_index.ts +189 -0
- package/src/postgres/rows.ts +132 -0
- package/src/postgres/schema.ts +355 -0
- package/src/postgres/secondary_index.ts +238 -0
- package/src/postgres/segments.ts +900 -0
- package/src/postgres/stats.ts +103 -0
- package/src/postgres/store.ts +947 -0
- package/src/postgres/touch.ts +591 -0
- package/src/postgres/types.ts +32 -0
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +473 -0
- package/src/profiles/generic.ts +51 -0
- package/src/profiles/index.ts +237 -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 +83 -0
- package/src/profiles/otelTraces/normalize.ts +955 -0
- package/src/profiles/otelTraces/otlp.ts +1002 -0
- package/src/profiles/otelTraces/schema.ts +408 -0
- package/src/profiles/otelTraces.ts +390 -0
- package/src/profiles/profile.ts +284 -0
- package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
- package/src/profiles/stateProtocol/changes.ts +24 -0
- package/src/profiles/stateProtocol/ingest.ts +115 -0
- package/src/profiles/stateProtocol/routes.ts +511 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +107 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2986 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +237 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/read_json.ts +51 -0
- package/src/schema/registry.ts +966 -0
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +409 -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 +327 -0
- package/src/search/companion_manager.ts +1305 -0
- package/src/search/companion_plan.ts +229 -0
- package/src/search/exact_format.ts +281 -0
- package/src/search/exact_runtime.ts +55 -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 +270 -0
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +403 -0
- package/src/segment/segmenter.ts +412 -0
- package/src/segment/segmenter_worker.ts +72 -0
- package/src/segment/segmenter_workers.ts +130 -0
- package/src/server.ts +264 -0
- package/src/server_auto_tune.ts +158 -0
- package/src/sqlite/adapter.ts +335 -0
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +205 -0
- package/src/store/append.ts +50 -0
- package/src/store/bootstrap_restore_store.ts +71 -0
- package/src/store/capabilities.ts +86 -0
- package/src/store/full_mode_details_store.ts +71 -0
- package/src/store/index_store.ts +104 -0
- package/src/store/profile_touch_store.ts +1 -0
- package/src/store/rows.ts +144 -0
- package/src/store/schema_profile_store.ts +73 -0
- package/src/store/schema_publication.ts +6 -0
- package/src/store/segment_manifest_store.ts +129 -0
- package/src/store/segment_read_store.ts +22 -0
- package/src/store/stats_accounting_store.ts +83 -0
- package/src/store/touch_store.ts +98 -0
- package/src/store/wal_store.ts +21 -0
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_keys.ts +158 -0
- package/src/touch/live_metrics.ts +841 -0
- package/src/touch/live_templates.ts +449 -0
- package/src/touch/manager.ts +1292 -0
- package/src/touch/process_batch.ts +576 -0
- package/src/touch/processor_worker.ts +85 -0
- package/src/touch/spec.ts +459 -0
- package/src/touch/touch_journal.ts +771 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +191 -0
- package/src/touch/worker_protocol.ts +57 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +358 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/byte_lru.ts +73 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +53 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +50 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/src/util/zstd.ts +24 -0
package/src/bootstrap.ts
ADDED
|
@@ -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
|
+
}
|