pi-mega-compact 0.4.20 → 0.4.23
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/dist/extensions/conflict-scan.js +201 -0
- package/dist/extensions/dashboard-server.js +3 -3
- package/dist/extensions/mega-compact-driver.js +79 -0
- package/dist/extensions/mega-compact.js +2 -0
- package/dist/extensions/mega-compact.test.js +54 -18
- package/dist/extensions/mega-config.js +10 -0
- package/dist/extensions/mega-conflict-cmds.js +121 -0
- package/dist/extensions/mega-events.js +45 -23
- package/dist/extensions/mega-pipeline.js +80 -8
- package/dist/extensions/mega-runtime.js +14 -20
- package/dist/src/config/dedup.js +4 -1
- package/dist/src/config.js +21 -0
- package/dist/src/dedup/raptor/index.js +28 -6
- package/dist/src/dedup/raptor/promote.test.js +69 -0
- package/dist/src/engine.js +1 -0
- package/dist/src/recall.js +30 -4
- package/dist/src/recall.test.js +28 -0
- package/dist/src/store/backfill.js +5 -6
- package/dist/src/store/compression.js +47 -7
- package/dist/src/store/compression.test.js +48 -0
- package/dist/src/store/sqlite.js +123 -41
- package/dist/src/store.test.js +19 -0
- package/dist/src/vectorStore.js +56 -1
- package/extensions/DASHBOARD.md +3 -3
- package/extensions/conflict-scan.ts +209 -0
- package/extensions/dashboard-server.ts +4 -4
- package/extensions/mega-compact-driver.ts +105 -0
- package/extensions/mega-compact.test.ts +65 -18
- package/extensions/mega-compact.ts +2 -0
- package/extensions/mega-config.ts +25 -0
- package/extensions/mega-conflict-cmds.ts +129 -0
- package/extensions/mega-events.ts +43 -24
- package/extensions/mega-pipeline.ts +86 -9
- package/extensions/mega-runtime.ts +14 -18
- package/package.json +6 -7
- package/src/config/dedup.ts +4 -1
- package/src/config.ts +26 -0
- package/src/dedup/raptor/index.ts +42 -7
- package/src/dedup/raptor/promote.test.ts +82 -0
- package/src/engine.ts +5 -0
- package/src/recall.test.ts +44 -0
- package/src/recall.ts +43 -4
- package/src/store/backfill.ts +10 -11
- package/src/store/compression.test.ts +58 -0
- package/src/store/compression.ts +48 -7
- package/src/store/sqlite.ts +156 -49
- package/src/store.test.ts +22 -0
- package/src/vectorStore.ts +63 -1
- package/dist/extensions/openclaw-mega-compact.js +0 -291
- package/dist/src/minilm.js +0 -92
- package/dist/src/wordpiece.js +0 -129
package/src/recall.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { recall as searchRecall } from "./engine.js";
|
|
18
18
|
import type { SearchHit, VectorStore } from "./vectorStore.js";
|
|
19
|
+
import { estimateBlockTokens } from "./tokens.js";
|
|
20
|
+
import { defaultEmbedder, cosineSimilarity } from "./embedder.js";
|
|
19
21
|
|
|
20
22
|
export type RecallSource = "resume" | "command" | "sentinel";
|
|
21
23
|
|
|
@@ -26,6 +28,16 @@ export interface RecallInjectOptions {
|
|
|
26
28
|
source: RecallSource;
|
|
27
29
|
/** Skip checkpoints already injected this session (recall dedup). */
|
|
28
30
|
skipInjected?: boolean;
|
|
31
|
+
/** Token ceiling for the re-injected block (Fix C). Recall stops adding once
|
|
32
|
+
* the block would exceed this, so the read path can never net-inflate. */
|
|
33
|
+
recallMaxTokens?: number;
|
|
34
|
+
/** Inline-dedupe hits against the live window (Fix C): drop a hit whose
|
|
35
|
+
* summary is ≥ `dedupSim` similar to a live message. */
|
|
36
|
+
windowDedupe?: boolean;
|
|
37
|
+
/** Live window text (from the session manager) used for inline dedupe. */
|
|
38
|
+
liveWindow?: string[];
|
|
39
|
+
/** Similarity threshold for inline dedupe (defaults to 0.9). */
|
|
40
|
+
dedupSim?: number;
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
export interface RecallInjectResult {
|
|
@@ -70,23 +82,50 @@ export function recallAndInline(
|
|
|
70
82
|
): RecallInjectResult {
|
|
71
83
|
const limit = opts.limit ?? 3;
|
|
72
84
|
const skip = opts.skipInjected ?? true;
|
|
85
|
+
const maxTokens = opts.recallMaxTokens ?? 0; // 0 = unbounded (legacy behavior)
|
|
86
|
+
const doWindowDedupe = opts.windowDedupe ?? false;
|
|
87
|
+
const dedupSim = opts.dedupSim ?? 0.9;
|
|
73
88
|
|
|
74
89
|
const { hits } = searchRecall(
|
|
75
90
|
{ sessionId: opts.sessionId, query: opts.query, limit, skipInjected: false },
|
|
76
91
|
store as VectorStore,
|
|
77
92
|
);
|
|
78
93
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
94
|
+
// Precompute live-window embeddings once for inline dedupe (Fix C). Trigram
|
|
95
|
+
// embedder is local + cheap; never a network call (PREVENT-PI-004).
|
|
96
|
+
let liveEmbeddings: number[][] = [];
|
|
97
|
+
if (doWindowDedupe && opts.liveWindow && opts.liveWindow.length > 0) {
|
|
98
|
+
const embedder = defaultEmbedder();
|
|
99
|
+
liveEmbeddings = opts.liveWindow.map((m) => embedder.embed(m));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Shared dedup + bounded/inline block assembly. We build the block
|
|
103
|
+
// incrementally so the token cap can stop mid-stream (Fix C).
|
|
82
104
|
const toInject: SearchHit[] = [];
|
|
105
|
+
const parts: string[] = [];
|
|
106
|
+
let blockTokens = 0;
|
|
107
|
+
|
|
83
108
|
for (const h of hits) {
|
|
84
109
|
if (skip && store.wasInjected(opts.sessionId, h.checkpoint.checkpointId)) continue;
|
|
110
|
+
|
|
111
|
+
// Inline dedupe: skip a hit already resident in the live window (Fix C).
|
|
112
|
+
if (doWindowDedupe && liveEmbeddings.length > 0) {
|
|
113
|
+
const hitVec = defaultEmbedder().embed(h.checkpoint.summary);
|
|
114
|
+
if (liveEmbeddings.some((v) => cosineSimilarity(v, hitVec) >= dedupSim)) continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const part = formatRecallBlock([h]);
|
|
118
|
+
const partTokens = estimateBlockTokens(part);
|
|
119
|
+
// Token cap: never push a chunk that would overrun the ceiling.
|
|
120
|
+
if (maxTokens > 0 && blockTokens + partTokens > maxTokens) break;
|
|
121
|
+
|
|
122
|
+
parts.push(part);
|
|
85
123
|
toInject.push(h);
|
|
124
|
+
blockTokens += partTokens;
|
|
86
125
|
store.markInjected(opts.sessionId, h.checkpoint.checkpointId);
|
|
87
126
|
}
|
|
88
127
|
|
|
89
|
-
const block =
|
|
128
|
+
const block = parts.join("\n");
|
|
90
129
|
const report = toInject.map(
|
|
91
130
|
(h) => ` • ${h.checkpoint.checkpointId} (${h.checkpoint.summary.slice(0, 60).replace(/\n/g, " ")}…)`,
|
|
92
131
|
);
|
package/src/store/backfill.ts
CHANGED
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
* SQLite is the source of truth; this touches no network (PREVENT-PI-004).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type {
|
|
19
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
20
20
|
import { openStore } from "./sqlite.js";
|
|
21
21
|
import { computeContentDigest } from "../dedup/digest.js";
|
|
22
22
|
import { minhashSignature, SIGNATURE_VERSION, NUM_HASHES } from "../dedup/l1-minhash.js";
|
|
23
23
|
import { lshBands } from "../dedup/l1-lsh.js";
|
|
24
|
-
import { upsertMinhashSignature, insertLshBuckets, listCheckpoints, saveRaptorTree } from "./sqlite.js";
|
|
24
|
+
import { upsertMinhashSignature, insertLshBuckets, listCheckpoints, saveRaptorTree, withTx } from "./sqlite.js";
|
|
25
25
|
import { buildRaptorTree, type Leaf } from "../dedup/raptor/tree.js";
|
|
26
26
|
import type { Embedder } from "../embedder.js";
|
|
27
27
|
import { defaultEmbedder } from "../embedder.js";
|
|
@@ -45,7 +45,7 @@ interface PhaseProgressRow {
|
|
|
45
45
|
processed: number;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function ensureProgressTable(db:
|
|
48
|
+
function ensureProgressTable(db: DatabaseSync): void {
|
|
49
49
|
db.exec(`
|
|
50
50
|
CREATE TABLE IF NOT EXISTS backfill_progress (
|
|
51
51
|
name TEXT PRIMARY KEY,
|
|
@@ -57,7 +57,7 @@ function ensureProgressTable(db: Database): void {
|
|
|
57
57
|
`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function progress(db:
|
|
60
|
+
function progress(db: DatabaseSync): { lastSid: string | null; lastId: string | null; updated: number; dups: number } {
|
|
61
61
|
const row = db
|
|
62
62
|
.prepare("SELECT last_session_id, last_id, updated, duplicates_resolved FROM backfill_progress WHERE name='content_hashes'")
|
|
63
63
|
.get() as { last_session_id: string | null; last_id: string | null; updated: number; duplicates_resolved: number } | undefined;
|
|
@@ -91,7 +91,7 @@ export function backfillContentHashes(stateDir: string = getStateDir()): Backfil
|
|
|
91
91
|
let lastSid = start.lastSid;
|
|
92
92
|
let lastId = start.lastId;
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
function applyRows(rows: { id: string; session_id: string; summary: string }[]): void {
|
|
95
95
|
const lookup = db.prepare(
|
|
96
96
|
"SELECT id FROM context_chunks WHERE session_id = ? AND content_hash = ? AND content_hash2 = ? AND id != ? LIMIT 1",
|
|
97
97
|
);
|
|
@@ -123,10 +123,10 @@ export function backfillContentHashes(stateDir: string = getStateDir()): Backfil
|
|
|
123
123
|
lastId = row.id;
|
|
124
124
|
processed++;
|
|
125
125
|
}
|
|
126
|
-
}
|
|
126
|
+
}
|
|
127
127
|
|
|
128
128
|
if (pending.length > 0) {
|
|
129
|
-
|
|
129
|
+
withTx(db, () => applyRows(pending));
|
|
130
130
|
db.prepare(
|
|
131
131
|
"INSERT INTO backfill_progress(name, last_session_id, last_id, updated, duplicates_resolved) VALUES('content_hashes',?,?,?,?) ON CONFLICT(name) DO UPDATE SET last_session_id=excluded.last_session_id, last_id=excluded.last_id, updated=excluded.updated, duplicates_resolved=excluded.duplicates_resolved",
|
|
132
132
|
).run(lastSid, lastId, updated, duplicatesResolved);
|
|
@@ -150,7 +150,7 @@ export function isBackfillComplete(stateDir: string = getStateDir()): boolean {
|
|
|
150
150
|
|
|
151
151
|
// ---- Sprint 14: L1 / L2 / RAPTOR phase backfill (resumable) ---------------
|
|
152
152
|
|
|
153
|
-
function phaseCursor(db:
|
|
153
|
+
function phaseCursor(db: DatabaseSync, phase: BackfillPhase): { lastId: string | null; processed: number } {
|
|
154
154
|
ensureProgressTable(db);
|
|
155
155
|
const row = db
|
|
156
156
|
.prepare("SELECT last_id, updated AS processed FROM backfill_progress WHERE name = ?")
|
|
@@ -158,7 +158,7 @@ function phaseCursor(db: Database, phase: BackfillPhase): { lastId: string | nul
|
|
|
158
158
|
return { lastId: row?.last_id ?? null, processed: row?.processed ?? 0 };
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
function savePhaseCursor(db:
|
|
161
|
+
function savePhaseCursor(db: DatabaseSync, phase: BackfillPhase, lastId: string | null, processed: number): void {
|
|
162
162
|
db.prepare(
|
|
163
163
|
`INSERT INTO backfill_progress(name, last_session_id, last_id, updated, duplicates_resolved)
|
|
164
164
|
VALUES(?, NULL, ?, ?, 0)
|
|
@@ -201,7 +201,7 @@ export function backfillPhase(
|
|
|
201
201
|
|
|
202
202
|
for (let i = Math.max(0, startIndex); i < all.length; i += batchSize) {
|
|
203
203
|
const batch = all.slice(i, i + batchSize);
|
|
204
|
-
|
|
204
|
+
withTx(db, () => {
|
|
205
205
|
for (const cp of batch) {
|
|
206
206
|
const sig = minhashSignature(cp.normalizedText ?? cp.summary ?? "");
|
|
207
207
|
if (sig.length === NUM_HASHES) {
|
|
@@ -217,7 +217,6 @@ export function backfillPhase(
|
|
|
217
217
|
processed++;
|
|
218
218
|
}
|
|
219
219
|
});
|
|
220
|
-
tx();
|
|
221
220
|
savePhaseCursor(db, phase, cursor ?? null, processed);
|
|
222
221
|
batches++;
|
|
223
222
|
if (THROTTLE_MS > 0) { const end = Date.now() + THROTTLE_MS; while (Date.now() < end) { /* throttle */ } }
|
|
@@ -81,3 +81,61 @@ test("zstd helper roundtrips (async) and is not sync-decoded", async () => {
|
|
|
81
81
|
assert.equal(auto.isZstd, true, "flagged as zstd");
|
|
82
82
|
assert.deepEqual(await decompressZstd(c), data, "zstd roundtrip");
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
test("module loads without a top-level zstd import (Fix A: no load crash)", async () => {
|
|
86
|
+
// The extension must load even when the @mongodb-js/zstd native addon is
|
|
87
|
+
// absent (clean/allowScripts-blocked install). The dynamic import() lives
|
|
88
|
+
// inside the helpers, so importing this module must never throw.
|
|
89
|
+
const mod = await import("./compression.js");
|
|
90
|
+
assert.equal(typeof mod.compressSmart, "function", "compressSmart exported");
|
|
91
|
+
assert.equal(typeof mod.compressZstd, "function", "compressZstd exported");
|
|
92
|
+
// The real invariant: no STATIC `import ... from "@mongodb-js/zstd"` at the
|
|
93
|
+
// top level (that's what crashed the whole extension). zstd must be loaded
|
|
94
|
+
// lazily inside the helpers only. Check the source text.
|
|
95
|
+
const { readFileSync } = await import("node:fs");
|
|
96
|
+
const { join } = await import("node:path");
|
|
97
|
+
// Tests run with cwd at repo root (`node --test`), so resolve the source.
|
|
98
|
+
const src = readFileSync(join(process.cwd(), "src/store/compression.ts"), "utf-8");
|
|
99
|
+
const staticImport = /^import\s+.+\s+from\s+["']@mongodb-js\/zstd["'];?$/m;
|
|
100
|
+
assert.equal(
|
|
101
|
+
staticImport.test(src),
|
|
102
|
+
false,
|
|
103
|
+
"no static top-level import of @mongodb-js/zstd (would crash load if binary absent)",
|
|
104
|
+
);
|
|
105
|
+
assert.ok(
|
|
106
|
+
src.includes('await import("@mongodb-js/zstd")'),
|
|
107
|
+
"zstd is loaded lazily via dynamic import() inside the helpers",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("compressSmart escalates brotli quality with pressure (Fix E)", () => {
|
|
112
|
+
// Large (>32KB) payloads hit the brotli tier; higher pressure → brotli-11
|
|
113
|
+
// → smaller output than the default brotli-4, and still decodes.
|
|
114
|
+
const words = Array.from({ length: 6000 }, (_, i) => "word" + ((i * 2654435761) % 9973));
|
|
115
|
+
const big = Buffer.from(words.join(" "));
|
|
116
|
+
const low = compressSmart(big, 0);
|
|
117
|
+
const high = compressSmart(big, 1);
|
|
118
|
+
assert.equal(isVersioned(low), true, "versioned header preserved at p=0");
|
|
119
|
+
assert.equal(isVersioned(high), true, "versioned header preserved at p=1");
|
|
120
|
+
assert.ok(high.length < low.length, "high pressure compresses smaller");
|
|
121
|
+
assert.deepEqual(decompressSmart(low), big, "p=0 roundtrip");
|
|
122
|
+
assert.deepEqual(decompressSmart(high), big, "p=1 roundtrip");
|
|
123
|
+
// Small payloads ignore pressure (gzip tier) but still roundtrip.
|
|
124
|
+
const small = buf("hello world ", 300);
|
|
125
|
+
assert.deepEqual(decompressSmart(compressSmart(small, 1)), small, "small ignores pressure");
|
|
126
|
+
// pressure out of range is clamped (no throw, still versioned + decodable).
|
|
127
|
+
assert.deepEqual(decompressSmart(compressSmart(big, 5)), big, "over-pressure clamped");
|
|
128
|
+
assert.deepEqual(decompressSmart(compressSmart(big, -1)), big, "under-pressure clamped");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("pressureFromPct + preserveRecentForPressure scale with context (Fix E)", async () => {
|
|
132
|
+
const { pressureFromPct, preserveRecentForPressure } = await import("../config.js");
|
|
133
|
+
assert.equal(pressureFromPct(50), 0.5, "pct→pressure");
|
|
134
|
+
assert.equal(pressureFromPct(null), 0, "null pct → 0");
|
|
135
|
+
assert.equal(pressureFromPct(150), 1, "pct clamped");
|
|
136
|
+
// low pressure keeps preserveRecent; high pressure compacts deeper (min floor).
|
|
137
|
+
assert.equal(preserveRecentForPressure(0, 4, 2), 4, "p=0 → preserveRecent");
|
|
138
|
+
assert.equal(preserveRecentForPressure(1, 4, 2), 2, "p=1 → preserveRecentMin");
|
|
139
|
+
assert.equal(preserveRecentForPressure(0.5, 4, 2), 3, "p=0.5 → interpolates");
|
|
140
|
+
assert.ok(preserveRecentForPressure(1, 4, 2) >= 2, "never below floor");
|
|
141
|
+
});
|
package/src/store/compression.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. `compressSmart` / `decompressSmart` — SYNCHRONOUS, zlib-based. Used by the
|
|
7
7
|
* VectorStore write path (which must stay synchronous — see Sprint 8 plan:
|
|
8
|
-
*
|
|
8
|
+
* node:sqlite replaced PGlite precisely to avoid an async cascade).
|
|
9
9
|
*
|
|
10
10
|
* 2. `compressZstd` / `decompressZstd` — ASYNCHRONOUS, via @mongodb-js/zstd.
|
|
11
11
|
* Optional, used for DR-export / large-blob paths where an await is fine.
|
|
@@ -32,7 +32,13 @@ import {
|
|
|
32
32
|
brotliDecompressSync,
|
|
33
33
|
constants as zlibConstants,
|
|
34
34
|
} from "node:zlib";
|
|
35
|
-
|
|
35
|
+
// zstd is loaded lazily (see compressZstdWithLevel / decompressZstd). It is an
|
|
36
|
+
// OPTIONAL async DR-export dependency: its native addon (`zstd.node`) is not in
|
|
37
|
+
// the npm tarball and may be absent on a clean/allowScripts-blocked install, so
|
|
38
|
+
// a static import here would crash the whole extension at load time. Lazy
|
|
39
|
+
// import keeps the extension loadable even when the binary is missing; the DR
|
|
40
|
+
// path throws a clear error only if it is actually used. (Fix A.)
|
|
41
|
+
// import zstd from "@mongodb-js/zstd";
|
|
36
42
|
|
|
37
43
|
// --- Versioned format markers ----------------------------------------------
|
|
38
44
|
const MAGIC_HI = 0xec;
|
|
@@ -59,6 +65,12 @@ function header(ver: number, tag: number): Buffer {
|
|
|
59
65
|
return Buffer.from([MAGIC_HI, MAGIC_LO, ver, tag]);
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
/** Clamp a value to the [0, 1] range (pressure bands). */
|
|
69
|
+
function clamp01(n: number): number {
|
|
70
|
+
if (Number.isNaN(n)) return 0;
|
|
71
|
+
return n < 0 ? 0 : n > 1 ? 1 : n;
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
/**
|
|
63
75
|
* Compress synchronously using the best zlib tier for the payload size.
|
|
64
76
|
*
|
|
@@ -68,21 +80,36 @@ function header(ver: number, tag: number): Buffer {
|
|
|
68
80
|
* 4KB–32KB → gzip level 6 (tag 0x02)
|
|
69
81
|
* > 32 KB → brotli 4 (tag 0x05)
|
|
70
82
|
*
|
|
71
|
-
*
|
|
83
|
+
* `pressure` (0–1, optional) escalates the brotli quality for the large tier
|
|
84
|
+
* when the session is near its context limit — the "variable compression as we
|
|
85
|
+
* approach the limit" design (Fix E). Low/undefined pressure keeps brotli-4;
|
|
86
|
+
* high pressure pushes toward brotli-11. Stays fully synchronous (brotli-11 is
|
|
87
|
+
* sync via brotliCompressSync) so the sync `add()` contract is preserved; zstd
|
|
88
|
+
* is reserved for the async DR-export path only. Same versioned header/tags for
|
|
89
|
+
* every pressure, so decompressSmart is unaffected.
|
|
72
90
|
*/
|
|
73
|
-
export function compressSmart(data: Buffer): Buffer {
|
|
91
|
+
export function compressSmart(data: Buffer, pressure = 0): Buffer {
|
|
92
|
+
const p = clamp01(pressure);
|
|
74
93
|
const len = data.length;
|
|
75
94
|
if (len < SIZE_TINY) {
|
|
76
95
|
return Buffer.concat([header(1, TAG_RAW), data]);
|
|
77
96
|
}
|
|
78
97
|
if (len < SIZE_SMALL) {
|
|
79
|
-
|
|
98
|
+
// Small tier: escalate gzip level 1 → 9 with context pressure (Fix E) so
|
|
99
|
+
// the "variable compression as we approach the limit" dial bites for
|
|
100
|
+
// short sessions too, not just the >32KB brotli tier.
|
|
101
|
+
const level = Math.max(1, Math.min(9, Math.round(1 + 8 * p)));
|
|
102
|
+
return Buffer.concat([header(1, TAG_GZIP_1), gzipSync(data, { level })]);
|
|
80
103
|
}
|
|
81
104
|
if (len < SIZE_MEDIUM) {
|
|
82
|
-
|
|
105
|
+
// Medium tier: escalate gzip level 6 → 9 with context pressure (Fix E).
|
|
106
|
+
const level = Math.max(6, Math.min(9, Math.round(6 + 3 * p)));
|
|
107
|
+
return Buffer.concat([header(1, TAG_GZIP_6), gzipSync(data, { level })]);
|
|
83
108
|
}
|
|
109
|
+
// Large tier: escalate brotli quality 4 → 11 with context pressure (Fix E).
|
|
110
|
+
const quality = Math.max(4, Math.min(11, Math.round(4 + 7 * p)));
|
|
84
111
|
const compressed = brotliCompressSync(data, {
|
|
85
|
-
params: { [zlibConstants.BROTLI_PARAM_QUALITY]:
|
|
112
|
+
params: { [zlibConstants.BROTLI_PARAM_QUALITY]: quality },
|
|
86
113
|
});
|
|
87
114
|
return Buffer.concat([header(1, TAG_BROTLI_4), compressed]);
|
|
88
115
|
}
|
|
@@ -164,6 +191,18 @@ const ZSTD_MAGIC_HI = 0x5a; // 'Z'
|
|
|
164
191
|
const ZSTD_MAGIC_LO = 0x53; // 'S'
|
|
165
192
|
|
|
166
193
|
async function compressZstdWithLevel(data: Buffer, level: number): Promise<Buffer> {
|
|
194
|
+
// Lazy import: the native addon may be absent (clean/allowScripts install).
|
|
195
|
+
// Throws a clear, actionable error instead of a load-time crash.
|
|
196
|
+
let zstd: typeof import("@mongodb-js/zstd");
|
|
197
|
+
try {
|
|
198
|
+
zstd = await import("@mongodb-js/zstd");
|
|
199
|
+
} catch {
|
|
200
|
+
throw new Error(
|
|
201
|
+
"zstd is not available — the @mongodb-js/zstd native addon (zstd.node) " +
|
|
202
|
+
"was not built. Run the extension's native install step (or allow npm " +
|
|
203
|
+
"install scripts) to enable DR-export compression.",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
167
206
|
const compressed = await zstd.compress(data, level);
|
|
168
207
|
return Buffer.concat([Buffer.from([ZSTD_MAGIC_HI, ZSTD_MAGIC_LO]), compressed]);
|
|
169
208
|
}
|
|
@@ -189,6 +228,8 @@ export async function decompressZstd(buf: Buffer): Promise<Buffer> {
|
|
|
189
228
|
if (!isZstd(buf)) {
|
|
190
229
|
throw new Error("decompressZstd: buffer is not a zstd blob (missing ZS marker)");
|
|
191
230
|
}
|
|
231
|
+
// Lazy import (see compressZstdWithLevel for rationale).
|
|
232
|
+
const zstd = await import("@mongodb-js/zstd");
|
|
192
233
|
return zstd.decompress(buf.subarray(2));
|
|
193
234
|
}
|
|
194
235
|
|