@prisma/streams-server 0.0.1 → 0.1.1
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 +68 -0
- package/LICENSE +201 -0
- package/README.md +39 -2
- package/SECURITY.md +33 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +29 -34
- package/src/app.ts +74 -0
- package/src/app_core.ts +1706 -0
- package/src/app_local.ts +46 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +239 -0
- package/src/config.ts +251 -0
- package/src/db/db.ts +1386 -0
- package/src/db/schema.ts +625 -0
- package/src/expiry_sweeper.ts +44 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +745 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/ingest.ts +655 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +114 -0
- package/src/memory.ts +155 -0
- package/src/metrics.ts +161 -0
- package/src/metrics_emitter.ts +50 -0
- package/src/notifier.ts +64 -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 +128 -0
- package/src/offset.ts +70 -0
- package/src/reader.ts +454 -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/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/registry.ts +405 -0
- package/src/segment/cache.ts +179 -0
- package/src/segment/format.ts +331 -0
- package/src/segment/segmenter.ts +326 -0
- package/src/segment/segmenter_worker.ts +43 -0
- package/src/segment/segmenter_workers.ts +94 -0
- package/src/server.ts +326 -0
- package/src/sqlite/adapter.ts +164 -0
- package/src/stats.ts +205 -0
- package/src/touch/engine.ts +41 -0
- package/src/touch/interpreter_worker.ts +442 -0
- package/src/touch/live_keys.ts +118 -0
- package/src/touch/live_metrics.ts +827 -0
- package/src/touch/live_templates.ts +619 -0
- package/src/touch/manager.ts +1199 -0
- package/src/touch/spec.ts +456 -0
- package/src/touch/touch_journal.ts +671 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +189 -0
- package/src/touch/worker_protocol.ts +56 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +317 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -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 +45 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +31 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/build/index.d.mts +0 -1
- package/build/index.d.ts +0 -1
- package/build/index.js +0 -0
- package/build/index.mjs +0 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { dsError } from "./ds_error.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A tiny 256-bit bloom filter (2048 bits) with 3 hash probes.
|
|
4
|
+
*
|
|
5
|
+
* This is used per DSB3 block to cheaply skip blocks during key-filtered reads.
|
|
6
|
+
*
|
|
7
|
+
* Correctness rule: bloom filter may have false positives but MUST NOT have false negatives.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BITS = 2048n;
|
|
11
|
+
const MASK64 = (1n << 64n) - 1n;
|
|
12
|
+
|
|
13
|
+
function fnv1a64(data: Uint8Array): bigint {
|
|
14
|
+
let h = 14695981039346656037n;
|
|
15
|
+
for (const b of data) {
|
|
16
|
+
h ^= BigInt(b);
|
|
17
|
+
h = (h * 1099511628211n) & MASK64;
|
|
18
|
+
}
|
|
19
|
+
return h;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mix64(x: bigint): bigint {
|
|
23
|
+
// SplitMix64-esque mix.
|
|
24
|
+
x = (x + 0x9e3779b97f4a7c15n) & MASK64;
|
|
25
|
+
x = (x ^ (x >> 30n)) * 0xbf58476d1ce4e5b9n & MASK64;
|
|
26
|
+
x = (x ^ (x >> 27n)) * 0x94d049bb133111ebn & MASK64;
|
|
27
|
+
x = x ^ (x >> 31n);
|
|
28
|
+
return x & MASK64;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class Bloom256 {
|
|
32
|
+
private bits: Uint8Array;
|
|
33
|
+
|
|
34
|
+
constructor(bits?: Uint8Array) {
|
|
35
|
+
if (bits && bits.byteLength !== 32) throw dsError("bloom must be 32 bytes");
|
|
36
|
+
this.bits = bits ? new Uint8Array(bits) : new Uint8Array(32);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
toBytes(): Uint8Array {
|
|
40
|
+
return new Uint8Array(this.bits);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
add(keyUtf8: Uint8Array): void {
|
|
44
|
+
if (keyUtf8.byteLength === 0) return;
|
|
45
|
+
const h1 = fnv1a64(keyUtf8);
|
|
46
|
+
const h2 = mix64(h1 ^ 0xa0761d6478bd642fn);
|
|
47
|
+
for (let i = 0; i < 3; i++) {
|
|
48
|
+
const idx = Number((h1 + BigInt(i) * h2) % BITS); // 0..2047
|
|
49
|
+
const byte = idx >> 3;
|
|
50
|
+
const bit = idx & 7;
|
|
51
|
+
this.bits[byte] |= 1 << bit;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
maybeHas(keyUtf8: Uint8Array): boolean {
|
|
56
|
+
if (keyUtf8.byteLength === 0) return true;
|
|
57
|
+
const h1 = fnv1a64(keyUtf8);
|
|
58
|
+
const h2 = mix64(h1 ^ 0xa0761d6478bd642fn);
|
|
59
|
+
for (let i = 0; i < 3; i++) {
|
|
60
|
+
const idx = Number((h1 + BigInt(i) * h2) % BITS);
|
|
61
|
+
const byte = idx >> 3;
|
|
62
|
+
const bit = idx & 7;
|
|
63
|
+
if ((this.bits[byte] & (1 << bit)) === 0) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function cleanupTempSegments(rootDir: string): void {
|
|
5
|
+
const base = join(rootDir, "local");
|
|
6
|
+
if (!existsSync(base)) return;
|
|
7
|
+
const walk = (dir: string) => {
|
|
8
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
9
|
+
const full = join(dir, entry.name);
|
|
10
|
+
if (entry.isDirectory()) {
|
|
11
|
+
walk(full);
|
|
12
|
+
} else if (entry.isFile() && entry.name.endsWith(".tmp")) {
|
|
13
|
+
try {
|
|
14
|
+
unlinkSync(full);
|
|
15
|
+
} catch {
|
|
16
|
+
// ignore
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
walk(base);
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRC32C (Castagnoli) implementation in pure TypeScript.
|
|
3
|
+
*
|
|
4
|
+
* Used for DSB3 block headers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const POLY = 0x82f63b78;
|
|
8
|
+
let TABLE: Uint32Array | null = null;
|
|
9
|
+
|
|
10
|
+
function makeTable(): Uint32Array {
|
|
11
|
+
const t = new Uint32Array(256);
|
|
12
|
+
for (let i = 0; i < 256; i++) {
|
|
13
|
+
let c = i;
|
|
14
|
+
for (let k = 0; k < 8; k++) {
|
|
15
|
+
c = (c & 1) ? (POLY ^ (c >>> 1)) : (c >>> 1);
|
|
16
|
+
}
|
|
17
|
+
t[i] = c >>> 0;
|
|
18
|
+
}
|
|
19
|
+
return t;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function crc32c(buf: Uint8Array): number {
|
|
23
|
+
if (!TABLE) TABLE = makeTable();
|
|
24
|
+
let c = 0xffffffff;
|
|
25
|
+
for (const b of buf) {
|
|
26
|
+
c = TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
27
|
+
}
|
|
28
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TaggedError } from "better-result";
|
|
2
|
+
|
|
3
|
+
export class DurableStreamsError extends TaggedError("DurableStreamsError")<{
|
|
4
|
+
message: string;
|
|
5
|
+
cause?: unknown;
|
|
6
|
+
code?: string;
|
|
7
|
+
}>() {}
|
|
8
|
+
|
|
9
|
+
export function dsError(message: string, opts?: { cause?: unknown; code?: string }): DurableStreamsError {
|
|
10
|
+
return new DurableStreamsError({
|
|
11
|
+
message,
|
|
12
|
+
...(opts?.cause !== undefined ? { cause: opts.cause } : {}),
|
|
13
|
+
...(opts?.code !== undefined ? { code: opts.code } : {}),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
|
|
3
|
+
/** Parse a duration like 15s, 30m, 24h into milliseconds. */
|
|
4
|
+
export function parseDurationMsResult(s: string): Result<number, { kind: "invalid_duration"; message: string }> {
|
|
5
|
+
const m = /^([0-9]+)(ms|s|m|h|d)$/.exec(s.trim());
|
|
6
|
+
if (!m) return Result.err({ kind: "invalid_duration", message: `invalid duration: ${s}` });
|
|
7
|
+
const n = Number(m[1]);
|
|
8
|
+
const unit = m[2];
|
|
9
|
+
switch (unit) {
|
|
10
|
+
case "ms": return Result.ok(n);
|
|
11
|
+
case "s": return Result.ok(n * 1000);
|
|
12
|
+
case "m": return Result.ok(n * 60 * 1000);
|
|
13
|
+
case "h": return Result.ok(n * 60 * 60 * 1000);
|
|
14
|
+
case "d": return Result.ok(n * 24 * 60 * 60 * 1000);
|
|
15
|
+
default: return Result.err({ kind: "invalid_duration", message: `invalid unit: ${unit}` });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endian helpers.
|
|
3
|
+
*
|
|
4
|
+
* The TieredStore spec uses big-endian for on-disk encodings.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function writeU32BE(dst: Uint8Array, offset: number, value: number): void {
|
|
8
|
+
const dv = new DataView(dst.buffer, dst.byteOffset, dst.byteLength);
|
|
9
|
+
dv.setUint32(offset, value >>> 0, false);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function writeU16BE(dst: Uint8Array, offset: number, value: number): void {
|
|
13
|
+
const dv = new DataView(dst.buffer, dst.byteOffset, dst.byteLength);
|
|
14
|
+
dv.setUint16(offset, value & 0xffff, false);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writeU64BE(dst: Uint8Array, offset: number, value: bigint): void {
|
|
18
|
+
const dv = new DataView(dst.buffer, dst.byteOffset, dst.byteLength);
|
|
19
|
+
dv.setBigUint64(offset, value, false);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readU32BE(src: Uint8Array, offset: number): number {
|
|
23
|
+
const dv = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
24
|
+
return dv.getUint32(offset, false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readU16BE(src: Uint8Array, offset: number): number {
|
|
28
|
+
const dv = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
29
|
+
return dv.getUint16(offset, false);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readU64BE(src: Uint8Array, offset: number): bigint {
|
|
33
|
+
const dv = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
34
|
+
return dv.getBigUint64(offset, false);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Little-endian helpers for metadata arrays (not on-disk segment format).
|
|
38
|
+
export function readU64LE(src: Uint8Array, offset: number): bigint {
|
|
39
|
+
const dv = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
40
|
+
return dv.getBigUint64(offset, true);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function concatBytes(parts: Uint8Array[]): Uint8Array {
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (const p of parts) total += p.byteLength;
|
|
46
|
+
const out = new Uint8Array(total);
|
|
47
|
+
let off = 0;
|
|
48
|
+
for (const p of parts) {
|
|
49
|
+
out.set(p, off);
|
|
50
|
+
off += p.byteLength;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import { dsError } from "./ds_error.ts";
|
|
3
|
+
|
|
4
|
+
type JsonPointerError = { kind: "invalid_json_pointer" | "invalid_json_pointer_operation"; message: string };
|
|
5
|
+
|
|
6
|
+
function invalidPointerOperation<T = never>(message: string): Result<T, JsonPointerError> {
|
|
7
|
+
return Result.err({ kind: "invalid_json_pointer_operation", message });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseJsonPointerResult(ptr: string): Result<string[], JsonPointerError> {
|
|
11
|
+
if (ptr === "") return Result.ok([]);
|
|
12
|
+
if (!ptr.startsWith("/")) return Result.err({ kind: "invalid_json_pointer", message: "invalid json pointer" });
|
|
13
|
+
return Result.ok(
|
|
14
|
+
ptr
|
|
15
|
+
.split("/")
|
|
16
|
+
.slice(1)
|
|
17
|
+
.map((seg) => seg.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseJsonPointer(ptr: string): string[] {
|
|
22
|
+
const res = parseJsonPointerResult(ptr);
|
|
23
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
24
|
+
return res.value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isArrayIndex(seg: string): boolean {
|
|
28
|
+
return seg !== "" && /^[0-9]+$/.test(seg);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getChild(container: any, seg: string): any {
|
|
32
|
+
if (Array.isArray(container) && isArrayIndex(seg)) {
|
|
33
|
+
return container[Number(seg)];
|
|
34
|
+
}
|
|
35
|
+
if (container && typeof container === "object") {
|
|
36
|
+
return (container as any)[seg];
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setChildResult(container: any, seg: string, value: any): Result<void, JsonPointerError> {
|
|
42
|
+
if (Array.isArray(container) && isArrayIndex(seg)) {
|
|
43
|
+
const idx = Number(seg);
|
|
44
|
+
if (idx < 0 || idx >= container.length) return invalidPointerOperation("array index out of bounds");
|
|
45
|
+
container[idx] = value;
|
|
46
|
+
return Result.ok(undefined);
|
|
47
|
+
}
|
|
48
|
+
if (container && typeof container === "object") {
|
|
49
|
+
(container as any)[seg] = value;
|
|
50
|
+
return Result.ok(undefined);
|
|
51
|
+
}
|
|
52
|
+
return invalidPointerOperation("invalid parent for set");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function deleteChildResult(container: any, seg: string): Result<void, JsonPointerError> {
|
|
56
|
+
if (Array.isArray(container) && isArrayIndex(seg)) {
|
|
57
|
+
const idx = Number(seg);
|
|
58
|
+
if (idx < 0 || idx >= container.length) return invalidPointerOperation("array index out of bounds");
|
|
59
|
+
container.splice(idx, 1);
|
|
60
|
+
return Result.ok(undefined);
|
|
61
|
+
}
|
|
62
|
+
if (container && typeof container === "object") {
|
|
63
|
+
delete (container as any)[seg];
|
|
64
|
+
return Result.ok(undefined);
|
|
65
|
+
}
|
|
66
|
+
return invalidPointerOperation("invalid parent for delete");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type PointerResolution = {
|
|
70
|
+
parent: any;
|
|
71
|
+
key: string | null;
|
|
72
|
+
value: any;
|
|
73
|
+
exists: boolean;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function resolvePointerResult(doc: any, ptr: string): Result<PointerResolution, JsonPointerError> {
|
|
77
|
+
const segmentsRes = parseJsonPointerResult(ptr);
|
|
78
|
+
if (Result.isError(segmentsRes)) return Result.err(segmentsRes.error);
|
|
79
|
+
const segments = segmentsRes.value;
|
|
80
|
+
if (segments.length === 0) {
|
|
81
|
+
return Result.ok({ parent: null, key: null, value: doc, exists: true });
|
|
82
|
+
}
|
|
83
|
+
let cur: any = doc;
|
|
84
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
85
|
+
cur = getChild(cur, segments[i]);
|
|
86
|
+
if (cur === undefined) return Result.ok({ parent: null, key: null, value: undefined, exists: false });
|
|
87
|
+
}
|
|
88
|
+
const key = segments[segments.length - 1];
|
|
89
|
+
const value = cur === undefined ? undefined : getChild(cur, key);
|
|
90
|
+
return Result.ok({ parent: cur, key, value, exists: value !== undefined });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolvePointer(doc: any, ptr: string): PointerResolution {
|
|
94
|
+
const res = resolvePointerResult(doc, ptr);
|
|
95
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
96
|
+
return res.value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function setPointerValueResult(doc: any, ptr: string, value: any, opts?: { createParents?: boolean }): Result<any, JsonPointerError> {
|
|
100
|
+
const segmentsRes = parseJsonPointerResult(ptr);
|
|
101
|
+
if (Result.isError(segmentsRes)) return Result.err(segmentsRes.error);
|
|
102
|
+
const segments = segmentsRes.value;
|
|
103
|
+
if (segments.length === 0) {
|
|
104
|
+
return Result.ok(value);
|
|
105
|
+
}
|
|
106
|
+
let cur: any = doc;
|
|
107
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
108
|
+
const seg = segments[i];
|
|
109
|
+
let next = getChild(cur, seg);
|
|
110
|
+
if (next === undefined) {
|
|
111
|
+
if (!opts?.createParents) return invalidPointerOperation("missing parent");
|
|
112
|
+
next = {};
|
|
113
|
+
const setRes = setChildResult(cur, seg, next);
|
|
114
|
+
if (Result.isError(setRes)) return setRes;
|
|
115
|
+
}
|
|
116
|
+
cur = next;
|
|
117
|
+
}
|
|
118
|
+
const leafRes = setChildResult(cur, segments[segments.length - 1], value);
|
|
119
|
+
if (Result.isError(leafRes)) return leafRes;
|
|
120
|
+
return Result.ok(doc);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function setPointerValue(doc: any, ptr: string, value: any, opts?: { createParents?: boolean }): any {
|
|
124
|
+
const res = setPointerValueResult(doc, ptr, value, opts);
|
|
125
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
126
|
+
return res.value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function deletePointerValueResult(doc: any, ptr: string): Result<any, JsonPointerError> {
|
|
130
|
+
const segmentsRes = parseJsonPointerResult(ptr);
|
|
131
|
+
if (Result.isError(segmentsRes)) return Result.err(segmentsRes.error);
|
|
132
|
+
const segments = segmentsRes.value;
|
|
133
|
+
if (segments.length === 0) return invalidPointerOperation("cannot delete document root");
|
|
134
|
+
let cur: any = doc;
|
|
135
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
136
|
+
cur = getChild(cur, segments[i]);
|
|
137
|
+
if (cur === undefined) return invalidPointerOperation("missing parent");
|
|
138
|
+
}
|
|
139
|
+
const deleteRes = deleteChildResult(cur, segments[segments.length - 1]);
|
|
140
|
+
if (Result.isError(deleteRes)) return deleteRes;
|
|
141
|
+
return Result.ok(doc);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function deletePointerValue(doc: any, ptr: string): any {
|
|
145
|
+
const res = deletePointerValueResult(doc, ptr);
|
|
146
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
147
|
+
return res.value;
|
|
148
|
+
}
|
package/src/util/log.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type ConsoleFn = (...args: any[]) => void;
|
|
2
|
+
|
|
3
|
+
let patched = false;
|
|
4
|
+
|
|
5
|
+
function wrapConsole(orig: ConsoleFn, level: string): ConsoleFn {
|
|
6
|
+
return (...args: any[]) => {
|
|
7
|
+
const prefix = `[${new Date().toISOString()}] [${level}]`;
|
|
8
|
+
if (args.length === 0) return orig(prefix);
|
|
9
|
+
return orig(prefix, ...args);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function initConsoleLogging(): void {
|
|
14
|
+
if (patched) return;
|
|
15
|
+
patched = true;
|
|
16
|
+
const globalAny = globalThis as any;
|
|
17
|
+
if (globalAny.__ds_console_patched) return;
|
|
18
|
+
globalAny.__ds_console_patched = true;
|
|
19
|
+
|
|
20
|
+
console.log = wrapConsole(console.log.bind(console), "INFO");
|
|
21
|
+
console.info = wrapConsole(console.info.bind(console), "INFO");
|
|
22
|
+
console.warn = wrapConsole(console.warn.bind(console), "WARN");
|
|
23
|
+
console.error = wrapConsole(console.error.bind(console), "ERROR");
|
|
24
|
+
if (console.debug) console.debug = wrapConsole(console.debug.bind(console), "DEBUG");
|
|
25
|
+
}
|
package/src/util/lru.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { dsError } from "./ds_error.ts";
|
|
2
|
+
export class LruCache<K, V> {
|
|
3
|
+
private readonly maxEntries: number;
|
|
4
|
+
private readonly map = new Map<K, V>();
|
|
5
|
+
|
|
6
|
+
constructor(maxEntries: number) {
|
|
7
|
+
if (maxEntries <= 0) throw dsError("maxEntries must be > 0");
|
|
8
|
+
this.maxEntries = maxEntries;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get size(): number {
|
|
12
|
+
return this.map.size;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get(key: K): V | undefined {
|
|
16
|
+
const value = this.map.get(key);
|
|
17
|
+
if (value === undefined) return undefined;
|
|
18
|
+
// Refresh LRU order.
|
|
19
|
+
this.map.delete(key);
|
|
20
|
+
this.map.set(key, value);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
set(key: K, value: V): void {
|
|
25
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
26
|
+
this.map.set(key, value);
|
|
27
|
+
while (this.map.size > this.maxEntries) {
|
|
28
|
+
const oldest = this.map.keys().next();
|
|
29
|
+
if (oldest.done) break;
|
|
30
|
+
this.map.delete(oldest.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
has(key: K): boolean {
|
|
35
|
+
return this.map.has(key);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
delete(key: K): boolean {
|
|
39
|
+
return this.map.delete(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
clear(): void {
|
|
43
|
+
this.map.clear();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { dsError } from "./ds_error.ts";
|
|
2
|
+
export type RetryOptions = {
|
|
3
|
+
retries: number;
|
|
4
|
+
baseDelayMs: number;
|
|
5
|
+
maxDelayMs: number;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function sleep(ms: number): Promise<void> {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
|
|
14
|
+
if (ms <= 0) return p;
|
|
15
|
+
return Promise.race([
|
|
16
|
+
p,
|
|
17
|
+
new Promise<T>((_, reject) => setTimeout(() => reject(dsError("timeout")), ms)),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function retry<T>(fn: () => Promise<T>, opts: RetryOptions): Promise<T> {
|
|
22
|
+
let attempt = 0;
|
|
23
|
+
let delay = opts.baseDelayMs;
|
|
24
|
+
for (;;) {
|
|
25
|
+
try {
|
|
26
|
+
return await withTimeout(fn(), opts.timeoutMs);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
attempt++;
|
|
29
|
+
if (attempt > opts.retries) throw e;
|
|
30
|
+
const jitter = Math.random() * 0.2 + 0.9; // 0.9-1.1
|
|
31
|
+
await sleep(Math.min(opts.maxDelayMs, Math.floor(delay * jitter)));
|
|
32
|
+
delay = Math.min(opts.maxDelayMs, delay * 2);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { dsError } from "./ds_error.ts";
|
|
2
|
+
const MASK_64 = 0xffffffffffffffffn;
|
|
3
|
+
|
|
4
|
+
function rotl(x: bigint, b: number): bigint {
|
|
5
|
+
return ((x << BigInt(b)) | (x >> BigInt(64 - b))) & MASK_64;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readU64LE(bytes: Uint8Array, offset: number): bigint {
|
|
9
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
10
|
+
return dv.getBigUint64(offset, true);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function siphash24(key: Uint8Array, msg: Uint8Array): bigint {
|
|
14
|
+
if (key.byteLength !== 16) throw dsError("siphash24 requires 16-byte key");
|
|
15
|
+
const k0 = readU64LE(key, 0);
|
|
16
|
+
const k1 = readU64LE(key, 8);
|
|
17
|
+
|
|
18
|
+
let v0 = 0x736f6d6570736575n ^ k0;
|
|
19
|
+
let v1 = 0x646f72616e646f6dn ^ k1;
|
|
20
|
+
let v2 = 0x6c7967656e657261n ^ k0;
|
|
21
|
+
let v3 = 0x7465646279746573n ^ k1;
|
|
22
|
+
|
|
23
|
+
const msgLen = msg.byteLength;
|
|
24
|
+
let off = 0;
|
|
25
|
+
while (off + 8 <= msgLen) {
|
|
26
|
+
const m = readU64LE(msg, off);
|
|
27
|
+
v3 ^= m;
|
|
28
|
+
sipRound();
|
|
29
|
+
sipRound();
|
|
30
|
+
v0 ^= m;
|
|
31
|
+
off += 8;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let b = BigInt(msgLen) << 56n;
|
|
35
|
+
for (let i = 0; i < msgLen - off; i++) {
|
|
36
|
+
b |= BigInt(msg[off + i]) << BigInt(8 * i);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
v3 ^= b;
|
|
40
|
+
sipRound();
|
|
41
|
+
sipRound();
|
|
42
|
+
v0 ^= b;
|
|
43
|
+
|
|
44
|
+
v2 ^= 0xffn;
|
|
45
|
+
sipRound();
|
|
46
|
+
sipRound();
|
|
47
|
+
sipRound();
|
|
48
|
+
sipRound();
|
|
49
|
+
|
|
50
|
+
return (v0 ^ v1 ^ v2 ^ v3) & MASK_64;
|
|
51
|
+
|
|
52
|
+
function sipRound(): void {
|
|
53
|
+
v0 = (v0 + v1) & MASK_64;
|
|
54
|
+
v1 = rotl(v1, 13);
|
|
55
|
+
v1 ^= v0;
|
|
56
|
+
v0 = rotl(v0, 32);
|
|
57
|
+
|
|
58
|
+
v2 = (v2 + v3) & MASK_64;
|
|
59
|
+
v3 = rotl(v3, 16);
|
|
60
|
+
v3 ^= v2;
|
|
61
|
+
|
|
62
|
+
v0 = (v0 + v3) & MASK_64;
|
|
63
|
+
v3 = rotl(v3, 21);
|
|
64
|
+
v3 ^= v0;
|
|
65
|
+
|
|
66
|
+
v2 = (v2 + v1) & MASK_64;
|
|
67
|
+
v1 = rotl(v1, 17);
|
|
68
|
+
v1 ^= v2;
|
|
69
|
+
v2 = rotl(v2, 32);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function streamHash16Hex(stream: string): string {
|
|
4
|
+
const full = createHash("sha256").update(stream).digest();
|
|
5
|
+
return full.subarray(0, 16).toString("hex"); // 32 hex chars
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function pad16(n: number): string {
|
|
9
|
+
const s = String(n);
|
|
10
|
+
return s.padStart(16, "0");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function segmentObjectKey(streamHash: string, segmentIndex: number): string {
|
|
14
|
+
return `streams/${streamHash}/segments/${pad16(segmentIndex)}.bin`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function manifestObjectKey(streamHash: string): string {
|
|
18
|
+
return `streams/${streamHash}/manifest.json`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function schemaObjectKey(streamHash: string): string {
|
|
22
|
+
return `streams/${streamHash}/schema-registry.json`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function indexRunObjectKey(streamHash: string, runId: string): string {
|
|
26
|
+
return `streams/${streamHash}/index/${runId}.idx`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function localSegmentPath(rootDir: string, streamHash: string, segmentIndex: number): string {
|
|
30
|
+
return `${rootDir}/local/streams/${streamHash}/segments/${pad16(segmentIndex)}.bin`;
|
|
31
|
+
}
|
package/src/util/time.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
|
|
3
|
+
export function parseTimestampMsResult(input: string): Result<bigint, { kind: "empty_timestamp" | "invalid_timestamp"; message: string }> {
|
|
4
|
+
const s = input.trim();
|
|
5
|
+
if (s === "") return Result.err({ kind: "empty_timestamp", message: "empty timestamp" });
|
|
6
|
+
// unix nanos
|
|
7
|
+
if (/^[0-9]+$/.test(s)) {
|
|
8
|
+
return Result.ok(BigInt(s) / 1_000_000n);
|
|
9
|
+
}
|
|
10
|
+
const d = new Date(s);
|
|
11
|
+
const ms = d.getTime();
|
|
12
|
+
if (Number.isNaN(ms)) return Result.err({ kind: "invalid_timestamp", message: `invalid timestamp: ${input}` });
|
|
13
|
+
return Result.ok(BigInt(ms));
|
|
14
|
+
}
|
package/build/index.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
package/build/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
package/build/index.js
DELETED
|
File without changes
|
package/build/index.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|