@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
package/src/hist.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export type HistogramSnapshot = {
|
|
2
|
+
counts: number[];
|
|
3
|
+
total: number;
|
|
4
|
+
max: number;
|
|
5
|
+
p50: number;
|
|
6
|
+
p95: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
class LatencyHistogram {
|
|
10
|
+
private readonly bounds: number[];
|
|
11
|
+
private counts: number[];
|
|
12
|
+
private spareCounts: number[];
|
|
13
|
+
private total = 0;
|
|
14
|
+
private max = 0;
|
|
15
|
+
|
|
16
|
+
constructor(bounds: number[]) {
|
|
17
|
+
this.bounds = bounds;
|
|
18
|
+
this.counts = new Array(bounds.length + 1).fill(0);
|
|
19
|
+
this.spareCounts = new Array(bounds.length + 1).fill(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
record(ms: number): void {
|
|
23
|
+
const v = Math.max(0, Math.floor(ms));
|
|
24
|
+
this.total += 1;
|
|
25
|
+
if (v > this.max) this.max = v;
|
|
26
|
+
let idx = this.bounds.length;
|
|
27
|
+
for (let i = 0; i < this.bounds.length; i++) {
|
|
28
|
+
if (v <= this.bounds[i]) {
|
|
29
|
+
idx = i;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.counts[idx] += 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private percentile(q: number, counts: number[], total: number, max: number): number {
|
|
37
|
+
if (total === 0) return 0;
|
|
38
|
+
const target = Math.ceil(total * q);
|
|
39
|
+
let acc = 0;
|
|
40
|
+
for (let i = 0; i < counts.length; i++) {
|
|
41
|
+
acc += counts[i];
|
|
42
|
+
if (acc >= target) {
|
|
43
|
+
if (i < this.bounds.length) return this.bounds[i];
|
|
44
|
+
return max;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return max;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
snapshotAndReset(): HistogramSnapshot {
|
|
51
|
+
const counts = this.counts;
|
|
52
|
+
const total = this.total;
|
|
53
|
+
const max = this.max;
|
|
54
|
+
const p50 = this.percentile(0.5, counts, total, max);
|
|
55
|
+
const p95 = this.percentile(0.95, counts, total, max);
|
|
56
|
+
this.counts = this.spareCounts;
|
|
57
|
+
this.counts.fill(0);
|
|
58
|
+
this.spareCounts = counts;
|
|
59
|
+
this.total = 0;
|
|
60
|
+
this.max = 0;
|
|
61
|
+
const snap: HistogramSnapshot = {
|
|
62
|
+
counts,
|
|
63
|
+
total,
|
|
64
|
+
max,
|
|
65
|
+
p50,
|
|
66
|
+
p95,
|
|
67
|
+
};
|
|
68
|
+
return snap;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class LatencyHistogramCollector {
|
|
73
|
+
private readonly writes: LatencyHistogram;
|
|
74
|
+
private readonly reads: LatencyHistogram;
|
|
75
|
+
private readonly bounds: number[];
|
|
76
|
+
|
|
77
|
+
constructor(bounds?: number[]) {
|
|
78
|
+
this.bounds = bounds ?? [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000];
|
|
79
|
+
this.writes = new LatencyHistogram(this.bounds);
|
|
80
|
+
this.reads = new LatencyHistogram(this.bounds);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
recordWrite(ms: number): void {
|
|
84
|
+
this.writes.record(ms);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
recordRead(ms: number): void {
|
|
88
|
+
this.reads.record(ms);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
snapshotAndReset(): { writes: HistogramSnapshot; reads: HistogramSnapshot } {
|
|
92
|
+
return { writes: this.writes.snapshotAndReset(), reads: this.reads.snapshotAndReset() };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getBounds(): number[] {
|
|
96
|
+
return this.bounds;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatMs(ms: number): string {
|
|
101
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
|
|
102
|
+
return `${ms}ms`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderSparkline(title: string, snap: HistogramSnapshot): string {
|
|
106
|
+
const total = snap.total;
|
|
107
|
+
if (total === 0) return `[hist] ${title} count=0`;
|
|
108
|
+
let maxCount = 1;
|
|
109
|
+
for (const count of snap.counts) {
|
|
110
|
+
if (count > maxCount) maxCount = count;
|
|
111
|
+
}
|
|
112
|
+
const symbols = " .:-=+*#%@";
|
|
113
|
+
let spark = "";
|
|
114
|
+
for (const count of snap.counts) {
|
|
115
|
+
const level = Math.round((count / maxCount) * (symbols.length - 1));
|
|
116
|
+
spark += symbols[level] ?? symbols[0];
|
|
117
|
+
}
|
|
118
|
+
return `[hist] ${title} count=${total} p50<=${formatMs(snap.p50)} p95<=${formatMs(snap.p95)} max=${formatMs(
|
|
119
|
+
snap.max
|
|
120
|
+
)} |${spark}|`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class HistogramReporter {
|
|
124
|
+
private timer: any | null = null;
|
|
125
|
+
private running = false;
|
|
126
|
+
private readonly intervalMs: number;
|
|
127
|
+
private readonly hist: LatencyHistogramCollector;
|
|
128
|
+
private legendPrinted = false;
|
|
129
|
+
private readonly legend: string;
|
|
130
|
+
|
|
131
|
+
constructor(hist: LatencyHistogramCollector, intervalMs = 60_000) {
|
|
132
|
+
this.hist = hist;
|
|
133
|
+
this.intervalMs = intervalMs;
|
|
134
|
+
const bounds = this.hist.getBounds();
|
|
135
|
+
const labels = bounds.map((b) => (b >= 1000 ? `${(b / 1000).toFixed(0)}s` : `${b}ms`));
|
|
136
|
+
this.legend = ` bins<=${labels.join(",")},>${labels[labels.length - 1]}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
start(): void {
|
|
140
|
+
if (this.timer) return;
|
|
141
|
+
this.timer = setInterval(() => {
|
|
142
|
+
void this.tick();
|
|
143
|
+
}, this.intervalMs);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
stop(): void {
|
|
147
|
+
if (this.timer) clearInterval(this.timer);
|
|
148
|
+
this.timer = null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async tick(): Promise<void> {
|
|
152
|
+
if (this.running) return;
|
|
153
|
+
this.running = true;
|
|
154
|
+
try {
|
|
155
|
+
const snap = this.hist.snapshotAndReset();
|
|
156
|
+
if (!this.legendPrinted) {
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.log(`[hist]${this.legend}`);
|
|
159
|
+
this.legendPrinted = true;
|
|
160
|
+
}
|
|
161
|
+
// eslint-disable-next-line no-console
|
|
162
|
+
console.log(renderSparkline("writes", snap.writes));
|
|
163
|
+
// eslint-disable-next-line no-console
|
|
164
|
+
console.log(renderSparkline("reads", snap.reads));
|
|
165
|
+
} finally {
|
|
166
|
+
this.running = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import { readU32BE, readU64BE, writeU32BE, writeU64BE } from "../util/endian";
|
|
3
|
+
import { dsError } from "../util/ds_error.ts";
|
|
4
|
+
|
|
5
|
+
const MASK_64 = 0xffffffffffffffffn;
|
|
6
|
+
const MAX_ITERATIONS = 1024;
|
|
7
|
+
|
|
8
|
+
export type BinaryFuseFilter = {
|
|
9
|
+
seed: bigint;
|
|
10
|
+
segmentLength: number;
|
|
11
|
+
segmentLengthMask: number;
|
|
12
|
+
segmentCount: number;
|
|
13
|
+
segmentCountLength: number;
|
|
14
|
+
fingerprints: Uint8Array;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type BinaryFuseDecodeError = {
|
|
18
|
+
kind: "invalid_binary_fuse";
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type BinaryFuseBuildError = {
|
|
23
|
+
kind: "invalid_binary_fuse_build";
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function invalidBinaryFuse<T = never>(message: string): Result<T, BinaryFuseDecodeError> {
|
|
28
|
+
return Result.err({ kind: "invalid_binary_fuse", message });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function invalidBinaryFuseBuild<T = never>(message: string): Result<T, BinaryFuseBuildError> {
|
|
32
|
+
return Result.err({ kind: "invalid_binary_fuse_build", message });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildBinaryFuse(keys: bigint[]): { filter: BinaryFuseFilter | null; bytes: Uint8Array } {
|
|
36
|
+
const res = buildBinaryFuseResult(keys);
|
|
37
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
38
|
+
return res.value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildBinaryFuseResult(
|
|
42
|
+
keys: bigint[]
|
|
43
|
+
): Result<{ filter: BinaryFuseFilter | null; bytes: Uint8Array }, BinaryFuseBuildError> {
|
|
44
|
+
if (keys.length === 0) return Result.ok({ filter: null, bytes: new Uint8Array(0) });
|
|
45
|
+
const uniqueKeys = uniqueSorted(keys);
|
|
46
|
+
if (uniqueKeys.length === 0) return Result.ok({ filter: null, bytes: new Uint8Array(0) });
|
|
47
|
+
|
|
48
|
+
const filterRes = buildFilterResult(uniqueKeys);
|
|
49
|
+
if (Result.isError(filterRes)) return filterRes;
|
|
50
|
+
const filter = filterRes.value;
|
|
51
|
+
const bytes = filter ? encodeBinaryFuse(filter) : new Uint8Array(0);
|
|
52
|
+
if (filter && bytes.byteLength >= 28) {
|
|
53
|
+
filter.fingerprints = bytes.subarray(28);
|
|
54
|
+
}
|
|
55
|
+
return Result.ok({ filter, bytes });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function encodeBinaryFuse(filter: BinaryFuseFilter | null): Uint8Array {
|
|
59
|
+
if (!filter) return new Uint8Array(0);
|
|
60
|
+
const fpLen = filter.fingerprints.byteLength;
|
|
61
|
+
const hdr = new Uint8Array(28);
|
|
62
|
+
writeU64BE(hdr, 0, filter.seed);
|
|
63
|
+
writeU32BE(hdr, 8, filter.segmentLength);
|
|
64
|
+
writeU32BE(hdr, 12, filter.segmentLengthMask);
|
|
65
|
+
writeU32BE(hdr, 16, filter.segmentCount);
|
|
66
|
+
writeU32BE(hdr, 20, filter.segmentCountLength);
|
|
67
|
+
writeU32BE(hdr, 24, fpLen);
|
|
68
|
+
const out = new Uint8Array(28 + fpLen);
|
|
69
|
+
out.set(hdr, 0);
|
|
70
|
+
out.set(filter.fingerprints, 28);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function decodeBinaryFuse(data: Uint8Array): BinaryFuseFilter | null {
|
|
75
|
+
const res = decodeBinaryFuseResult(data);
|
|
76
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
77
|
+
return res.value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function decodeBinaryFuseResult(data: Uint8Array): Result<BinaryFuseFilter | null, BinaryFuseDecodeError> {
|
|
81
|
+
if (data.byteLength === 0) return Result.ok(null);
|
|
82
|
+
if (data.byteLength < 28) return invalidBinaryFuse("filter data too short");
|
|
83
|
+
const seed = readU64BE(data, 0);
|
|
84
|
+
const segmentLength = readU32BE(data, 8);
|
|
85
|
+
const segmentLengthMask = readU32BE(data, 12);
|
|
86
|
+
const segmentCount = readU32BE(data, 16);
|
|
87
|
+
const segmentCountLength = readU32BE(data, 20);
|
|
88
|
+
const fpLen = readU32BE(data, 24);
|
|
89
|
+
if (data.byteLength < 28 + fpLen) return invalidBinaryFuse("filter data truncated");
|
|
90
|
+
const fingerprints = data.subarray(28, 28 + fpLen);
|
|
91
|
+
return Result.ok({
|
|
92
|
+
seed,
|
|
93
|
+
segmentLength,
|
|
94
|
+
segmentLengthMask,
|
|
95
|
+
segmentCount,
|
|
96
|
+
segmentCountLength,
|
|
97
|
+
fingerprints,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function binaryFuseContains(filter: BinaryFuseFilter, key: bigint): boolean {
|
|
102
|
+
const hash = mixsplit(key, filter.seed);
|
|
103
|
+
let f = Number(fingerprint(hash) & 0xffn);
|
|
104
|
+
const [h0, h1, h2] = getHashFromHash(filter, hash);
|
|
105
|
+
f ^= filter.fingerprints[h0] ^ filter.fingerprints[h1] ^ filter.fingerprints[h2];
|
|
106
|
+
return f === 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildFilterResult(keys: bigint[]): Result<BinaryFuseFilter | null, BinaryFuseBuildError> {
|
|
110
|
+
const size = keys.length;
|
|
111
|
+
const params = initParams(size);
|
|
112
|
+
if (!params) return Result.ok(null);
|
|
113
|
+
|
|
114
|
+
const filter: BinaryFuseFilter = {
|
|
115
|
+
seed: 0n,
|
|
116
|
+
segmentLength: params.segmentLength,
|
|
117
|
+
segmentLengthMask: params.segmentLengthMask,
|
|
118
|
+
segmentCount: params.segmentCount,
|
|
119
|
+
segmentCountLength: params.segmentCountLength,
|
|
120
|
+
fingerprints: params.fingerprints,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const capacity = filter.fingerprints.length;
|
|
124
|
+
const alone = new Uint32Array(capacity);
|
|
125
|
+
const t2count = new Uint8Array(capacity);
|
|
126
|
+
const reverseH = new Uint8Array(size);
|
|
127
|
+
const t2hash = new BigUint64Array(capacity);
|
|
128
|
+
const reverseOrder = new BigUint64Array(size + 1);
|
|
129
|
+
reverseOrder[size] = 1n;
|
|
130
|
+
|
|
131
|
+
const rng = { value: 1n };
|
|
132
|
+
let iterations = 0;
|
|
133
|
+
let expectedSize = size;
|
|
134
|
+
|
|
135
|
+
for (;;) {
|
|
136
|
+
iterations += 1;
|
|
137
|
+
if (iterations > MAX_ITERATIONS) {
|
|
138
|
+
return invalidBinaryFuseBuild("binary fuse: too many iterations");
|
|
139
|
+
}
|
|
140
|
+
t2count.fill(0);
|
|
141
|
+
t2hash.fill(0n);
|
|
142
|
+
reverseOrder.fill(0n, 0, expectedSize);
|
|
143
|
+
reverseOrder[expectedSize] = 1n;
|
|
144
|
+
|
|
145
|
+
filter.seed = splitmix64(rng);
|
|
146
|
+
|
|
147
|
+
let blockBits = 1;
|
|
148
|
+
while ((1 << blockBits) < filter.segmentCount) blockBits += 1;
|
|
149
|
+
const startPos = new Uint32Array(1 << blockBits);
|
|
150
|
+
for (let i = 0; i < startPos.length; i++) {
|
|
151
|
+
startPos[i] = Number((BigInt(i) * BigInt(expectedSize)) >> BigInt(blockBits));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
const hash = mixsplit(key, filter.seed);
|
|
156
|
+
let segmentIndex = Number(hash >> BigInt(64 - blockBits));
|
|
157
|
+
const mask = (1 << blockBits) - 1;
|
|
158
|
+
while (reverseOrder[startPos[segmentIndex]] !== 0n) {
|
|
159
|
+
segmentIndex = (segmentIndex + 1) & mask;
|
|
160
|
+
}
|
|
161
|
+
reverseOrder[startPos[segmentIndex]] = hash;
|
|
162
|
+
startPos[segmentIndex] += 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let error = 0;
|
|
166
|
+
let duplicates = 0;
|
|
167
|
+
const h012 = new Uint32Array(6);
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < expectedSize; i++) {
|
|
170
|
+
const hash = reverseOrder[i];
|
|
171
|
+
if (hash === 0n) continue;
|
|
172
|
+
const [index1, index2, index3] = getHashFromHash(filter, hash);
|
|
173
|
+
t2count[index1] = (t2count[index1] + 4) & 0xff;
|
|
174
|
+
t2hash[index1] ^= hash;
|
|
175
|
+
t2count[index2] = (t2count[index2] + 4) & 0xff;
|
|
176
|
+
t2count[index2] ^= 1;
|
|
177
|
+
t2hash[index2] ^= hash;
|
|
178
|
+
t2count[index3] = (t2count[index3] + 4) & 0xff;
|
|
179
|
+
t2count[index3] ^= 2;
|
|
180
|
+
t2hash[index3] ^= hash;
|
|
181
|
+
|
|
182
|
+
if ((t2hash[index1] & t2hash[index2] & t2hash[index3]) === 0n) {
|
|
183
|
+
if (
|
|
184
|
+
(t2hash[index1] === 0n && t2count[index1] === 8) ||
|
|
185
|
+
(t2hash[index2] === 0n && t2count[index2] === 8) ||
|
|
186
|
+
(t2hash[index3] === 0n && t2count[index3] === 8)
|
|
187
|
+
) {
|
|
188
|
+
duplicates += 1;
|
|
189
|
+
t2count[index1] = (t2count[index1] - 4) & 0xff;
|
|
190
|
+
t2hash[index1] ^= hash;
|
|
191
|
+
t2count[index2] = (t2count[index2] - 4) & 0xff;
|
|
192
|
+
t2count[index2] ^= 1;
|
|
193
|
+
t2hash[index2] ^= hash;
|
|
194
|
+
t2count[index3] = (t2count[index3] - 4) & 0xff;
|
|
195
|
+
t2count[index3] ^= 2;
|
|
196
|
+
t2hash[index3] ^= hash;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (t2count[index1] < 4 || t2count[index2] < 4 || t2count[index3] < 4) {
|
|
200
|
+
error = 1;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (error === 1) continue;
|
|
205
|
+
|
|
206
|
+
let qsize = 0;
|
|
207
|
+
for (let i = 0; i < capacity; i++) {
|
|
208
|
+
if ((t2count[i] >> 2) === 1) {
|
|
209
|
+
alone[qsize++] = i >>> 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let stacksize = 0;
|
|
214
|
+
while (qsize > 0) {
|
|
215
|
+
qsize -= 1;
|
|
216
|
+
const index = alone[qsize];
|
|
217
|
+
if ((t2count[index] >> 2) !== 1) continue;
|
|
218
|
+
const hash = t2hash[index];
|
|
219
|
+
const found = t2count[index] & 3;
|
|
220
|
+
reverseH[stacksize] = found;
|
|
221
|
+
reverseOrder[stacksize] = hash;
|
|
222
|
+
stacksize += 1;
|
|
223
|
+
|
|
224
|
+
const [index1, index2, index3] = getHashFromHash(filter, hash);
|
|
225
|
+
h012[1] = index2;
|
|
226
|
+
h012[2] = index3;
|
|
227
|
+
h012[3] = index1;
|
|
228
|
+
h012[4] = h012[1];
|
|
229
|
+
|
|
230
|
+
const other1 = h012[found + 1];
|
|
231
|
+
if ((t2count[other1] >> 2) === 2) alone[qsize++] = other1;
|
|
232
|
+
t2count[other1] = (t2count[other1] - 4) & 0xff;
|
|
233
|
+
t2count[other1] ^= mod3(found + 1);
|
|
234
|
+
t2hash[other1] ^= hash;
|
|
235
|
+
|
|
236
|
+
const other2 = h012[found + 2];
|
|
237
|
+
if ((t2count[other2] >> 2) === 2) alone[qsize++] = other2;
|
|
238
|
+
t2count[other2] = (t2count[other2] - 4) & 0xff;
|
|
239
|
+
t2count[other2] ^= mod3(found + 2);
|
|
240
|
+
t2hash[other2] ^= hash;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (stacksize + duplicates === expectedSize) {
|
|
244
|
+
expectedSize = stacksize;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (expectedSize === 0) return Result.ok(filter);
|
|
250
|
+
|
|
251
|
+
for (let i = expectedSize - 1; i >= 0; i--) {
|
|
252
|
+
const hash = reverseOrder[i];
|
|
253
|
+
const xor2 = Number(fingerprint(hash) & 0xffn);
|
|
254
|
+
const [index1, index2, index3] = getHashFromHash(filter, hash);
|
|
255
|
+
const found = reverseH[i];
|
|
256
|
+
const h0 = index1;
|
|
257
|
+
const h1 = index2;
|
|
258
|
+
const h2 = index3;
|
|
259
|
+
const h3 = h0;
|
|
260
|
+
const h4 = h1;
|
|
261
|
+
const dest = found === 0 ? h0 : found === 1 ? h1 : h2;
|
|
262
|
+
const v1 = found === 0 ? h1 : found === 1 ? h2 : h3;
|
|
263
|
+
const v2 = found === 0 ? h2 : found === 1 ? h3 : h4;
|
|
264
|
+
filter.fingerprints[dest] = xor2 ^ filter.fingerprints[v1] ^ filter.fingerprints[v2];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return Result.ok(filter);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function initParams(size: number): {
|
|
271
|
+
segmentLength: number;
|
|
272
|
+
segmentLengthMask: number;
|
|
273
|
+
segmentCount: number;
|
|
274
|
+
segmentCountLength: number;
|
|
275
|
+
fingerprints: Uint8Array;
|
|
276
|
+
} | null {
|
|
277
|
+
const arity = 3;
|
|
278
|
+
let segmentLength = calculateSegmentLength(arity, size);
|
|
279
|
+
if (segmentLength > 262144) segmentLength = 262144;
|
|
280
|
+
const segmentLengthMask = segmentLength - 1;
|
|
281
|
+
const sizeFactor = calculateSizeFactor(arity, size);
|
|
282
|
+
let capacity = 0;
|
|
283
|
+
if (size > 1) capacity = Math.round(size * sizeFactor);
|
|
284
|
+
const initSegmentCount = Math.floor((capacity + segmentLength - 1) / segmentLength) - (arity - 1);
|
|
285
|
+
let arrayLength = (initSegmentCount + arity - 1) * segmentLength;
|
|
286
|
+
let segmentCount = Math.floor((arrayLength + segmentLength - 1) / segmentLength);
|
|
287
|
+
if (segmentCount <= arity - 1) segmentCount = 1;
|
|
288
|
+
else segmentCount -= arity - 1;
|
|
289
|
+
arrayLength = (segmentCount + arity - 1) * segmentLength;
|
|
290
|
+
const segmentCountLength = (segmentCount * segmentLength) >>> 0;
|
|
291
|
+
const fingerprints = new Uint8Array(arrayLength);
|
|
292
|
+
return {
|
|
293
|
+
segmentLength,
|
|
294
|
+
segmentLengthMask,
|
|
295
|
+
segmentCount,
|
|
296
|
+
segmentCountLength,
|
|
297
|
+
fingerprints,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function calculateSegmentLength(arity: number, size: number): number {
|
|
302
|
+
if (size === 0) return 4;
|
|
303
|
+
if (arity === 3) {
|
|
304
|
+
return 1 << Math.floor(Math.log(size) / Math.log(3.33) + 2.25);
|
|
305
|
+
}
|
|
306
|
+
if (arity === 4) {
|
|
307
|
+
return 1 << Math.floor(Math.log(size) / Math.log(2.91) - 0.5);
|
|
308
|
+
}
|
|
309
|
+
return 65536;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function calculateSizeFactor(arity: number, size: number): number {
|
|
313
|
+
if (arity === 3) {
|
|
314
|
+
return Math.max(1.125, 0.875 + (0.25 * Math.log(1_000_000)) / Math.log(size));
|
|
315
|
+
}
|
|
316
|
+
if (arity === 4) {
|
|
317
|
+
return Math.max(1.075, 0.77 + (0.305 * Math.log(600_000)) / Math.log(size));
|
|
318
|
+
}
|
|
319
|
+
return 2.0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function mod3(x: number): number {
|
|
323
|
+
return x > 2 ? x - 3 : x;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getHashFromHash(filter: BinaryFuseFilter, hash: bigint): [number, number, number] {
|
|
327
|
+
const hi = (hash * BigInt(filter.segmentCountLength)) >> 64n;
|
|
328
|
+
let h0 = Number(hi & 0xffffffffn) >>> 0;
|
|
329
|
+
let h1 = (h0 + filter.segmentLength) >>> 0;
|
|
330
|
+
let h2 = (h1 + filter.segmentLength) >>> 0;
|
|
331
|
+
h1 = (h1 ^ Number((hash >> 18n) & BigInt(filter.segmentLengthMask))) >>> 0;
|
|
332
|
+
h2 = (h2 ^ Number(hash & BigInt(filter.segmentLengthMask))) >>> 0;
|
|
333
|
+
return [h0, h1, h2];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function uniqueSorted(keys: bigint[]): bigint[] {
|
|
337
|
+
if (keys.length <= 1) return keys.slice();
|
|
338
|
+
let sorted = true;
|
|
339
|
+
for (let i = 1; i < keys.length; i++) {
|
|
340
|
+
if (keys[i - 1] > keys[i]) {
|
|
341
|
+
sorted = false;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const out = sorted ? keys.slice() : keys.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
346
|
+
let write = 1;
|
|
347
|
+
for (let i = 1; i < out.length; i++) {
|
|
348
|
+
if (out[i] !== out[write - 1]) {
|
|
349
|
+
out[write] = out[i];
|
|
350
|
+
write += 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return out.slice(0, write);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function murmur64(h: bigint): bigint {
|
|
357
|
+
h ^= h >> 33n;
|
|
358
|
+
h = (h * 0xff51afd7ed558ccdn) & MASK_64;
|
|
359
|
+
h ^= h >> 33n;
|
|
360
|
+
h = (h * 0xc4ceb9fe1a85ec53n) & MASK_64;
|
|
361
|
+
h ^= h >> 33n;
|
|
362
|
+
return h & MASK_64;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function splitmix64(seed: { value: bigint }): bigint {
|
|
366
|
+
seed.value = (seed.value + 0x9e3779b97f4a7c15n) & MASK_64;
|
|
367
|
+
let z = seed.value;
|
|
368
|
+
z = ((z ^ (z >> 30n)) * 0xbf58476d1ce4e5b9n) & MASK_64;
|
|
369
|
+
z = ((z ^ (z >> 27n)) * 0x94d049bb133111ebn) & MASK_64;
|
|
370
|
+
return (z ^ (z >> 31n)) & MASK_64;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function mixsplit(key: bigint, seed: bigint): bigint {
|
|
374
|
+
return murmur64((key + seed) & MASK_64);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function fingerprint(hash: bigint): bigint {
|
|
378
|
+
return hash ^ (hash >> 32n);
|
|
379
|
+
}
|