@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/dist/index.cjs CHANGED
@@ -87,6 +87,15 @@ const removeGlobalListener = binding.removeListener;
87
87
  const globalListenerCount = binding.listenerCount;
88
88
  const globalNotify = binding.notify;
89
89
  const constants = binding.constants;
90
+ /**
91
+ * The friendly names of every compression algorithm compiled into the loaded
92
+ * RocksDB native binding, e.g. `['none', 'snappy', 'lz4', 'zstd']`. The set is
93
+ * fixed for a given binary (it depends on which compression libraries RocksDB
94
+ * was linked against), so this is a static list. `'none'` (no compression) is
95
+ * always present. Use it to validate a `compression` option or to pick an
96
+ * algorithm that is actually available at runtime.
97
+ */
98
+ const supportedCompression = Object.freeze(binding.supportedCompression);
90
99
  const NativeDatabase = binding.Database;
91
100
  const NativeIterator = binding.Iterator;
92
101
  const NativeTransaction = binding.Transaction;
@@ -115,13 +124,21 @@ const coolTransactionLogs = binding.coolTransactionLogs;
115
124
  */
116
125
  const transactionLogMapCount = binding.transactionLogMapCount;
117
126
  /**
127
+ * Test-only: force the next `count` transaction commits to fail with TryAgain (rolled back, so
128
+ * no data is committed), reproducing a stranded-snapshot conflict deterministically. Pass 0 to
129
+ * disarm. Used by the ERR_TRY_AGAIN retry regression test.
130
+ */
131
+ const forceTryAgainForTesting = binding.forceTryAgainForTesting;
132
+ /**
118
133
  * Creates a native file lock using the specified file path (`flock` on POSIX,
119
- * `LockFileEx` on Windows). Returns an opaque non-zero token to pass to
120
- * `fileLockRelease`, or `0` if another holder — in any process, container, or
121
- * worker thread — currently has it. Throws if `file` is missing or on a hard
122
- * error. The OS handle is owned entirely in native code (no fd crosses into
123
- * JS), and the kernel releases the lock when the handle closes, including on
124
- * process death.
134
+ * `LockFileEx` on Windows), creating the file and any missing parent
135
+ * directories. Exclusive by default; pass `shared` for a shared (reader) lock
136
+ * that coexists with other shared holders but conflicts with an exclusive
137
+ * holder in either direction. Returns an opaque non-zero token to pass to
138
+ * `fileLockRelease`, or `0` if a conflicting holder — in any process,
139
+ * container, or worker thread — currently has it. Throws on a hard error. The
140
+ * OS handle is owned entirely in native code (no fd crosses into JS), and the
141
+ * kernel releases the lock when the handle closes, including on process death.
125
142
  */
126
143
  const tryFileLock = binding.tryFileLock;
127
144
  /**
@@ -134,9 +151,51 @@ const nativeBackupList = binding.backupList;
134
151
  const nativeBackupDelete = binding.backupDelete;
135
152
  const nativeBackupPurge = binding.backupPurge;
136
153
  const nativeBackupVerify = binding.backupVerify;
154
+ const nativeValidateTransactionLog = binding.validateTransactionLog;
137
155
  const stats = binding.stats;
138
156
  const version = binding.version;
139
157
 
158
+ //#endregion
159
+ //#region src/validate-transaction-log.ts
160
+ /**
161
+ * Validates a transaction log store directory: every `<sequence>.txnlog` file
162
+ * is checked for a valid header (token, version) and intact v1 entry framing
163
+ * (using the same scan as open-time crash recovery), file-name and sequence
164
+ * continuity are checked, and the `txn.state` side file (when present) is
165
+ * checked for shape and a plausible flushed position. The result reports
166
+ * per-file entry counts, errors, and warnings; it does not throw when the
167
+ * store is invalid — check `result.valid`.
168
+ *
169
+ * Runs entirely in native code on a worker thread and does not require (or
170
+ * open) a database, so it works on the store of a closed database and on the
171
+ * per-backup snapshots under `<backupDir>/transaction_logs/<backupId>/` alike.
172
+ *
173
+ * Validating a store that is being actively appended to can spuriously report
174
+ * a torn tail for the current log file — the tail of an in-flight append is
175
+ * indistinguishable from a crash artifact.
176
+ *
177
+ * Rejects if `path` does not exist or is not a directory (a bad argument, as
178
+ * opposed to an invalid store).
179
+ *
180
+ * @param path The store directory, e.g. `<dbDir>/transaction_logs/<name>`.
181
+ * @param options Validation options.
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * import { validateTransactionLogStore } from '@harperfast/rocksdb-js';
186
+ *
187
+ * const result = await validateTransactionLogStore('/path/to/db/transaction_logs/mylog');
188
+ * if (!result.valid) {
189
+ * for (const file of result.files) {
190
+ * console.error(file.file, file.errors);
191
+ * }
192
+ * }
193
+ * ```
194
+ */
195
+ function validateTransactionLogStore(path, options) {
196
+ return new Promise((resolve, reject) => nativeValidateTransactionLog(resolve, reject, path, options?.strict ?? false));
197
+ }
198
+
140
199
  //#endregion
141
200
  //#region src/backup.ts
142
201
  /** Subdirectory (under a backup directory) holding per-backup transaction log snapshots. */
@@ -152,17 +211,29 @@ async function exists(path) {
152
211
  }
153
212
  /**
154
213
  * Runs `fn` while holding a native file lock for `backupDir`, releasing it when
155
- * `fn` settles. Used by the writable-engine operations `backups.delete` and
156
- * `backups.purge`; backup creation (`Store.backup`) takes the same lock on the
157
- * same file natively inside `Database::Backup` (see `runCreateBackup` in
214
+ * `fn` settles. The lock is exclusive by default — used by the writable-engine
215
+ * operations `backups.delete` and `backups.purge`; backup creation
216
+ * (`Store.backup`) takes the same exclusive lock on the same file natively
217
+ * inside `Database::Backup` (see `runCreateBackup` in
158
218
  * `src/binding/database/backup.cpp`), where the backup directory is also
159
- * created. Throws immediately (without running `fn`) if another writer holds
160
- * the directory lock.
219
+ * created. Throws immediately (without running `fn`) if the backup directory
220
+ * is missing — `tryFileLock` creates missing parents, but `delete`/`purge`/
221
+ * `restore` on a nonexistent directory is a caller error and must not conjure
222
+ * an empty one — or if a conflicting holder has the directory lock.
161
223
  *
162
- * Read-only operations (`list`, `verify`, and a restore's source read) use
163
- * `BackupEngineReadOnly` and are not locked: concurrent readers are safe.
164
- * Running a reader concurrently with a `delete`/`purge` on the same directory is
165
- * a caller-managed hazard, matching RocksDB's one-writable-engine-per-dir model.
224
+ * `backups.restore` holds the lock in `shared` mode for its source read:
225
+ * shared holders coexist (concurrent restores are safe), but a writer's
226
+ * exclusive acquisition rejects while a restore is in flight — and vice versa.
227
+ * Without this, a `purge`/`delete` racing a restore can delete the very files
228
+ * the restore is copying, after the restore's default `purgeAllFiles` mode has
229
+ * already wiped the destination — failing the restore with no fallback. A
230
+ * rejected writer, by contrast, just retries later.
231
+ *
232
+ * The remaining read-only operations (`list`, `verify`) use
233
+ * `BackupEngineReadOnly` and are not locked: concurrent readers are safe, and
234
+ * locking them would make cheap listings reject during a long backup. Running
235
+ * them concurrently with a `delete`/`purge` on the same directory is a
236
+ * caller-managed hazard, matching RocksDB's one-writable-engine-per-dir model.
166
237
  *
167
238
  * RocksDB only serializes work within a single `BackupEngine` instance and has
168
239
  * no lock on the backup directory itself. Two writable engines — in any
@@ -188,8 +259,9 @@ async function exists(path) {
188
259
  * The lock file is never deleted, only locked and unlocked; an unlocked, empty
189
260
  * `.backup.lock` at the directory root is the steady state.
190
261
  */
191
- async function withBackupDirLock(backupDir, fn) {
192
- const token = tryFileLock((0, node_path.join)(backupDir, ".backup.lock"));
262
+ async function withBackupDirLock(backupDir, fn, options) {
263
+ if (!await exists(backupDir)) throw new Error(`Backup directory does not exist: ${backupDir}`);
264
+ const token = tryFileLock((0, node_path.join)(backupDir, ".backup.lock"), options?.shared === true);
193
265
  if (token === 0) throw new Error(`Backup directory is locked: ${backupDir}`);
194
266
  try {
195
267
  return await fn();
@@ -198,6 +270,40 @@ async function withBackupDirLock(backupDir, fn) {
198
270
  }
199
271
  }
200
272
  /**
273
+ * Throws if `backupDir` resolves to the database directory itself or any path
274
+ * beneath it. Backing up into the live database directory would write backup
275
+ * files (the `.backup.lock`, `meta/`, `shared/`, `private/` subtrees) on top of
276
+ * RocksDB's own files, and every subsequent backup would recursively capture
277
+ * the prior ones. Both paths are resolved first so trailing slashes and
278
+ * relative/absolute variants of the same location cannot slip past the check.
279
+ *
280
+ * Only the "backup dir at or under the database dir" direction is rejected —
281
+ * the case the user hits by pointing a backup at `<db>` or `<db>/backups`. The
282
+ * reverse (a database nested under the backup dir) is left to the caller: a
283
+ * backup engine writes into named subtrees, not on top of arbitrary sibling
284
+ * files, so it does not clobber the database in place.
285
+ *
286
+ * On case-insensitive filesystems `<db>` and `<db>/../DB/backup` are the same
287
+ * directory on disk, but `relative()` compares case-sensitively and would call
288
+ * the second one "outside". We case-fold both paths on the platforms whose
289
+ * default filesystem is case-insensitive (macOS, Windows) so a casing
290
+ * difference cannot slip a backup into the database directory. This is a
291
+ * platform heuristic, not a per-volume probe (a case-sensitive macOS/Windows
292
+ * volume exists), so it can over-reject a legitimately differently-cased
293
+ * sibling there — the safe direction for a guard against clobbering the live
294
+ * database.
295
+ */
296
+ function assertBackupDirOutsideDatabase(dbPath, backupDir) {
297
+ let resolvedDb = (0, node_path.resolve)(dbPath);
298
+ let resolvedBackup = (0, node_path.resolve)(backupDir);
299
+ if (process.platform === "win32" || process.platform === "darwin") {
300
+ resolvedDb = resolvedDb.toLowerCase();
301
+ resolvedBackup = resolvedBackup.toLowerCase();
302
+ }
303
+ const rel = (0, node_path.relative)(resolvedDb, resolvedBackup);
304
+ if (!(rel === ".." || rel.startsWith(`..${node_path.sep}`) || (0, node_path.isAbsolute)(rel))) throw new Error(`Backup directory must not be inside the database directory: ${backupDir} is at or within ${dbPath}`);
305
+ }
306
+ /**
201
307
  * Removes `<backupDir>/transaction_logs/<id>` subtrees whose backup id no longer
202
308
  * corresponds to a surviving backup. Called after a `purge` (which holds the
203
309
  * backup-dir lock); a no-op when no transaction logs were backed up. Self-healing:
@@ -278,18 +384,32 @@ const backups = {
278
384
  * Restores a database from a backup directory into a (closed) database
279
385
  * directory. The default mode purges the destination directory, so it must
280
386
  * not point at a live database.
387
+ *
388
+ * Holds the backup directory lock in shared mode for the duration: multiple
389
+ * restores can read concurrently, but a writer (`db.backup()`, `delete`,
390
+ * `purge`) rejects while a restore is in flight rather than deleting files
391
+ * out from under it — and a restore rejects while a writer holds the lock.
392
+ *
393
+ * The shared lock needs no write access to `backupDir`, so restoring from a
394
+ * read-only backup directory (immutable/WORM store, read-only-mounted volume)
395
+ * works: the lock is taken on an existing `.backup.lock` read-only, degrading
396
+ * to a no-op only when the media is read-only for every process (`EROFS`). A
397
+ * mere permission denial hard-fails instead — it doesn't prove a privileged
398
+ * writer isn't running.
281
399
  */
282
400
  async restore(backupDir, dbDir, options) {
283
401
  if ((0, node_path.resolve)(backupDir) === (0, node_path.resolve)(dbDir)) throw new Error("Backup directory and database directory must be different");
284
- const walDir = options?.walDir ?? dbDir;
285
- await (0, node_fs_promises.mkdir)(dbDir, { recursive: true });
286
- if (walDir !== dbDir) await (0, node_fs_promises.mkdir)(walDir, { recursive: true });
287
- await new Promise((resolve, reject) => nativeBackupRestore(resolve, reject, backupDir, dbDir, walDir, {
288
- backupId: options?.backupId,
289
- keepLogFiles: options?.keepLogFiles,
290
- mode: options?.mode
291
- }));
292
- await restoreTransactionLogs(backupDir, dbDir, options?.backupId);
402
+ return withBackupDirLock(backupDir, async () => {
403
+ const walDir = options?.walDir ?? dbDir;
404
+ await (0, node_fs_promises.mkdir)(dbDir, { recursive: true });
405
+ if (walDir !== dbDir) await (0, node_fs_promises.mkdir)(walDir, { recursive: true });
406
+ await new Promise((resolve, reject) => nativeBackupRestore(resolve, reject, backupDir, dbDir, walDir, {
407
+ backupId: options?.backupId,
408
+ keepLogFiles: options?.keepLogFiles,
409
+ mode: options?.mode
410
+ }));
411
+ await restoreTransactionLogs(backupDir, dbDir, options?.backupId);
412
+ }, { shared: true });
293
413
  },
294
414
  /**
295
415
  * Lists the non-corrupt backups in a backup directory, ordered by id.
@@ -326,9 +446,28 @@ const backups = {
326
446
  /**
327
447
  * Verifies a backup's file sizes, and optionally their checksums (which
328
448
  * requires reading all backed-up data).
329
- */
330
- verify(backupDir, backupId, options) {
331
- return new Promise((resolve, reject) => nativeBackupVerify(resolve, reject, backupDir, backupId, options?.verifyWithChecksum ?? false));
449
+ *
450
+ * When the backup was created with `transactionLogs: true`, the backup's
451
+ * transaction log snapshot (`<backupDir>/transaction_logs/<backupId>/`) is
452
+ * also validated — every log file's header and entry framing must be intact
453
+ * (snapshots are copied on committed entry boundaries, so even a torn tail
454
+ * is a verification failure). Set `verifyTransactionLogs: false` to skip.
455
+ */
456
+ async verify(backupDir, backupId, options) {
457
+ await new Promise((resolve, reject) => nativeBackupVerify(resolve, reject, backupDir, backupId, options?.verifyWithChecksum ?? false));
458
+ if (options?.verifyTransactionLogs === false) return;
459
+ const logsDir = (0, node_path.join)(backupDir, TRANSACTION_LOGS_DIRNAME, String(backupId));
460
+ if (!await exists(logsDir)) return;
461
+ const failures = [];
462
+ for (const entry of await (0, node_fs_promises.readdir)(logsDir, { withFileTypes: true })) {
463
+ if (!entry.isDirectory()) continue;
464
+ const result = await validateTransactionLogStore((0, node_path.join)(logsDir, entry.name), { strict: true });
465
+ if (!result.valid) {
466
+ const details = [...result.errors, ...result.files.flatMap((file) => file.errors.map((error) => `${file.file}: ${error}`))];
467
+ failures.push(`${entry.name}: ${details.join("; ")}`);
468
+ }
469
+ }
470
+ if (failures.length > 0) throw new Error(`Backup ${backupId} transaction log verification failed: ${failures.join("\n")}`);
332
471
  }
333
472
  };
334
473
 
@@ -1098,13 +1237,50 @@ const ITERATOR_STATE = new Uint32Array(ITERATOR_STATE_BUFFER.buffer, ITERATOR_ST
1098
1237
  const MAX_KEY_SIZE = 1024 * 1024;
1099
1238
  const SAVE_BUFFER_SIZE = 8192;
1100
1239
  /**
1240
+ * Normalizes the public `compression` option into the primitive fields the
1241
+ * native layer expects. When `option` is omitted, returns an empty object and
1242
+ * lets the native layer apply the default (LZ4 when the build supports it, else
1243
+ * RocksDB's own default). Throws a `TypeError` for a malformed option and an
1244
+ * `Error` for an algorithm that isn't supported by this build.
1245
+ */
1246
+ function normalizeCompression(option) {
1247
+ if (option === void 0 || option === null) return {};
1248
+ let algorithm;
1249
+ let level;
1250
+ if (typeof option === "string") algorithm = option;
1251
+ else if (typeof option === "object" && option.algorithm !== void 0) {
1252
+ algorithm = option.algorithm;
1253
+ if (option.level !== void 0 && option.level !== null) {
1254
+ const raw = option.level;
1255
+ const coerced = typeof raw === "number" ? raw : typeof raw === "string" && raw.trim() !== "" ? Number(raw) : NaN;
1256
+ if (!Number.isInteger(coerced) || coerced < -2147483648 || coerced > 2147483647) throw new TypeError(`compression level must be a 32-bit integer, got ${JSON.stringify(option.level)}`);
1257
+ level = coerced;
1258
+ }
1259
+ } else throw new TypeError("compression must be an algorithm name or an { algorithm, level } object");
1260
+ if (!supportedCompression.includes(algorithm)) throw new Error(`Unsupported compression algorithm "${algorithm}". This build supports: ${supportedCompression.join(", ")}`);
1261
+ return {
1262
+ compression: algorithm,
1263
+ compressionLevel: level
1264
+ };
1265
+ }
1266
+ /**
1101
1267
  * A store wraps the `NativeDatabase` binding and database settings so that a
1102
1268
  * single database instance can be shared between the main `RocksDatabase`
1103
1269
  * instance and the `Transaction` instance.
1104
1270
  *
1105
- * This store should not be shared between `RocksDatabase` instances.
1271
+ * A store is bound to a single `RocksDatabase` instance and must not be shared
1272
+ * between `RocksDatabase` instances. Each `RocksDatabase` claims its store on
1273
+ * construction (see `claim()`); passing a store that another `RocksDatabase`
1274
+ * already owns throws. (Sharing with the owning database's `Transaction`
1275
+ * instances is expected and allowed.)
1106
1276
  */
1107
1277
  var Store = class {
1278
+ /**
1279
+ * Whether a `RocksDatabase` has claimed this store. Durable across
1280
+ * close/reopen (unlike the handle's open state), so a store reused after its
1281
+ * owner temporarily closed is still recognized as in-use. See `claim()`.
1282
+ */
1283
+ #claimed = false;
1108
1284
  /**
1109
1285
  * The database instance.
1110
1286
  */
@@ -1119,6 +1295,11 @@ var Store = class {
1119
1295
  */
1120
1296
  decoderCopies = false;
1121
1297
  /**
1298
+ * The compression algorithm (or `{ algorithm, level }`) requested for this
1299
+ * store's column family. Normalized and applied when the database opens.
1300
+ */
1301
+ compression;
1302
+ /**
1122
1303
  * Whether to disable the write ahead log.
1123
1304
  */
1124
1305
  disableWAL;
@@ -1157,6 +1338,16 @@ var Store = class {
1157
1338
  */
1158
1339
  maxKeySize;
1159
1340
  /**
1341
+ * The maximum number of table files RocksDB keeps open. `0` (the default)
1342
+ * derives a budget from the effective per-process open-file limit (an
1343
+ * eighth of the limit — several databases can share one process — clamped
1344
+ * to `[1024, 262144]`); `-1` holds every table file open (the RocksDB
1345
+ * default, which can exhaust the process file-descriptor limit when
1346
+ * compaction falls behind under sustained ingest); a positive `int32` is
1347
+ * an explicit cap.
1348
+ */
1349
+ maxOpenFiles;
1350
+ /**
1160
1351
  * The maximum number of memtables that can be queued per column family
1161
1352
  * before writes stall. Higher values absorb write bursts while flushes catch
1162
1353
  * up, at the cost of memory.
@@ -1266,6 +1457,7 @@ var Store = class {
1266
1457
  if (options !== void 0 && options !== null && typeof options !== "object") throw new TypeError("Database options must be an object");
1267
1458
  const { keyEncoding, readKey, writeKey } = initKeyEncoder(options?.keyEncoding, options?.keyEncoder);
1268
1459
  this.db = new NativeDatabase();
1460
+ this.compression = options?.compression;
1269
1461
  this.dbWriteBufferSize = options?.dbWriteBufferSize;
1270
1462
  this.decoder = options?.decoder ?? null;
1271
1463
  this.disableWAL = options?.disableWAL ?? false;
@@ -1277,6 +1469,7 @@ var Store = class {
1277
1469
  this.keyBuffer = KEY_BUFFER;
1278
1470
  this.keyEncoding = keyEncoding;
1279
1471
  this.maxKeySize = options?.maxKeySize ?? MAX_KEY_SIZE;
1472
+ this.maxOpenFiles = options?.maxOpenFiles;
1280
1473
  this.maxWriteBufferNumber = options?.maxWriteBufferNumber;
1281
1474
  this.maxWriteBufferSizeToMaintain = options?.maxWriteBufferSizeToMaintain;
1282
1475
  this.name = options?.name ?? "default";
@@ -1304,6 +1497,16 @@ var Store = class {
1304
1497
  this.db.close();
1305
1498
  }
1306
1499
  /**
1500
+ * Claims this store for a `RocksDatabase`. A store is bound to a single
1501
+ * database instance, so claiming a store that another `RocksDatabase` has
1502
+ * already claimed throws. The claim is durable across close/reopen, so a
1503
+ * store reused after its owner temporarily closed still throws.
1504
+ */
1505
+ claim() {
1506
+ if (this.#claimed) throw new Error("Store is already in use by another RocksDatabase instance");
1507
+ this.#claimed = true;
1508
+ }
1509
+ /**
1307
1510
  * Compacts the entire key range of the database asynchronously.
1308
1511
  * This triggers manual compaction which removes tombstones and reclaims space.
1309
1512
  *
@@ -1328,6 +1531,7 @@ var Store = class {
1328
1531
  }
1329
1532
  async backup(target, options) {
1330
1533
  if (typeof target !== "string" && typeof target?.getWriter === "function") return backupToStream(this.db, target, options);
1534
+ assertBackupDirOutsideDatabase(this.path, target);
1331
1535
  return new Promise((resolve, reject) => this.db.backup(resolve, reject, target, options));
1332
1536
  }
1333
1537
  /**
@@ -1564,10 +1768,14 @@ var Store = class {
1564
1768
  */
1565
1769
  open() {
1566
1770
  if (this.db.opened) return true;
1771
+ const { compression, compressionLevel } = normalizeCompression(this.compression);
1567
1772
  this.db.open(this.path, {
1773
+ compression,
1774
+ compressionLevel,
1568
1775
  dbWriteBufferSize: this.dbWriteBufferSize,
1569
1776
  disableWAL: this.disableWAL,
1570
1777
  enableStats: this.enableStats,
1778
+ maxOpenFiles: this.maxOpenFiles,
1571
1779
  maxWriteBufferNumber: this.maxWriteBufferNumber,
1572
1780
  maxWriteBufferSizeToMaintain: this.maxWriteBufferSizeToMaintain,
1573
1781
  mode: this.pessimistic ? "pessimistic" : "optimistic",
@@ -1686,8 +1894,18 @@ const RETRY_NOW = constants.RETRY_NOW_VALUE;
1686
1894
  var TransactionAlreadyAbortedError = class extends Error {
1687
1895
  code = "ERR_ALREADY_ABORTED";
1688
1896
  };
1689
- var TransactionIsBusyError = class extends Error {
1690
- code = "ERR_BUSY";
1897
+ /**
1898
+ * Base for commit conflicts the native layer has already recovered from by resetting the
1899
+ * transaction in place (fresh RocksDB snapshot; `committedPosition` preserved, so a staged
1900
+ * transaction-log batch stays write-once): the caller should re-run the transaction body
1901
+ * and retry the commit — the handling is identical for every subclass. `hasLog` reports
1902
+ * whether the transaction wrote to a transaction log (drives the default retry decision).
1903
+ *
1904
+ * {@link TransactionAbandonedError} shares this field shape but deliberately does NOT
1905
+ * extend this class — an abandoned transaction is not retryable, and retryability is the
1906
+ * boundary this hierarchy expresses.
1907
+ */
1908
+ var TransactionRetryableError = class extends Error {
1691
1909
  hasLog;
1692
1910
  txn;
1693
1911
  constructor(error, txn) {
@@ -1696,6 +1914,21 @@ var TransactionIsBusyError = class extends Error {
1696
1914
  this.txn = txn;
1697
1915
  }
1698
1916
  };
1917
+ /**
1918
+ * An optimistic commit detected a write conflict with a concurrently committed
1919
+ * transaction (RocksDB `Busy`).
1920
+ */
1921
+ var TransactionIsBusyError = class extends TransactionRetryableError {
1922
+ code = "ERR_BUSY";
1923
+ };
1924
+ /**
1925
+ * An optimistic commit could not be validated: the transaction's snapshot was stranded
1926
+ * outside the memtable window (its sequence history was flushed away — RocksDB
1927
+ * `TryAgain`), so the conflict check had nothing to validate against.
1928
+ */
1929
+ var TransactionTryAgainError = class extends TransactionRetryableError {
1930
+ code = "ERR_TRY_AGAIN";
1931
+ };
1699
1932
  var TransactionAbandonedError = class extends Error {
1700
1933
  code = "ERR_TRANSACTION_ABANDONED";
1701
1934
  hasLog;
@@ -1790,6 +2023,7 @@ var Transaction = class extends DBI {
1790
2023
  if (err instanceof Error && "code" in err) {
1791
2024
  if (err.code === "ERR_ALREADY_ABORTED") return new TransactionAlreadyAbortedError(err.message);
1792
2025
  if (err.code === "ERR_BUSY") return new TransactionIsBusyError(err, this);
2026
+ if (err.code === "ERR_TRY_AGAIN") return new TransactionTryAgainError(err, this);
1793
2027
  }
1794
2028
  return err;
1795
2029
  }
@@ -1839,9 +2073,12 @@ var RocksDatabase = class RocksDatabase extends DBI {
1839
2073
  */
1840
2074
  #name;
1841
2075
  constructor(pathOrStore, options) {
1842
- if (typeof pathOrStore === "string") super(new Store(pathOrStore, options));
1843
- else if (pathOrStore instanceof Store) super(pathOrStore);
2076
+ let store;
2077
+ if (typeof pathOrStore === "string") store = new Store(pathOrStore, options);
2078
+ else if (pathOrStore instanceof Store) store = pathOrStore;
1844
2079
  else throw new TypeError("Invalid database path or store");
2080
+ store.claim();
2081
+ super(store);
1845
2082
  this.#name = options?.name ?? "default";
1846
2083
  }
1847
2084
  /**
@@ -1955,6 +2192,22 @@ var RocksDatabase = class RocksDatabase extends DBI {
1955
2192
  return this.store.db.columns;
1956
2193
  }
1957
2194
  /**
2195
+ * The compression currently in effect for this database's column family, read
2196
+ * live from RocksDB, as `{ algorithm, level? }`. `algorithm` is a friendly
2197
+ * name (e.g. `'lz4'`, `'zstd'`, `'none'`); `level` is present only when a
2198
+ * non-default compression level is set. The database must be open. Defaults to
2199
+ * LZ4 when the native build supports it.
2200
+ *
2201
+ * @example
2202
+ * ```typescript
2203
+ * const db = RocksDatabase.open('/path/to/db', { compression: { algorithm: 'zstd', level: 3 } });
2204
+ * db.compression; // { algorithm: 'zstd', level: 3 }
2205
+ * ```
2206
+ */
2207
+ get compression() {
2208
+ return this.store.db.getCompression();
2209
+ }
2210
+ /**
1958
2211
  * Set global database settings.
1959
2212
  *
1960
2213
  * @param options - The options for the database.
@@ -2370,7 +2623,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
2370
2623
  commitResult = await txn.commit();
2371
2624
  } catch (commitErr) {
2372
2625
  if (commitErr instanceof TransactionAlreadyAbortedError) return;
2373
- if (commitErr instanceof TransactionIsBusyError && (options?.retryOnBusy ?? commitErr.hasLog) && attempt < maxRetries) continue;
2626
+ if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? commitErr.hasLog) && attempt < maxRetries) continue;
2374
2627
  this.#abandonTransaction(txn, commitErr);
2375
2628
  return;
2376
2629
  }
@@ -2402,7 +2655,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
2402
2655
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
2403
2656
  const maxRetries = options?.maxRetries ?? 3;
2404
2657
  const isRetryable = (err, attempt) => {
2405
- return err instanceof TransactionIsBusyError && (options?.retryOnBusy ?? err.hasLog) && attempt <= maxRetries;
2658
+ return err instanceof TransactionRetryableError && (options?.retryOnBusy ?? err.hasLog) && attempt <= maxRetries;
2406
2659
  };
2407
2660
  const runAttempt = (attempt) => {
2408
2661
  const txn = new Transaction(this.store, options);
@@ -2598,7 +2851,7 @@ function parseTransactionLog(path, options = {}) {
2598
2851
 
2599
2852
  //#endregion
2600
2853
  //#region src/transaction-log-reader.ts
2601
- const FLOAT_TO_UINT32 = new Float64Array(1);
2854
+ const FLOAT_TO_UINT32 = /* @__PURE__ */ new Float64Array(1);
2602
2855
  const UINT32_FROM_FLOAT = new Uint32Array(FLOAT_TO_UINT32.buffer);
2603
2856
  const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE } = constants;
2604
2857
  /**
@@ -2784,7 +3037,7 @@ function loadLastPosition(transactionLog, readUncommitted) {
2784
3037
  //#region src/index.ts
2785
3038
  const versions = {
2786
3039
  rocksdb: version,
2787
- "rocksdb-js": "2.4.1"
3040
+ "rocksdb-js": "2.6.0"
2788
3041
  };
2789
3042
 
2790
3043
  //#endregion
@@ -2803,6 +3056,8 @@ exports.parseTransactionLog = parseTransactionLog;
2803
3056
  exports.registryStatus = registryStatus;
2804
3057
  exports.shutdown = shutdown;
2805
3058
  exports.stats = stats;
3059
+ exports.supportedCompression = supportedCompression;
2806
3060
  exports.tryFileLock = tryFileLock;
3061
+ exports.validateTransactionLogStore = validateTransactionLogStore;
2807
3062
  exports.versions = versions;
2808
3063
  //# sourceMappingURL=index.cjs.map