@prisma/streams-server 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +8 -0
- package/package.json +2 -1
- package/src/app.ts +290 -17
- package/src/app_core.ts +1833 -698
- package/src/app_local.ts +144 -4
- package/src/auto_tune.ts +62 -0
- package/src/bootstrap.ts +159 -1
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +116 -14
- package/src/db/db.ts +1201 -131
- package/src/db/schema.ts +308 -8
- package/src/foreground_activity.ts +55 -0
- package/src/index/indexer.ts +254 -124
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +789 -0
- package/src/index/secondary_indexer.ts +824 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +10 -12
- package/src/manifest.ts +143 -8
- package/src/memory.ts +183 -8
- package/src/metrics.ts +15 -29
- package/src/metrics_emitter.ts +26 -3
- package/src/notifier.ts +121 -5
- package/src/objectstore/accounting.ts +92 -0
- package/src/objectstore/mock_r2.ts +1 -1
- package/src/objectstore/r2.ts +17 -1
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +299 -0
- package/src/profiles/generic.ts +47 -0
- package/src/profiles/index.ts +205 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +85 -0
- package/src/profiles/profile.ts +225 -0
- package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
- package/src/profiles/stateProtocol/routes.ts +389 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +100 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2151 -164
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +235 -0
- package/src/schema/read_json.ts +43 -0
- package/src/schema/registry.ts +563 -59
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +389 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +313 -0
- package/src/search/companion_manager.ts +1086 -0
- package/src/search/companion_plan.ts +218 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +93 -2
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +108 -36
- package/src/segment/segmenter.ts +79 -5
- package/src/segment/segmenter_worker.ts +35 -6
- package/src/segment/segmenter_workers.ts +42 -12
- package/src/server.ts +150 -36
- package/src/sqlite/adapter.ts +185 -14
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +3 -3
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_metrics.ts +94 -64
- package/src/touch/live_templates.ts +15 -1
- package/src/touch/manager.ts +166 -88
- package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
- package/src/touch/spec.ts +95 -92
- package/src/touch/touch_journal.ts +4 -0
- package/src/touch/worker_pool.ts +8 -14
- package/src/touch/worker_protocol.ts +3 -3
- package/src/uploader.ts +77 -6
- package/src/util/bloom256.ts +2 -2
- package/src/util/byte_lru.ts +73 -0
- package/src/util/lru.ts +8 -0
- package/src/util/stream_paths.ts +19 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const INT64_SIGN = 0x8000_0000_0000_0000n;
|
|
2
|
+
const UINT64_MASK = 0xffff_ffff_ffff_ffffn;
|
|
3
|
+
|
|
4
|
+
export function encodeSortableInt64(value: bigint): Uint8Array {
|
|
5
|
+
const signed = BigInt.asIntN(64, value);
|
|
6
|
+
const sortable = BigInt.asUintN(64, signed ^ INT64_SIGN);
|
|
7
|
+
const out = new Uint8Array(8);
|
|
8
|
+
const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
9
|
+
dv.setBigUint64(0, sortable, false);
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function decodeSortableInt64(bytes: Uint8Array, offset = 0): bigint {
|
|
14
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset + offset, 8);
|
|
15
|
+
const sortable = dv.getBigUint64(0, false);
|
|
16
|
+
return BigInt.asIntN(64, sortable ^ INT64_SIGN);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function encodeSortableFloat64(value: number): Uint8Array {
|
|
20
|
+
const out = new Uint8Array(8);
|
|
21
|
+
const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
22
|
+
dv.setFloat64(0, value, false);
|
|
23
|
+
const raw = dv.getBigUint64(0, false);
|
|
24
|
+
const sortable = (raw & INT64_SIGN) !== 0n ? (~raw) & UINT64_MASK : raw ^ INT64_SIGN;
|
|
25
|
+
dv.setBigUint64(0, sortable, false);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decodeSortableFloat64(bytes: Uint8Array, offset = 0): number {
|
|
30
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset + offset, 8);
|
|
31
|
+
const sortable = dv.getBigUint64(0, false);
|
|
32
|
+
const raw = (sortable & INT64_SIGN) !== 0n ? sortable ^ INT64_SIGN : (~sortable) & UINT64_MASK;
|
|
33
|
+
dv.setBigUint64(0, raw, false);
|
|
34
|
+
return dv.getFloat64(0, false);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function encodeSortableBool(value: boolean): Uint8Array {
|
|
38
|
+
return new Uint8Array([value ? 1 : 0]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function decodeSortableBool(bytes: Uint8Array, offset = 0): boolean {
|
|
42
|
+
return bytes[offset] === 1;
|
|
43
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
statSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname, join, relative } from "node:path";
|
|
14
|
+
import { Result } from "better-result";
|
|
15
|
+
import { type CompanionToc } from "./companion_format";
|
|
16
|
+
import { LruCache } from "../util/lru";
|
|
17
|
+
|
|
18
|
+
export type CompanionFileCacheError = { kind: "invalid_companion_cache"; message: string };
|
|
19
|
+
|
|
20
|
+
export type MappedCompanionBundle = {
|
|
21
|
+
objectKey: string;
|
|
22
|
+
path: string;
|
|
23
|
+
bytes: Uint8Array;
|
|
24
|
+
toc: CompanionToc;
|
|
25
|
+
sizeBytes: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type FileEntry = {
|
|
29
|
+
path: string;
|
|
30
|
+
size: number;
|
|
31
|
+
mtimeMs: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type CompanionFileCacheStats = {
|
|
35
|
+
usedBytes: number;
|
|
36
|
+
entryCount: number;
|
|
37
|
+
mappedBytes: number;
|
|
38
|
+
mappedEntryCount: number;
|
|
39
|
+
pinnedEntryCount: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function invalidCompanionCache<T = never>(message: string): Result<T, CompanionFileCacheError> {
|
|
43
|
+
return Result.err({ kind: "invalid_companion_cache", message });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class CompanionFileCache {
|
|
47
|
+
private readonly entries = new Map<string, FileEntry>();
|
|
48
|
+
private readonly pinnedKeys = new Set<string>();
|
|
49
|
+
private readonly mappedBundles: LruCache<string, MappedCompanionBundle>;
|
|
50
|
+
private totalBytes = 0;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
private readonly rootDir: string,
|
|
54
|
+
private readonly maxBytes: number,
|
|
55
|
+
private readonly maxAgeMs: number,
|
|
56
|
+
mappedEntries: number
|
|
57
|
+
) {
|
|
58
|
+
mkdirSync(this.rootDir, { recursive: true });
|
|
59
|
+
this.mappedBundles = new LruCache(Math.max(1, mappedEntries));
|
|
60
|
+
this.loadIndex();
|
|
61
|
+
this.pruneForBudget(Date.now(), 0, true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clearMapped(): void {
|
|
65
|
+
this.mappedBundles.clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
bytesForObjectKeyPrefix(prefix: string): number {
|
|
69
|
+
let total = 0;
|
|
70
|
+
for (const [objectKey, entry] of this.entries.entries()) {
|
|
71
|
+
if (objectKey.startsWith(prefix)) total += entry.size;
|
|
72
|
+
}
|
|
73
|
+
return total;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
remove(objectKey: string): void {
|
|
77
|
+
this.removeEntry(objectKey, true);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
storeBytesResult(objectKey: string, bytes: Uint8Array): Result<string, CompanionFileCacheError> {
|
|
81
|
+
if (this.maxBytes <= 0) return invalidCompanionCache("search companion file cache must have a positive size budget");
|
|
82
|
+
if (bytes.byteLength > this.maxBytes) {
|
|
83
|
+
return invalidCompanionCache(`companion cache object exceeds budget: ${objectKey}`);
|
|
84
|
+
}
|
|
85
|
+
this.pruneForBudget(Date.now(), bytes.byteLength, false);
|
|
86
|
+
const path = this.pathFor(objectKey);
|
|
87
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
88
|
+
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
89
|
+
try {
|
|
90
|
+
writeFileSync(tmpPath, bytes);
|
|
91
|
+
renameSync(tmpPath, path);
|
|
92
|
+
} catch (e: unknown) {
|
|
93
|
+
try {
|
|
94
|
+
unlinkSync(tmpPath);
|
|
95
|
+
} catch {
|
|
96
|
+
// ignore temp cleanup failures
|
|
97
|
+
}
|
|
98
|
+
return invalidCompanionCache(String((e as any)?.message ?? e));
|
|
99
|
+
}
|
|
100
|
+
const existing = this.entries.get(objectKey);
|
|
101
|
+
if (existing) this.totalBytes = Math.max(0, this.totalBytes - existing.size);
|
|
102
|
+
const nextEntry = { path, size: bytes.byteLength, mtimeMs: Date.now() };
|
|
103
|
+
this.entries.delete(objectKey);
|
|
104
|
+
this.entries.set(objectKey, nextEntry);
|
|
105
|
+
this.totalBytes += bytes.byteLength;
|
|
106
|
+
this.touch(objectKey);
|
|
107
|
+
return Result.ok(path);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async loadMappedBundleResult(args: {
|
|
111
|
+
objectKey: string;
|
|
112
|
+
expectedSize: number;
|
|
113
|
+
loadBytes: () => Promise<Uint8Array>;
|
|
114
|
+
decodeToc: (bytes: Uint8Array) => Result<CompanionToc, { message: string }>;
|
|
115
|
+
}): Promise<Result<MappedCompanionBundle, CompanionFileCacheError>> {
|
|
116
|
+
const cached = this.mappedBundles.get(args.objectKey);
|
|
117
|
+
if (cached) {
|
|
118
|
+
this.pinnedKeys.add(args.objectKey);
|
|
119
|
+
this.touch(args.objectKey);
|
|
120
|
+
return Result.ok(cached);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const localPathRes = await this.ensureLocalFileResult(args.objectKey, args.expectedSize, args.loadBytes);
|
|
124
|
+
if (Result.isError(localPathRes)) return localPathRes;
|
|
125
|
+
|
|
126
|
+
let mappedRes = this.mapBundleResult(args.objectKey, localPathRes.value, args.expectedSize, args.decodeToc);
|
|
127
|
+
if (Result.isError(mappedRes)) {
|
|
128
|
+
this.removeEntry(args.objectKey, true);
|
|
129
|
+
const refetchedPathRes = await this.ensureLocalFileResult(args.objectKey, args.expectedSize, args.loadBytes);
|
|
130
|
+
if (Result.isError(refetchedPathRes)) return refetchedPathRes;
|
|
131
|
+
mappedRes = this.mapBundleResult(args.objectKey, refetchedPathRes.value, args.expectedSize, args.decodeToc);
|
|
132
|
+
}
|
|
133
|
+
if (Result.isError(mappedRes)) return mappedRes;
|
|
134
|
+
|
|
135
|
+
this.pinnedKeys.add(args.objectKey);
|
|
136
|
+
this.mappedBundles.set(args.objectKey, mappedRes.value);
|
|
137
|
+
this.touch(args.objectKey);
|
|
138
|
+
return mappedRes;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
stats(): CompanionFileCacheStats {
|
|
142
|
+
let mappedBytes = 0;
|
|
143
|
+
let mappedEntryCount = 0;
|
|
144
|
+
for (const bundle of this.mappedBundles.values()) {
|
|
145
|
+
mappedBytes += bundle.sizeBytes;
|
|
146
|
+
mappedEntryCount += 1;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
usedBytes: this.totalBytes,
|
|
150
|
+
entryCount: this.entries.size,
|
|
151
|
+
mappedBytes,
|
|
152
|
+
mappedEntryCount,
|
|
153
|
+
pinnedEntryCount: this.pinnedKeys.size,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async ensureLocalFileResult(
|
|
158
|
+
objectKey: string,
|
|
159
|
+
expectedSize: number,
|
|
160
|
+
loadBytes: () => Promise<Uint8Array>
|
|
161
|
+
): Promise<Result<string, CompanionFileCacheError>> {
|
|
162
|
+
const existing = this.entries.get(objectKey);
|
|
163
|
+
if (existing) {
|
|
164
|
+
const stat = this.safeStat(existing.path);
|
|
165
|
+
if (stat && stat.size === expectedSize) {
|
|
166
|
+
this.touch(objectKey);
|
|
167
|
+
return Result.ok(existing.path);
|
|
168
|
+
}
|
|
169
|
+
this.removeEntry(objectKey, true);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let bytes: Uint8Array;
|
|
173
|
+
try {
|
|
174
|
+
bytes = await loadBytes();
|
|
175
|
+
} catch (e: unknown) {
|
|
176
|
+
return invalidCompanionCache(String((e as any)?.message ?? e));
|
|
177
|
+
}
|
|
178
|
+
if (bytes.byteLength !== expectedSize) {
|
|
179
|
+
return invalidCompanionCache(`unexpected companion object size for ${objectKey}`);
|
|
180
|
+
}
|
|
181
|
+
return this.storeBytesResult(objectKey, bytes);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private mapBundleResult(
|
|
185
|
+
objectKey: string,
|
|
186
|
+
path: string,
|
|
187
|
+
expectedSize: number,
|
|
188
|
+
decodeToc: (bytes: Uint8Array) => Result<CompanionToc, { message: string }>
|
|
189
|
+
): Result<MappedCompanionBundle, CompanionFileCacheError> {
|
|
190
|
+
const stat = this.safeStat(path);
|
|
191
|
+
if (!stat) return invalidCompanionCache(`missing cached companion file ${path}`);
|
|
192
|
+
if (stat.size !== expectedSize) return invalidCompanionCache(`unexpected cached companion size for ${objectKey}`);
|
|
193
|
+
const tocProbeLength = Math.min(expectedSize, 512);
|
|
194
|
+
const tocProbe = this.readPrefix(path, tocProbeLength);
|
|
195
|
+
if (Result.isError(tocProbe)) return tocProbe;
|
|
196
|
+
const tocRes = decodeToc(tocProbe.value);
|
|
197
|
+
if (Result.isError(tocRes)) return invalidCompanionCache(tocRes.error.message);
|
|
198
|
+
let bytes: Uint8Array;
|
|
199
|
+
try {
|
|
200
|
+
bytes = (Bun as any).mmap(path, { shared: true }) as Uint8Array;
|
|
201
|
+
} catch (e: unknown) {
|
|
202
|
+
return invalidCompanionCache(String((e as any)?.message ?? e));
|
|
203
|
+
}
|
|
204
|
+
if (bytes.byteLength !== expectedSize) {
|
|
205
|
+
return invalidCompanionCache(`unexpected mapped companion size for ${objectKey}`);
|
|
206
|
+
}
|
|
207
|
+
return Result.ok({
|
|
208
|
+
objectKey,
|
|
209
|
+
path,
|
|
210
|
+
bytes,
|
|
211
|
+
toc: tocRes.value,
|
|
212
|
+
sizeBytes: expectedSize,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private pathFor(objectKey: string): string {
|
|
217
|
+
return join(this.rootDir, objectKey);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private loadIndex(): void {
|
|
221
|
+
const files: Array<{ key: string; path: string; size: number; mtimeMs: number }> = [];
|
|
222
|
+
const walk = (dir: string) => {
|
|
223
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
224
|
+
const full = join(dir, entry.name);
|
|
225
|
+
if (entry.isDirectory()) {
|
|
226
|
+
walk(full);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!entry.isFile()) continue;
|
|
230
|
+
const stat = statSync(full);
|
|
231
|
+
files.push({
|
|
232
|
+
key: relative(this.rootDir, full),
|
|
233
|
+
path: full,
|
|
234
|
+
size: stat.size,
|
|
235
|
+
mtimeMs: stat.mtimeMs,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
if (!existsSync(this.rootDir)) return;
|
|
240
|
+
walk(this.rootDir);
|
|
241
|
+
files.sort((left, right) => left.mtimeMs - right.mtimeMs);
|
|
242
|
+
for (const file of files) {
|
|
243
|
+
this.entries.set(file.key, { path: file.path, size: file.size, mtimeMs: file.mtimeMs });
|
|
244
|
+
this.totalBytes += file.size;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private pruneForBudget(nowMs: number, incomingBytes: number, allowMappedDeletes: boolean): void {
|
|
249
|
+
if (this.maxAgeMs > 0) {
|
|
250
|
+
for (const [objectKey, entry] of Array.from(this.entries.entries())) {
|
|
251
|
+
if (this.pinnedKeys.has(objectKey)) continue;
|
|
252
|
+
if (nowMs - entry.mtimeMs <= this.maxAgeMs) continue;
|
|
253
|
+
this.removeEntry(objectKey, allowMappedDeletes);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (this.maxBytes <= 0) return;
|
|
257
|
+
while (this.totalBytes + incomingBytes > this.maxBytes) {
|
|
258
|
+
const next = this.entries.keys().next();
|
|
259
|
+
if (next.done) break;
|
|
260
|
+
const objectKey = next.value as string;
|
|
261
|
+
if (this.pinnedKeys.has(objectKey)) {
|
|
262
|
+
let removed = false;
|
|
263
|
+
for (const candidateKey of this.entries.keys()) {
|
|
264
|
+
if (this.pinnedKeys.has(candidateKey)) continue;
|
|
265
|
+
this.removeEntry(candidateKey, false);
|
|
266
|
+
removed = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
if (!removed) break;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
this.removeEntry(objectKey, allowMappedDeletes);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private touch(objectKey: string): void {
|
|
277
|
+
const entry = this.entries.get(objectKey);
|
|
278
|
+
if (!entry) return;
|
|
279
|
+
this.entries.delete(objectKey);
|
|
280
|
+
this.entries.set(objectKey, { ...entry });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private removeEntry(objectKey: string, allowMappedDelete: boolean): void {
|
|
284
|
+
if (this.pinnedKeys.has(objectKey)) return;
|
|
285
|
+
const entry = this.entries.get(objectKey);
|
|
286
|
+
if (!entry) return;
|
|
287
|
+
try {
|
|
288
|
+
unlinkSync(entry.path);
|
|
289
|
+
} catch {
|
|
290
|
+
// ignore remove failures
|
|
291
|
+
}
|
|
292
|
+
this.totalBytes = Math.max(0, this.totalBytes - entry.size);
|
|
293
|
+
this.entries.delete(objectKey);
|
|
294
|
+
this.mappedBundles.delete(objectKey);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private safeStat(path: string): { size: number } | null {
|
|
298
|
+
try {
|
|
299
|
+
return existsSync(path) ? { size: statSync(path).size } : null;
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private readPrefix(path: string, byteLength: number): Result<Uint8Array, CompanionFileCacheError> {
|
|
306
|
+
try {
|
|
307
|
+
const fd = openSync(path, "r");
|
|
308
|
+
try {
|
|
309
|
+
const out = new Uint8Array(byteLength);
|
|
310
|
+
const bytesRead = readSync(fd, out, 0, byteLength, 0);
|
|
311
|
+
return Result.ok(out.subarray(0, bytesRead));
|
|
312
|
+
} finally {
|
|
313
|
+
closeSync(fd);
|
|
314
|
+
}
|
|
315
|
+
} catch (e: unknown) {
|
|
316
|
+
return invalidCompanionCache(String((e as any)?.message ?? e));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import { decodeAggSegmentCompanionResult, encodeAggSegmentCompanion, type AggSectionInput, type AggSectionView } from "./agg_format";
|
|
3
|
+
import { decodeColSegmentCompanionResult, encodeColSegmentCompanion, type ColSectionInput, type ColSectionView } from "./col_format";
|
|
4
|
+
import { decodeFtsSegmentCompanionResult, encodeFtsSegmentCompanion, type FtsSectionInput, type FtsSectionView } from "./fts_format";
|
|
5
|
+
import {
|
|
6
|
+
decodeMetricsBlockSegmentCompanionResult,
|
|
7
|
+
encodeMetricsBlockSegmentCompanion,
|
|
8
|
+
type MetricsBlockSectionInput,
|
|
9
|
+
type MetricsBlockSectionView,
|
|
10
|
+
} from "../profiles/metrics/block_format";
|
|
11
|
+
import type { SearchCompanionPlan } from "./companion_plan";
|
|
12
|
+
import { BinaryCursor, BinaryWriter, concatBytes, readU16, readU32, readU64 } from "./binary/codec";
|
|
13
|
+
import { streamHash16Hex } from "../util/stream_paths";
|
|
14
|
+
|
|
15
|
+
const MAGIC = new TextEncoder().encode("PSCIX2");
|
|
16
|
+
const MAJOR_VERSION = 2;
|
|
17
|
+
const HEADER_BYTES = 58;
|
|
18
|
+
const SECTION_ENTRY_BYTES = 28;
|
|
19
|
+
export const PSCIX2_MAX_SECTION_COUNT = 4;
|
|
20
|
+
export const PSCIX2_MAX_TOC_BYTES = HEADER_BYTES + SECTION_ENTRY_BYTES * PSCIX2_MAX_SECTION_COUNT;
|
|
21
|
+
|
|
22
|
+
const SECTION_KIND_CODE = {
|
|
23
|
+
col: 1,
|
|
24
|
+
fts: 2,
|
|
25
|
+
agg: 3,
|
|
26
|
+
mblk: 4,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
const CODE_SECTION_KIND = {
|
|
30
|
+
1: "col",
|
|
31
|
+
2: "fts",
|
|
32
|
+
3: "agg",
|
|
33
|
+
4: "mblk",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type CompanionSectionKind = "col" | "fts" | "agg" | "mblk";
|
|
37
|
+
|
|
38
|
+
export type CompanionSectionInputMap = {
|
|
39
|
+
col?: ColSectionInput;
|
|
40
|
+
fts?: FtsSectionInput;
|
|
41
|
+
agg?: AggSectionInput;
|
|
42
|
+
mblk?: MetricsBlockSectionInput;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type CompanionSectionMap = {
|
|
46
|
+
col?: ColSectionView;
|
|
47
|
+
fts?: FtsSectionView;
|
|
48
|
+
agg?: AggSectionView;
|
|
49
|
+
mblk?: MetricsBlockSectionView;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type CompanionTocSection = {
|
|
53
|
+
kind: CompanionSectionKind;
|
|
54
|
+
version: number;
|
|
55
|
+
compression: number;
|
|
56
|
+
flags: number;
|
|
57
|
+
offset: number;
|
|
58
|
+
length: number;
|
|
59
|
+
dir_length: number;
|
|
60
|
+
logical_length: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type CompanionToc = {
|
|
64
|
+
version: 2;
|
|
65
|
+
plan_generation: number;
|
|
66
|
+
segment_index: number;
|
|
67
|
+
stream_hash16: string;
|
|
68
|
+
sections: CompanionTocSection[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type BundledSegmentCompanion = {
|
|
72
|
+
toc: CompanionToc;
|
|
73
|
+
sections: CompanionSectionMap;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type EncodedCompanionSectionPayload = {
|
|
77
|
+
kind: CompanionSectionKind;
|
|
78
|
+
version: number;
|
|
79
|
+
compression: number;
|
|
80
|
+
flags: number;
|
|
81
|
+
dirLength: number;
|
|
82
|
+
logicalLength: number;
|
|
83
|
+
payload: Uint8Array;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type CompanionFormatError = { kind: "invalid_companion"; message: string };
|
|
87
|
+
|
|
88
|
+
function invalidCompanion<T = never>(message: string): Result<T, CompanionFormatError> {
|
|
89
|
+
return Result.err({ kind: "invalid_companion", message });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sectionKindCode(kind: CompanionSectionKind): number {
|
|
93
|
+
return SECTION_KIND_CODE[kind];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function encodeSectionPayload(
|
|
97
|
+
kind: CompanionSectionKind,
|
|
98
|
+
section: CompanionSectionInputMap[CompanionSectionKind],
|
|
99
|
+
plan: SearchCompanionPlan
|
|
100
|
+
): EncodedCompanionSectionPayload {
|
|
101
|
+
if (kind === "col") {
|
|
102
|
+
const payload = encodeColSegmentCompanion(section as ColSectionInput, plan);
|
|
103
|
+
return { kind, version: 2, compression: 0, flags: 0, dirLength: 8, logicalLength: payload.byteLength, payload };
|
|
104
|
+
}
|
|
105
|
+
if (kind === "fts") {
|
|
106
|
+
const payload = encodeFtsSegmentCompanion(section as FtsSectionInput, plan);
|
|
107
|
+
return { kind, version: 2, compression: 0, flags: 0, dirLength: 8, logicalLength: payload.byteLength, payload };
|
|
108
|
+
}
|
|
109
|
+
if (kind === "agg") {
|
|
110
|
+
const payload = encodeAggSegmentCompanion(section as AggSectionInput, plan);
|
|
111
|
+
return { kind, version: 2, compression: 0, flags: 0, dirLength: 4, logicalLength: payload.byteLength, payload };
|
|
112
|
+
}
|
|
113
|
+
const payload = encodeMetricsBlockSegmentCompanion(section as MetricsBlockSectionInput);
|
|
114
|
+
return { kind, version: 2, compression: 0, flags: 0, dirLength: 20, logicalLength: payload.byteLength, payload };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function encodeCompanionSectionPayload(
|
|
118
|
+
kind: CompanionSectionKind,
|
|
119
|
+
section: CompanionSectionInputMap[CompanionSectionKind],
|
|
120
|
+
plan: SearchCompanionPlan
|
|
121
|
+
): EncodedCompanionSectionPayload {
|
|
122
|
+
return encodeSectionPayload(kind, section, plan);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decodeSectionResult(
|
|
126
|
+
kind: CompanionSectionKind,
|
|
127
|
+
bytes: Uint8Array,
|
|
128
|
+
plan: SearchCompanionPlan
|
|
129
|
+
): Result<CompanionSectionMap[CompanionSectionKind], CompanionFormatError> {
|
|
130
|
+
if (kind === "col") {
|
|
131
|
+
const decoded = decodeColSegmentCompanionResult(bytes, plan);
|
|
132
|
+
if (Result.isError(decoded)) return invalidCompanion(decoded.error.message);
|
|
133
|
+
return Result.ok(decoded.value as CompanionSectionMap[CompanionSectionKind]);
|
|
134
|
+
}
|
|
135
|
+
if (kind === "fts") {
|
|
136
|
+
const decoded = decodeFtsSegmentCompanionResult(bytes, plan);
|
|
137
|
+
if (Result.isError(decoded)) return invalidCompanion(decoded.error.message);
|
|
138
|
+
return Result.ok(decoded.value as CompanionSectionMap[CompanionSectionKind]);
|
|
139
|
+
}
|
|
140
|
+
if (kind === "agg") {
|
|
141
|
+
const decoded = decodeAggSegmentCompanionResult(bytes, plan);
|
|
142
|
+
if (Result.isError(decoded)) return invalidCompanion(decoded.error.message);
|
|
143
|
+
return Result.ok(decoded.value as CompanionSectionMap[CompanionSectionKind]);
|
|
144
|
+
}
|
|
145
|
+
const decoded = decodeMetricsBlockSegmentCompanionResult(bytes);
|
|
146
|
+
if (Result.isError(decoded)) return invalidCompanion(decoded.error.message);
|
|
147
|
+
return Result.ok(decoded.value as CompanionSectionMap[CompanionSectionKind]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function encodeBundledSegmentCompanion(companion: {
|
|
151
|
+
stream: string;
|
|
152
|
+
segment_index: number;
|
|
153
|
+
plan_generation: number;
|
|
154
|
+
plan: SearchCompanionPlan;
|
|
155
|
+
sections: CompanionSectionInputMap;
|
|
156
|
+
}): Uint8Array {
|
|
157
|
+
const sectionPayloads: EncodedCompanionSectionPayload[] = [];
|
|
158
|
+
for (const kind of ["col", "fts", "agg", "mblk"] as CompanionSectionKind[]) {
|
|
159
|
+
const section = companion.sections[kind];
|
|
160
|
+
if (!section) continue;
|
|
161
|
+
sectionPayloads.push(encodeSectionPayload(kind, section, companion.plan));
|
|
162
|
+
}
|
|
163
|
+
return encodeBundledSegmentCompanionFromPayloads({
|
|
164
|
+
stream: companion.stream,
|
|
165
|
+
segment_index: companion.segment_index,
|
|
166
|
+
plan_generation: companion.plan_generation,
|
|
167
|
+
sections: sectionPayloads,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function encodeBundledSegmentCompanionFromPayloads(companion: {
|
|
172
|
+
stream: string;
|
|
173
|
+
segment_index: number;
|
|
174
|
+
plan_generation: number;
|
|
175
|
+
sections: EncodedCompanionSectionPayload[];
|
|
176
|
+
}): Uint8Array {
|
|
177
|
+
const writer = new BinaryWriter();
|
|
178
|
+
writer.writeBytes(MAGIC);
|
|
179
|
+
writer.writeU16(MAJOR_VERSION);
|
|
180
|
+
writer.writeU16(0);
|
|
181
|
+
writer.writeU16(companion.sections.length);
|
|
182
|
+
writer.writeU16(0);
|
|
183
|
+
writer.writeU32(companion.plan_generation);
|
|
184
|
+
writer.writeU64(BigInt(companion.segment_index));
|
|
185
|
+
writer.writeBytes(hexToBytes(streamHash16Hex(companion.stream)));
|
|
186
|
+
writer.writeBytes(new Uint8Array(16));
|
|
187
|
+
|
|
188
|
+
let payloadOffset = HEADER_BYTES + SECTION_ENTRY_BYTES * companion.sections.length;
|
|
189
|
+
const sectionTable = new BinaryWriter();
|
|
190
|
+
for (const section of companion.sections) {
|
|
191
|
+
sectionTable.writeU8(sectionKindCode(section.kind));
|
|
192
|
+
sectionTable.writeU8(section.version);
|
|
193
|
+
sectionTable.writeU8(section.compression);
|
|
194
|
+
sectionTable.writeU8(section.flags);
|
|
195
|
+
sectionTable.writeU64(BigInt(payloadOffset));
|
|
196
|
+
sectionTable.writeU64(BigInt(section.payload.byteLength));
|
|
197
|
+
sectionTable.writeU32(section.dirLength);
|
|
198
|
+
sectionTable.writeU32(section.logicalLength);
|
|
199
|
+
payloadOffset += section.payload.byteLength;
|
|
200
|
+
}
|
|
201
|
+
return concatBytes([writer.finish(), sectionTable.finish(), ...companion.sections.map((section) => section.payload)]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function decodeBundledSegmentCompanionTocResult(bytes: Uint8Array): Result<CompanionToc, CompanionFormatError> {
|
|
205
|
+
if (bytes.byteLength < HEADER_BYTES) return invalidCompanion("invalid PSCIX2 header");
|
|
206
|
+
for (let index = 0; index < MAGIC.byteLength; index++) {
|
|
207
|
+
if (bytes[index] !== MAGIC[index]) return invalidCompanion("invalid PSCIX2 magic");
|
|
208
|
+
}
|
|
209
|
+
const majorVersion = readU16(bytes, MAGIC.byteLength);
|
|
210
|
+
if (majorVersion !== MAJOR_VERSION) return invalidCompanion("unsupported PSCIX2 version");
|
|
211
|
+
const sectionCount = readU16(bytes, MAGIC.byteLength + 4);
|
|
212
|
+
if (sectionCount < 0 || sectionCount > PSCIX2_MAX_SECTION_COUNT) {
|
|
213
|
+
return invalidCompanion("invalid PSCIX2 section count");
|
|
214
|
+
}
|
|
215
|
+
if (bytes.byteLength < HEADER_BYTES + SECTION_ENTRY_BYTES * sectionCount) {
|
|
216
|
+
return invalidCompanion("truncated PSCIX2 section table");
|
|
217
|
+
}
|
|
218
|
+
const planGeneration = readU32(bytes, MAGIC.byteLength + 8);
|
|
219
|
+
const segmentIndex = Number(readU64(bytes, MAGIC.byteLength + 12));
|
|
220
|
+
const streamHash16 = bytesToHex(bytes.subarray(MAGIC.byteLength + 20, MAGIC.byteLength + 36));
|
|
221
|
+
const sections: CompanionTocSection[] = [];
|
|
222
|
+
let offset = HEADER_BYTES;
|
|
223
|
+
for (let index = 0; index < sectionCount; index++) {
|
|
224
|
+
const kindCode = bytes[offset]!;
|
|
225
|
+
const kind = CODE_SECTION_KIND[kindCode as keyof typeof CODE_SECTION_KIND];
|
|
226
|
+
if (!kind) return invalidCompanion("invalid PSCIX2 section kind");
|
|
227
|
+
sections.push({
|
|
228
|
+
kind,
|
|
229
|
+
version: bytes[offset + 1]!,
|
|
230
|
+
compression: bytes[offset + 2]!,
|
|
231
|
+
flags: bytes[offset + 3]!,
|
|
232
|
+
offset: Number(readU64(bytes, offset + 4)),
|
|
233
|
+
length: Number(readU64(bytes, offset + 12)),
|
|
234
|
+
dir_length: readU32(bytes, offset + 20),
|
|
235
|
+
logical_length: readU32(bytes, offset + 24),
|
|
236
|
+
});
|
|
237
|
+
offset += SECTION_ENTRY_BYTES;
|
|
238
|
+
}
|
|
239
|
+
return Result.ok({
|
|
240
|
+
version: 2,
|
|
241
|
+
plan_generation: planGeneration,
|
|
242
|
+
segment_index: segmentIndex,
|
|
243
|
+
stream_hash16: streamHash16,
|
|
244
|
+
sections,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function decodeBundledSegmentCompanionSectionResult<K extends CompanionSectionKind>(
|
|
249
|
+
bytes: Uint8Array,
|
|
250
|
+
kind: K,
|
|
251
|
+
plan: SearchCompanionPlan
|
|
252
|
+
): Result<CompanionSectionMap[K] | null, CompanionFormatError> {
|
|
253
|
+
const tocRes = decodeBundledSegmentCompanionTocResult(bytes);
|
|
254
|
+
if (Result.isError(tocRes)) return tocRes;
|
|
255
|
+
return decodeBundledSegmentCompanionSectionFromTocResult(bytes, tocRes.value, kind, plan);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function decodeBundledSegmentCompanionSectionFromTocResult<K extends CompanionSectionKind>(
|
|
259
|
+
bytes: Uint8Array,
|
|
260
|
+
toc: CompanionToc,
|
|
261
|
+
kind: K,
|
|
262
|
+
plan: SearchCompanionPlan
|
|
263
|
+
): Result<CompanionSectionMap[K] | null, CompanionFormatError> {
|
|
264
|
+
const section = toc.sections.find((entry) => entry.kind === kind);
|
|
265
|
+
if (!section) return Result.ok(null);
|
|
266
|
+
const sectionEnd = section.offset + section.length;
|
|
267
|
+
if (section.offset < 0 || section.length < 0 || sectionEnd > bytes.byteLength) {
|
|
268
|
+
return invalidCompanion("invalid PSCIX2 section bounds");
|
|
269
|
+
}
|
|
270
|
+
const decodedRes = decodeSectionResult(kind, bytes.subarray(section.offset, sectionEnd), plan);
|
|
271
|
+
if (Result.isError(decodedRes)) return decodedRes;
|
|
272
|
+
return Result.ok(decodedRes.value as CompanionSectionMap[K]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function decodeCompanionSectionPayloadResult<K extends CompanionSectionKind>(
|
|
276
|
+
kind: K,
|
|
277
|
+
bytes: Uint8Array,
|
|
278
|
+
plan: SearchCompanionPlan
|
|
279
|
+
): Result<NonNullable<CompanionSectionMap[K]>, CompanionFormatError> {
|
|
280
|
+
const decodedRes = decodeSectionResult(kind, bytes, plan);
|
|
281
|
+
if (Result.isError(decodedRes)) return decodedRes as Result<NonNullable<CompanionSectionMap[K]>, CompanionFormatError>;
|
|
282
|
+
if (!decodedRes.value) return invalidCompanion(`missing PSCIX2 ${kind} payload`);
|
|
283
|
+
return Result.ok(decodedRes.value as NonNullable<CompanionSectionMap[K]>);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function decodeBundledSegmentCompanionResult(
|
|
287
|
+
bytes: Uint8Array,
|
|
288
|
+
plan: SearchCompanionPlan
|
|
289
|
+
): Result<BundledSegmentCompanion, CompanionFormatError> {
|
|
290
|
+
const tocRes = decodeBundledSegmentCompanionTocResult(bytes);
|
|
291
|
+
if (Result.isError(tocRes)) return tocRes;
|
|
292
|
+
const sections: CompanionSectionMap = {};
|
|
293
|
+
for (const entry of tocRes.value.sections) {
|
|
294
|
+
const decodedRes = decodeBundledSegmentCompanionSectionFromTocResult(bytes, tocRes.value, entry.kind, plan);
|
|
295
|
+
if (Result.isError(decodedRes)) return decodedRes;
|
|
296
|
+
if (decodedRes.value) (sections as Record<string, unknown>)[entry.kind] = decodedRes.value;
|
|
297
|
+
}
|
|
298
|
+
return Result.ok({ toc: tocRes.value, sections });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function hexToBytes(value: string): Uint8Array {
|
|
302
|
+
const out = new Uint8Array(value.length / 2);
|
|
303
|
+
for (let index = 0; index < value.length; index += 2) {
|
|
304
|
+
out[index / 2] = Number.parseInt(value.slice(index, index + 2), 16);
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function bytesToHex(value: Uint8Array): string {
|
|
310
|
+
return Array.from(value)
|
|
311
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
312
|
+
.join("");
|
|
313
|
+
}
|