@harperfast/rocksdb-js 2.4.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +224 -15
- package/bin/rocksdb-js.mjs +67 -3
- package/dist/index.cjs +293 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1382 -1125
- package/dist/index.d.mts +1382 -1125
- package/dist/index.mjs +294 -41
- package/dist/index.mjs.map +1 -1
- package/docs/backups.md +15 -0
- package/docs/stats.md +2 -0
- package/docs/transaction-log.md +10 -0
- package/package.json +17 -17
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
3
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { access, cp, mkdir, readdir, rm } from "node:fs/promises";
|
|
7
7
|
import * as orderedBinary from "ordered-binary";
|
|
@@ -9,7 +9,7 @@ import { ExtendedIterable } from "@harperfast/extended-iterable";
|
|
|
9
9
|
import { Encoder } from "msgpackr";
|
|
10
10
|
|
|
11
11
|
//#region \0rolldown/runtime.js
|
|
12
|
-
var __require = /*
|
|
12
|
+
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
13
13
|
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/load-binding.ts
|
|
@@ -62,6 +62,15 @@ const removeGlobalListener = binding.removeListener;
|
|
|
62
62
|
const globalListenerCount = binding.listenerCount;
|
|
63
63
|
const globalNotify = binding.notify;
|
|
64
64
|
const constants = binding.constants;
|
|
65
|
+
/**
|
|
66
|
+
* The friendly names of every compression algorithm compiled into the loaded
|
|
67
|
+
* RocksDB native binding, e.g. `['none', 'snappy', 'lz4', 'zstd']`. The set is
|
|
68
|
+
* fixed for a given binary (it depends on which compression libraries RocksDB
|
|
69
|
+
* was linked against), so this is a static list. `'none'` (no compression) is
|
|
70
|
+
* always present. Use it to validate a `compression` option or to pick an
|
|
71
|
+
* algorithm that is actually available at runtime.
|
|
72
|
+
*/
|
|
73
|
+
const supportedCompression = Object.freeze(binding.supportedCompression);
|
|
65
74
|
const NativeDatabase = binding.Database;
|
|
66
75
|
const NativeIterator = binding.Iterator;
|
|
67
76
|
const NativeTransaction = binding.Transaction;
|
|
@@ -90,13 +99,21 @@ const coolTransactionLogs = binding.coolTransactionLogs;
|
|
|
90
99
|
*/
|
|
91
100
|
const transactionLogMapCount = binding.transactionLogMapCount;
|
|
92
101
|
/**
|
|
102
|
+
* Test-only: force the next `count` transaction commits to fail with TryAgain (rolled back, so
|
|
103
|
+
* no data is committed), reproducing a stranded-snapshot conflict deterministically. Pass 0 to
|
|
104
|
+
* disarm. Used by the ERR_TRY_AGAIN retry regression test.
|
|
105
|
+
*/
|
|
106
|
+
const forceTryAgainForTesting = binding.forceTryAgainForTesting;
|
|
107
|
+
/**
|
|
93
108
|
* Creates a native file lock using the specified file path (`flock` on POSIX,
|
|
94
|
-
* `LockFileEx` on Windows)
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
109
|
+
* `LockFileEx` on Windows), creating the file and any missing parent
|
|
110
|
+
* directories. Exclusive by default; pass `shared` for a shared (reader) lock
|
|
111
|
+
* that coexists with other shared holders but conflicts with an exclusive
|
|
112
|
+
* holder in either direction. Returns an opaque non-zero token to pass to
|
|
113
|
+
* `fileLockRelease`, or `0` if a conflicting holder — in any process,
|
|
114
|
+
* container, or worker thread — currently has it. Throws on a hard error. The
|
|
115
|
+
* OS handle is owned entirely in native code (no fd crosses into JS), and the
|
|
116
|
+
* kernel releases the lock when the handle closes, including on process death.
|
|
100
117
|
*/
|
|
101
118
|
const tryFileLock = binding.tryFileLock;
|
|
102
119
|
/**
|
|
@@ -109,9 +126,51 @@ const nativeBackupList = binding.backupList;
|
|
|
109
126
|
const nativeBackupDelete = binding.backupDelete;
|
|
110
127
|
const nativeBackupPurge = binding.backupPurge;
|
|
111
128
|
const nativeBackupVerify = binding.backupVerify;
|
|
129
|
+
const nativeValidateTransactionLog = binding.validateTransactionLog;
|
|
112
130
|
const stats = binding.stats;
|
|
113
131
|
const version = binding.version;
|
|
114
132
|
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/validate-transaction-log.ts
|
|
135
|
+
/**
|
|
136
|
+
* Validates a transaction log store directory: every `<sequence>.txnlog` file
|
|
137
|
+
* is checked for a valid header (token, version) and intact v1 entry framing
|
|
138
|
+
* (using the same scan as open-time crash recovery), file-name and sequence
|
|
139
|
+
* continuity are checked, and the `txn.state` side file (when present) is
|
|
140
|
+
* checked for shape and a plausible flushed position. The result reports
|
|
141
|
+
* per-file entry counts, errors, and warnings; it does not throw when the
|
|
142
|
+
* store is invalid — check `result.valid`.
|
|
143
|
+
*
|
|
144
|
+
* Runs entirely in native code on a worker thread and does not require (or
|
|
145
|
+
* open) a database, so it works on the store of a closed database and on the
|
|
146
|
+
* per-backup snapshots under `<backupDir>/transaction_logs/<backupId>/` alike.
|
|
147
|
+
*
|
|
148
|
+
* Validating a store that is being actively appended to can spuriously report
|
|
149
|
+
* a torn tail for the current log file — the tail of an in-flight append is
|
|
150
|
+
* indistinguishable from a crash artifact.
|
|
151
|
+
*
|
|
152
|
+
* Rejects if `path` does not exist or is not a directory (a bad argument, as
|
|
153
|
+
* opposed to an invalid store).
|
|
154
|
+
*
|
|
155
|
+
* @param path The store directory, e.g. `<dbDir>/transaction_logs/<name>`.
|
|
156
|
+
* @param options Validation options.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* import { validateTransactionLogStore } from '@harperfast/rocksdb-js';
|
|
161
|
+
*
|
|
162
|
+
* const result = await validateTransactionLogStore('/path/to/db/transaction_logs/mylog');
|
|
163
|
+
* if (!result.valid) {
|
|
164
|
+
* for (const file of result.files) {
|
|
165
|
+
* console.error(file.file, file.errors);
|
|
166
|
+
* }
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
function validateTransactionLogStore(path, options) {
|
|
171
|
+
return new Promise((resolve, reject) => nativeValidateTransactionLog(resolve, reject, path, options?.strict ?? false));
|
|
172
|
+
}
|
|
173
|
+
|
|
115
174
|
//#endregion
|
|
116
175
|
//#region src/backup.ts
|
|
117
176
|
/** Subdirectory (under a backup directory) holding per-backup transaction log snapshots. */
|
|
@@ -127,17 +186,29 @@ async function exists(path) {
|
|
|
127
186
|
}
|
|
128
187
|
/**
|
|
129
188
|
* Runs `fn` while holding a native file lock for `backupDir`, releasing it when
|
|
130
|
-
* `fn` settles.
|
|
131
|
-
* `backups.purge`; backup creation
|
|
132
|
-
* same
|
|
189
|
+
* `fn` settles. The lock is exclusive by default — used by the writable-engine
|
|
190
|
+
* operations `backups.delete` and `backups.purge`; backup creation
|
|
191
|
+
* (`Store.backup`) takes the same exclusive lock on the same file natively
|
|
192
|
+
* inside `Database::Backup` (see `runCreateBackup` in
|
|
133
193
|
* `src/binding/database/backup.cpp`), where the backup directory is also
|
|
134
|
-
* created. Throws immediately (without running `fn`) if
|
|
135
|
-
*
|
|
194
|
+
* created. Throws immediately (without running `fn`) if the backup directory
|
|
195
|
+
* is missing — `tryFileLock` creates missing parents, but `delete`/`purge`/
|
|
196
|
+
* `restore` on a nonexistent directory is a caller error and must not conjure
|
|
197
|
+
* an empty one — or if a conflicting holder has the directory lock.
|
|
136
198
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* a
|
|
199
|
+
* `backups.restore` holds the lock in `shared` mode for its source read:
|
|
200
|
+
* shared holders coexist (concurrent restores are safe), but a writer's
|
|
201
|
+
* exclusive acquisition rejects while a restore is in flight — and vice versa.
|
|
202
|
+
* Without this, a `purge`/`delete` racing a restore can delete the very files
|
|
203
|
+
* the restore is copying, after the restore's default `purgeAllFiles` mode has
|
|
204
|
+
* already wiped the destination — failing the restore with no fallback. A
|
|
205
|
+
* rejected writer, by contrast, just retries later.
|
|
206
|
+
*
|
|
207
|
+
* The remaining read-only operations (`list`, `verify`) use
|
|
208
|
+
* `BackupEngineReadOnly` and are not locked: concurrent readers are safe, and
|
|
209
|
+
* locking them would make cheap listings reject during a long backup. Running
|
|
210
|
+
* them concurrently with a `delete`/`purge` on the same directory is a
|
|
211
|
+
* caller-managed hazard, matching RocksDB's one-writable-engine-per-dir model.
|
|
141
212
|
*
|
|
142
213
|
* RocksDB only serializes work within a single `BackupEngine` instance and has
|
|
143
214
|
* no lock on the backup directory itself. Two writable engines — in any
|
|
@@ -163,8 +234,9 @@ async function exists(path) {
|
|
|
163
234
|
* The lock file is never deleted, only locked and unlocked; an unlocked, empty
|
|
164
235
|
* `.backup.lock` at the directory root is the steady state.
|
|
165
236
|
*/
|
|
166
|
-
async function withBackupDirLock(backupDir, fn) {
|
|
167
|
-
|
|
237
|
+
async function withBackupDirLock(backupDir, fn, options) {
|
|
238
|
+
if (!await exists(backupDir)) throw new Error(`Backup directory does not exist: ${backupDir}`);
|
|
239
|
+
const token = tryFileLock(join(backupDir, ".backup.lock"), options?.shared === true);
|
|
168
240
|
if (token === 0) throw new Error(`Backup directory is locked: ${backupDir}`);
|
|
169
241
|
try {
|
|
170
242
|
return await fn();
|
|
@@ -173,6 +245,40 @@ async function withBackupDirLock(backupDir, fn) {
|
|
|
173
245
|
}
|
|
174
246
|
}
|
|
175
247
|
/**
|
|
248
|
+
* Throws if `backupDir` resolves to the database directory itself or any path
|
|
249
|
+
* beneath it. Backing up into the live database directory would write backup
|
|
250
|
+
* files (the `.backup.lock`, `meta/`, `shared/`, `private/` subtrees) on top of
|
|
251
|
+
* RocksDB's own files, and every subsequent backup would recursively capture
|
|
252
|
+
* the prior ones. Both paths are resolved first so trailing slashes and
|
|
253
|
+
* relative/absolute variants of the same location cannot slip past the check.
|
|
254
|
+
*
|
|
255
|
+
* Only the "backup dir at or under the database dir" direction is rejected —
|
|
256
|
+
* the case the user hits by pointing a backup at `<db>` or `<db>/backups`. The
|
|
257
|
+
* reverse (a database nested under the backup dir) is left to the caller: a
|
|
258
|
+
* backup engine writes into named subtrees, not on top of arbitrary sibling
|
|
259
|
+
* files, so it does not clobber the database in place.
|
|
260
|
+
*
|
|
261
|
+
* On case-insensitive filesystems `<db>` and `<db>/../DB/backup` are the same
|
|
262
|
+
* directory on disk, but `relative()` compares case-sensitively and would call
|
|
263
|
+
* the second one "outside". We case-fold both paths on the platforms whose
|
|
264
|
+
* default filesystem is case-insensitive (macOS, Windows) so a casing
|
|
265
|
+
* difference cannot slip a backup into the database directory. This is a
|
|
266
|
+
* platform heuristic, not a per-volume probe (a case-sensitive macOS/Windows
|
|
267
|
+
* volume exists), so it can over-reject a legitimately differently-cased
|
|
268
|
+
* sibling there — the safe direction for a guard against clobbering the live
|
|
269
|
+
* database.
|
|
270
|
+
*/
|
|
271
|
+
function assertBackupDirOutsideDatabase(dbPath, backupDir) {
|
|
272
|
+
let resolvedDb = resolve(dbPath);
|
|
273
|
+
let resolvedBackup = resolve(backupDir);
|
|
274
|
+
if (process.platform === "win32" || process.platform === "darwin") {
|
|
275
|
+
resolvedDb = resolvedDb.toLowerCase();
|
|
276
|
+
resolvedBackup = resolvedBackup.toLowerCase();
|
|
277
|
+
}
|
|
278
|
+
const rel = relative(resolvedDb, resolvedBackup);
|
|
279
|
+
if (!(rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))) throw new Error(`Backup directory must not be inside the database directory: ${backupDir} is at or within ${dbPath}`);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
176
282
|
* Removes `<backupDir>/transaction_logs/<id>` subtrees whose backup id no longer
|
|
177
283
|
* corresponds to a surviving backup. Called after a `purge` (which holds the
|
|
178
284
|
* backup-dir lock); a no-op when no transaction logs were backed up. Self-healing:
|
|
@@ -253,18 +359,32 @@ const backups = {
|
|
|
253
359
|
* Restores a database from a backup directory into a (closed) database
|
|
254
360
|
* directory. The default mode purges the destination directory, so it must
|
|
255
361
|
* not point at a live database.
|
|
362
|
+
*
|
|
363
|
+
* Holds the backup directory lock in shared mode for the duration: multiple
|
|
364
|
+
* restores can read concurrently, but a writer (`db.backup()`, `delete`,
|
|
365
|
+
* `purge`) rejects while a restore is in flight rather than deleting files
|
|
366
|
+
* out from under it — and a restore rejects while a writer holds the lock.
|
|
367
|
+
*
|
|
368
|
+
* The shared lock needs no write access to `backupDir`, so restoring from a
|
|
369
|
+
* read-only backup directory (immutable/WORM store, read-only-mounted volume)
|
|
370
|
+
* works: the lock is taken on an existing `.backup.lock` read-only, degrading
|
|
371
|
+
* to a no-op only when the media is read-only for every process (`EROFS`). A
|
|
372
|
+
* mere permission denial hard-fails instead — it doesn't prove a privileged
|
|
373
|
+
* writer isn't running.
|
|
256
374
|
*/
|
|
257
375
|
async restore(backupDir, dbDir, options) {
|
|
258
376
|
if (resolve(backupDir) === resolve(dbDir)) throw new Error("Backup directory and database directory must be different");
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
377
|
+
return withBackupDirLock(backupDir, async () => {
|
|
378
|
+
const walDir = options?.walDir ?? dbDir;
|
|
379
|
+
await mkdir(dbDir, { recursive: true });
|
|
380
|
+
if (walDir !== dbDir) await mkdir(walDir, { recursive: true });
|
|
381
|
+
await new Promise((resolve, reject) => nativeBackupRestore(resolve, reject, backupDir, dbDir, walDir, {
|
|
382
|
+
backupId: options?.backupId,
|
|
383
|
+
keepLogFiles: options?.keepLogFiles,
|
|
384
|
+
mode: options?.mode
|
|
385
|
+
}));
|
|
386
|
+
await restoreTransactionLogs(backupDir, dbDir, options?.backupId);
|
|
387
|
+
}, { shared: true });
|
|
268
388
|
},
|
|
269
389
|
/**
|
|
270
390
|
* Lists the non-corrupt backups in a backup directory, ordered by id.
|
|
@@ -301,9 +421,28 @@ const backups = {
|
|
|
301
421
|
/**
|
|
302
422
|
* Verifies a backup's file sizes, and optionally their checksums (which
|
|
303
423
|
* requires reading all backed-up data).
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
424
|
+
*
|
|
425
|
+
* When the backup was created with `transactionLogs: true`, the backup's
|
|
426
|
+
* transaction log snapshot (`<backupDir>/transaction_logs/<backupId>/`) is
|
|
427
|
+
* also validated — every log file's header and entry framing must be intact
|
|
428
|
+
* (snapshots are copied on committed entry boundaries, so even a torn tail
|
|
429
|
+
* is a verification failure). Set `verifyTransactionLogs: false` to skip.
|
|
430
|
+
*/
|
|
431
|
+
async verify(backupDir, backupId, options) {
|
|
432
|
+
await new Promise((resolve, reject) => nativeBackupVerify(resolve, reject, backupDir, backupId, options?.verifyWithChecksum ?? false));
|
|
433
|
+
if (options?.verifyTransactionLogs === false) return;
|
|
434
|
+
const logsDir = join(backupDir, TRANSACTION_LOGS_DIRNAME, String(backupId));
|
|
435
|
+
if (!await exists(logsDir)) return;
|
|
436
|
+
const failures = [];
|
|
437
|
+
for (const entry of await readdir(logsDir, { withFileTypes: true })) {
|
|
438
|
+
if (!entry.isDirectory()) continue;
|
|
439
|
+
const result = await validateTransactionLogStore(join(logsDir, entry.name), { strict: true });
|
|
440
|
+
if (!result.valid) {
|
|
441
|
+
const details = [...result.errors, ...result.files.flatMap((file) => file.errors.map((error) => `${file.file}: ${error}`))];
|
|
442
|
+
failures.push(`${entry.name}: ${details.join("; ")}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (failures.length > 0) throw new Error(`Backup ${backupId} transaction log verification failed: ${failures.join("\n")}`);
|
|
307
446
|
}
|
|
308
447
|
};
|
|
309
448
|
|
|
@@ -1073,13 +1212,50 @@ const ITERATOR_STATE = new Uint32Array(ITERATOR_STATE_BUFFER.buffer, ITERATOR_ST
|
|
|
1073
1212
|
const MAX_KEY_SIZE = 1024 * 1024;
|
|
1074
1213
|
const SAVE_BUFFER_SIZE = 8192;
|
|
1075
1214
|
/**
|
|
1215
|
+
* Normalizes the public `compression` option into the primitive fields the
|
|
1216
|
+
* native layer expects. When `option` is omitted, returns an empty object and
|
|
1217
|
+
* lets the native layer apply the default (LZ4 when the build supports it, else
|
|
1218
|
+
* RocksDB's own default). Throws a `TypeError` for a malformed option and an
|
|
1219
|
+
* `Error` for an algorithm that isn't supported by this build.
|
|
1220
|
+
*/
|
|
1221
|
+
function normalizeCompression(option) {
|
|
1222
|
+
if (option === void 0 || option === null) return {};
|
|
1223
|
+
let algorithm;
|
|
1224
|
+
let level;
|
|
1225
|
+
if (typeof option === "string") algorithm = option;
|
|
1226
|
+
else if (typeof option === "object" && option.algorithm !== void 0) {
|
|
1227
|
+
algorithm = option.algorithm;
|
|
1228
|
+
if (option.level !== void 0 && option.level !== null) {
|
|
1229
|
+
const raw = option.level;
|
|
1230
|
+
const coerced = typeof raw === "number" ? raw : typeof raw === "string" && raw.trim() !== "" ? Number(raw) : NaN;
|
|
1231
|
+
if (!Number.isInteger(coerced) || coerced < -2147483648 || coerced > 2147483647) throw new TypeError(`compression level must be a 32-bit integer, got ${JSON.stringify(option.level)}`);
|
|
1232
|
+
level = coerced;
|
|
1233
|
+
}
|
|
1234
|
+
} else throw new TypeError("compression must be an algorithm name or an { algorithm, level } object");
|
|
1235
|
+
if (!supportedCompression.includes(algorithm)) throw new Error(`Unsupported compression algorithm "${algorithm}". This build supports: ${supportedCompression.join(", ")}`);
|
|
1236
|
+
return {
|
|
1237
|
+
compression: algorithm,
|
|
1238
|
+
compressionLevel: level
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1076
1242
|
* A store wraps the `NativeDatabase` binding and database settings so that a
|
|
1077
1243
|
* single database instance can be shared between the main `RocksDatabase`
|
|
1078
1244
|
* instance and the `Transaction` instance.
|
|
1079
1245
|
*
|
|
1080
|
-
*
|
|
1246
|
+
* A store is bound to a single `RocksDatabase` instance and must not be shared
|
|
1247
|
+
* between `RocksDatabase` instances. Each `RocksDatabase` claims its store on
|
|
1248
|
+
* construction (see `claim()`); passing a store that another `RocksDatabase`
|
|
1249
|
+
* already owns throws. (Sharing with the owning database's `Transaction`
|
|
1250
|
+
* instances is expected and allowed.)
|
|
1081
1251
|
*/
|
|
1082
1252
|
var Store = class {
|
|
1253
|
+
/**
|
|
1254
|
+
* Whether a `RocksDatabase` has claimed this store. Durable across
|
|
1255
|
+
* close/reopen (unlike the handle's open state), so a store reused after its
|
|
1256
|
+
* owner temporarily closed is still recognized as in-use. See `claim()`.
|
|
1257
|
+
*/
|
|
1258
|
+
#claimed = false;
|
|
1083
1259
|
/**
|
|
1084
1260
|
* The database instance.
|
|
1085
1261
|
*/
|
|
@@ -1094,6 +1270,11 @@ var Store = class {
|
|
|
1094
1270
|
*/
|
|
1095
1271
|
decoderCopies = false;
|
|
1096
1272
|
/**
|
|
1273
|
+
* The compression algorithm (or `{ algorithm, level }`) requested for this
|
|
1274
|
+
* store's column family. Normalized and applied when the database opens.
|
|
1275
|
+
*/
|
|
1276
|
+
compression;
|
|
1277
|
+
/**
|
|
1097
1278
|
* Whether to disable the write ahead log.
|
|
1098
1279
|
*/
|
|
1099
1280
|
disableWAL;
|
|
@@ -1132,6 +1313,16 @@ var Store = class {
|
|
|
1132
1313
|
*/
|
|
1133
1314
|
maxKeySize;
|
|
1134
1315
|
/**
|
|
1316
|
+
* The maximum number of table files RocksDB keeps open. `0` (the default)
|
|
1317
|
+
* derives a budget from the effective per-process open-file limit (an
|
|
1318
|
+
* eighth of the limit — several databases can share one process — clamped
|
|
1319
|
+
* to `[1024, 262144]`); `-1` holds every table file open (the RocksDB
|
|
1320
|
+
* default, which can exhaust the process file-descriptor limit when
|
|
1321
|
+
* compaction falls behind under sustained ingest); a positive `int32` is
|
|
1322
|
+
* an explicit cap.
|
|
1323
|
+
*/
|
|
1324
|
+
maxOpenFiles;
|
|
1325
|
+
/**
|
|
1135
1326
|
* The maximum number of memtables that can be queued per column family
|
|
1136
1327
|
* before writes stall. Higher values absorb write bursts while flushes catch
|
|
1137
1328
|
* up, at the cost of memory.
|
|
@@ -1241,6 +1432,7 @@ var Store = class {
|
|
|
1241
1432
|
if (options !== void 0 && options !== null && typeof options !== "object") throw new TypeError("Database options must be an object");
|
|
1242
1433
|
const { keyEncoding, readKey, writeKey } = initKeyEncoder(options?.keyEncoding, options?.keyEncoder);
|
|
1243
1434
|
this.db = new NativeDatabase();
|
|
1435
|
+
this.compression = options?.compression;
|
|
1244
1436
|
this.dbWriteBufferSize = options?.dbWriteBufferSize;
|
|
1245
1437
|
this.decoder = options?.decoder ?? null;
|
|
1246
1438
|
this.disableWAL = options?.disableWAL ?? false;
|
|
@@ -1252,6 +1444,7 @@ var Store = class {
|
|
|
1252
1444
|
this.keyBuffer = KEY_BUFFER;
|
|
1253
1445
|
this.keyEncoding = keyEncoding;
|
|
1254
1446
|
this.maxKeySize = options?.maxKeySize ?? MAX_KEY_SIZE;
|
|
1447
|
+
this.maxOpenFiles = options?.maxOpenFiles;
|
|
1255
1448
|
this.maxWriteBufferNumber = options?.maxWriteBufferNumber;
|
|
1256
1449
|
this.maxWriteBufferSizeToMaintain = options?.maxWriteBufferSizeToMaintain;
|
|
1257
1450
|
this.name = options?.name ?? "default";
|
|
@@ -1279,6 +1472,16 @@ var Store = class {
|
|
|
1279
1472
|
this.db.close();
|
|
1280
1473
|
}
|
|
1281
1474
|
/**
|
|
1475
|
+
* Claims this store for a `RocksDatabase`. A store is bound to a single
|
|
1476
|
+
* database instance, so claiming a store that another `RocksDatabase` has
|
|
1477
|
+
* already claimed throws. The claim is durable across close/reopen, so a
|
|
1478
|
+
* store reused after its owner temporarily closed still throws.
|
|
1479
|
+
*/
|
|
1480
|
+
claim() {
|
|
1481
|
+
if (this.#claimed) throw new Error("Store is already in use by another RocksDatabase instance");
|
|
1482
|
+
this.#claimed = true;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1282
1485
|
* Compacts the entire key range of the database asynchronously.
|
|
1283
1486
|
* This triggers manual compaction which removes tombstones and reclaims space.
|
|
1284
1487
|
*
|
|
@@ -1303,6 +1506,7 @@ var Store = class {
|
|
|
1303
1506
|
}
|
|
1304
1507
|
async backup(target, options) {
|
|
1305
1508
|
if (typeof target !== "string" && typeof target?.getWriter === "function") return backupToStream(this.db, target, options);
|
|
1509
|
+
assertBackupDirOutsideDatabase(this.path, target);
|
|
1306
1510
|
return new Promise((resolve, reject) => this.db.backup(resolve, reject, target, options));
|
|
1307
1511
|
}
|
|
1308
1512
|
/**
|
|
@@ -1539,10 +1743,14 @@ var Store = class {
|
|
|
1539
1743
|
*/
|
|
1540
1744
|
open() {
|
|
1541
1745
|
if (this.db.opened) return true;
|
|
1746
|
+
const { compression, compressionLevel } = normalizeCompression(this.compression);
|
|
1542
1747
|
this.db.open(this.path, {
|
|
1748
|
+
compression,
|
|
1749
|
+
compressionLevel,
|
|
1543
1750
|
dbWriteBufferSize: this.dbWriteBufferSize,
|
|
1544
1751
|
disableWAL: this.disableWAL,
|
|
1545
1752
|
enableStats: this.enableStats,
|
|
1753
|
+
maxOpenFiles: this.maxOpenFiles,
|
|
1546
1754
|
maxWriteBufferNumber: this.maxWriteBufferNumber,
|
|
1547
1755
|
maxWriteBufferSizeToMaintain: this.maxWriteBufferSizeToMaintain,
|
|
1548
1756
|
mode: this.pessimistic ? "pessimistic" : "optimistic",
|
|
@@ -1661,8 +1869,18 @@ const RETRY_NOW = constants.RETRY_NOW_VALUE;
|
|
|
1661
1869
|
var TransactionAlreadyAbortedError = class extends Error {
|
|
1662
1870
|
code = "ERR_ALREADY_ABORTED";
|
|
1663
1871
|
};
|
|
1664
|
-
|
|
1665
|
-
|
|
1872
|
+
/**
|
|
1873
|
+
* Base for commit conflicts the native layer has already recovered from by resetting the
|
|
1874
|
+
* transaction in place (fresh RocksDB snapshot; `committedPosition` preserved, so a staged
|
|
1875
|
+
* transaction-log batch stays write-once): the caller should re-run the transaction body
|
|
1876
|
+
* and retry the commit — the handling is identical for every subclass. `hasLog` reports
|
|
1877
|
+
* whether the transaction wrote to a transaction log (drives the default retry decision).
|
|
1878
|
+
*
|
|
1879
|
+
* {@link TransactionAbandonedError} shares this field shape but deliberately does NOT
|
|
1880
|
+
* extend this class — an abandoned transaction is not retryable, and retryability is the
|
|
1881
|
+
* boundary this hierarchy expresses.
|
|
1882
|
+
*/
|
|
1883
|
+
var TransactionRetryableError = class extends Error {
|
|
1666
1884
|
hasLog;
|
|
1667
1885
|
txn;
|
|
1668
1886
|
constructor(error, txn) {
|
|
@@ -1671,6 +1889,21 @@ var TransactionIsBusyError = class extends Error {
|
|
|
1671
1889
|
this.txn = txn;
|
|
1672
1890
|
}
|
|
1673
1891
|
};
|
|
1892
|
+
/**
|
|
1893
|
+
* An optimistic commit detected a write conflict with a concurrently committed
|
|
1894
|
+
* transaction (RocksDB `Busy`).
|
|
1895
|
+
*/
|
|
1896
|
+
var TransactionIsBusyError = class extends TransactionRetryableError {
|
|
1897
|
+
code = "ERR_BUSY";
|
|
1898
|
+
};
|
|
1899
|
+
/**
|
|
1900
|
+
* An optimistic commit could not be validated: the transaction's snapshot was stranded
|
|
1901
|
+
* outside the memtable window (its sequence history was flushed away — RocksDB
|
|
1902
|
+
* `TryAgain`), so the conflict check had nothing to validate against.
|
|
1903
|
+
*/
|
|
1904
|
+
var TransactionTryAgainError = class extends TransactionRetryableError {
|
|
1905
|
+
code = "ERR_TRY_AGAIN";
|
|
1906
|
+
};
|
|
1674
1907
|
var TransactionAbandonedError = class extends Error {
|
|
1675
1908
|
code = "ERR_TRANSACTION_ABANDONED";
|
|
1676
1909
|
hasLog;
|
|
@@ -1765,6 +1998,7 @@ var Transaction = class extends DBI {
|
|
|
1765
1998
|
if (err instanceof Error && "code" in err) {
|
|
1766
1999
|
if (err.code === "ERR_ALREADY_ABORTED") return new TransactionAlreadyAbortedError(err.message);
|
|
1767
2000
|
if (err.code === "ERR_BUSY") return new TransactionIsBusyError(err, this);
|
|
2001
|
+
if (err.code === "ERR_TRY_AGAIN") return new TransactionTryAgainError(err, this);
|
|
1768
2002
|
}
|
|
1769
2003
|
return err;
|
|
1770
2004
|
}
|
|
@@ -1814,9 +2048,12 @@ var RocksDatabase = class RocksDatabase extends DBI {
|
|
|
1814
2048
|
*/
|
|
1815
2049
|
#name;
|
|
1816
2050
|
constructor(pathOrStore, options) {
|
|
1817
|
-
|
|
1818
|
-
|
|
2051
|
+
let store;
|
|
2052
|
+
if (typeof pathOrStore === "string") store = new Store(pathOrStore, options);
|
|
2053
|
+
else if (pathOrStore instanceof Store) store = pathOrStore;
|
|
1819
2054
|
else throw new TypeError("Invalid database path or store");
|
|
2055
|
+
store.claim();
|
|
2056
|
+
super(store);
|
|
1820
2057
|
this.#name = options?.name ?? "default";
|
|
1821
2058
|
}
|
|
1822
2059
|
/**
|
|
@@ -1930,6 +2167,22 @@ var RocksDatabase = class RocksDatabase extends DBI {
|
|
|
1930
2167
|
return this.store.db.columns;
|
|
1931
2168
|
}
|
|
1932
2169
|
/**
|
|
2170
|
+
* The compression currently in effect for this database's column family, read
|
|
2171
|
+
* live from RocksDB, as `{ algorithm, level? }`. `algorithm` is a friendly
|
|
2172
|
+
* name (e.g. `'lz4'`, `'zstd'`, `'none'`); `level` is present only when a
|
|
2173
|
+
* non-default compression level is set. The database must be open. Defaults to
|
|
2174
|
+
* LZ4 when the native build supports it.
|
|
2175
|
+
*
|
|
2176
|
+
* @example
|
|
2177
|
+
* ```typescript
|
|
2178
|
+
* const db = RocksDatabase.open('/path/to/db', { compression: { algorithm: 'zstd', level: 3 } });
|
|
2179
|
+
* db.compression; // { algorithm: 'zstd', level: 3 }
|
|
2180
|
+
* ```
|
|
2181
|
+
*/
|
|
2182
|
+
get compression() {
|
|
2183
|
+
return this.store.db.getCompression();
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
1933
2186
|
* Set global database settings.
|
|
1934
2187
|
*
|
|
1935
2188
|
* @param options - The options for the database.
|
|
@@ -2345,7 +2598,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
|
|
|
2345
2598
|
commitResult = await txn.commit();
|
|
2346
2599
|
} catch (commitErr) {
|
|
2347
2600
|
if (commitErr instanceof TransactionAlreadyAbortedError) return;
|
|
2348
|
-
if (commitErr instanceof
|
|
2601
|
+
if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? commitErr.hasLog) && attempt < maxRetries) continue;
|
|
2349
2602
|
this.#abandonTransaction(txn, commitErr);
|
|
2350
2603
|
return;
|
|
2351
2604
|
}
|
|
@@ -2377,7 +2630,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
|
|
|
2377
2630
|
if (typeof callback !== "function") throw new TypeError("Callback must be a function");
|
|
2378
2631
|
const maxRetries = options?.maxRetries ?? 3;
|
|
2379
2632
|
const isRetryable = (err, attempt) => {
|
|
2380
|
-
return err instanceof
|
|
2633
|
+
return err instanceof TransactionRetryableError && (options?.retryOnBusy ?? err.hasLog) && attempt <= maxRetries;
|
|
2381
2634
|
};
|
|
2382
2635
|
const runAttempt = (attempt) => {
|
|
2383
2636
|
const txn = new Transaction(this.store, options);
|
|
@@ -2573,7 +2826,7 @@ function parseTransactionLog(path, options = {}) {
|
|
|
2573
2826
|
|
|
2574
2827
|
//#endregion
|
|
2575
2828
|
//#region src/transaction-log-reader.ts
|
|
2576
|
-
const FLOAT_TO_UINT32 = new Float64Array(1);
|
|
2829
|
+
const FLOAT_TO_UINT32 = /* @__PURE__ */ new Float64Array(1);
|
|
2577
2830
|
const UINT32_FROM_FLOAT = new Uint32Array(FLOAT_TO_UINT32.buffer);
|
|
2578
2831
|
const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE } = constants;
|
|
2579
2832
|
/**
|
|
@@ -2759,9 +3012,9 @@ function loadLastPosition(transactionLog, readUncommitted) {
|
|
|
2759
3012
|
//#region src/index.ts
|
|
2760
3013
|
const versions = {
|
|
2761
3014
|
rocksdb: version,
|
|
2762
|
-
"rocksdb-js": "2.
|
|
3015
|
+
"rocksdb-js": "2.6.0"
|
|
2763
3016
|
};
|
|
2764
3017
|
|
|
2765
3018
|
//#endregion
|
|
2766
|
-
export { DBI, DBIterator, RocksDatabase, Store, Transaction, TransactionLog, backups, constants, coolTransactionLogs, currentThreadId, fileLockRelease, parseTransactionLog, registryStatus, shutdown, stats, tryFileLock, versions };
|
|
3019
|
+
export { DBI, DBIterator, RocksDatabase, Store, Transaction, TransactionLog, backups, constants, coolTransactionLogs, currentThreadId, fileLockRelease, parseTransactionLog, registryStatus, shutdown, stats, supportedCompression, tryFileLock, validateTransactionLogStore, versions };
|
|
2767
3020
|
//# sourceMappingURL=index.mjs.map
|