@harperfast/rocksdb-js 2.12.0 → 2.13.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
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- //#region \0rolldown/runtime.js
2
+ // \0rolldown/runtime.js
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -25,7 +25,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  enumerable: true
26
26
  }) : target, mod));
27
27
 
28
- //#endregion
29
28
  let node_child_process = require("node:child_process");
30
29
  let node_fs = require("node:fs");
31
30
  let node_module = require("node:module");
@@ -37,7 +36,7 @@ ordered_binary = __toESM(ordered_binary, 1);
37
36
  let _harperfast_extended_iterable = require("@harperfast/extended-iterable");
38
37
  let msgpackr = require("msgpackr");
39
38
 
40
- //#region src/load-binding.ts
39
+ // src/load-binding.ts
41
40
  const nativeExtRE = /\.node$/;
42
41
  const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
43
42
  /**
@@ -172,7 +171,7 @@ const currentThreadId = binding.currentThreadId;
172
171
  * Reads the process-wide steady clock: `std::chrono::steady_clock` as
173
172
  * fractional milliseconds from an unspecified origin that is fixed for the life
174
173
  * of the process. Every sample in the process — main thread and every
175
- * `worker_threads` worker, whenever started or restarted, on Node, Bun and Deno
174
+ * `worker_threads` worker, whenever started or restarted, on Node and Bun
176
175
  * — is in one domain, and the difference between two samples is real elapsed
177
176
  * time unaffected by wall-clock steps. Non-decreasing but not unique (two
178
177
  * samples may be equal; treat `==` as "not later"). Windows QPC samples from
@@ -210,12 +209,30 @@ const transactionLogMapCount = binding.transactionLogMapCount;
210
209
  */
211
210
  const lockWakeCallbackCount = binding.lockWakeCallbackCount;
212
211
  /**
212
+ * The calling thread's counters for the per-transaction iterator pool: pooled
213
+ * iterators created, and idle ones reused by a later range. Internal — used by
214
+ * tests to prove a range was served by a reused iterator.
215
+ */
216
+ const iteratorPoolStats = binding.iteratorPoolStats;
217
+ /**
218
+ * Whether transaction `getRange()` reuses pooled native iterators (off with
219
+ * `ROCKSDB_JS_ITERATOR_POOL=0`). Internal.
220
+ */
221
+ const iteratorPoolEnabled = binding.iteratorPoolEnabled;
222
+ /**
213
223
  * Test-only: force the next `count` transaction commits to fail with TryAgain (rolled back, so
214
224
  * no data is committed), reproducing a stranded-snapshot conflict deterministically. Pass 0 to
215
225
  * disarm. Used by the ERR_TRY_AGAIN retry regression test.
216
226
  */
217
227
  const forceTryAgainForTesting = binding.forceTryAgainForTesting;
218
228
  /**
229
+ * Test-only: turn the RocksDB conflict check before the transaction-log write off (`true`) or back
230
+ * on, so a test can make a logged batch conflict at commit from a stale snapshot.
231
+ */
232
+ const disablePrelogConflictCheckForTesting = binding.disablePrelogConflictCheckForTesting;
233
+ /** Test-only: run the pre-log conflict check on retries alone (`true`, the default) or on every attempt. */
234
+ const setPrelogConflictCheckRetriesOnlyForTesting = binding.setPrelogConflictCheckRetriesOnlyForTesting;
235
+ /**
219
236
  * Test-only seam: makes the physical column-family drop report failure.
220
237
  * `0` = inert, `1` = fail without dropping (the retry performs the drop),
221
238
  * `2` = drop for real and then report failure (the retry resolves as
@@ -258,8 +275,7 @@ const nativeValidateTransactionLog = binding.validateTransactionLog;
258
275
  const stats = binding.stats;
259
276
  const version = binding.version;
260
277
 
261
- //#endregion
262
- //#region src/validate-transaction-log.ts
278
+ // src/validate-transaction-log.ts
263
279
  /**
264
280
  * Validates a transaction log store directory: every `<sequence>.txnlog` file
265
281
  * is checked for a valid header (token, version) and intact v1 entry framing
@@ -299,8 +315,7 @@ function validateTransactionLogStore(path, options) {
299
315
  return new Promise((resolve, reject) => nativeValidateTransactionLog(resolve, reject, path, options?.strict ?? false));
300
316
  }
301
317
 
302
- //#endregion
303
- //#region src/backup.ts
318
+ // src/backup.ts
304
319
  /** Subdirectory (under a backup directory) holding per-backup transaction log snapshots. */
305
320
  const TRANSACTION_LOGS_DIRNAME = "transaction_logs";
306
321
  /** Non-blocking existence check (`fs.existsSync` would block the event loop). */
@@ -584,8 +599,7 @@ const backups = {
584
599
  }
585
600
  };
586
601
 
587
- //#endregion
588
- //#region src/count-estimator.ts
602
+ // src/count-estimator.ts
589
603
  /**
590
604
  * Below this many traversed entries the observed density is too noisy to
591
605
  * calibrate with, so `estimate()` returns the raw statistical estimate.
@@ -728,8 +742,7 @@ var CountEstimator = class {
728
742
  }
729
743
  };
730
744
 
731
- //#endregion
732
- //#region src/util.ts
745
+ // src/util.ts
733
746
  /**
734
747
  * Parses a duration string into milliseconds.
735
748
  *
@@ -800,8 +813,7 @@ function when(subject, callback, errback) {
800
813
  }
801
814
  }
802
815
 
803
- //#endregion
804
- //#region src/dbi.ts
816
+ // src/dbi.ts
805
817
  /**
806
818
  * The base class for all database operations. This base class is shared by
807
819
  * `RocksDatabase` and `Transaction`.
@@ -1086,8 +1098,7 @@ var DBI = class DBI {
1086
1098
  }
1087
1099
  };
1088
1100
 
1089
- //#endregion
1090
- //#region src/tar.ts
1101
+ // src/tar.ts
1091
1102
  /**
1092
1103
  * A minimal, dependency-free streaming USTAR (POSIX tar) encoder.
1093
1104
  *
@@ -1293,8 +1304,7 @@ var TarEncoder = class {
1293
1304
  }
1294
1305
  };
1295
1306
 
1296
- //#endregion
1297
- //#region src/backup-stream.ts
1307
+ // src/backup-stream.ts
1298
1308
  /** Native event discriminator: a new file header (vs. a payload chunk). */
1299
1309
  const EVENT_FILE = 0;
1300
1310
  /**
@@ -1338,9 +1348,22 @@ async function backupToStream(db, stream, options) {
1338
1348
  }
1339
1349
  }
1340
1350
 
1341
- //#endregion
1342
- //#region src/dbi-iterator.ts
1343
- const { ITERATOR_RESULT_DONE, ITERATOR_RESULT_FAST } = constants;
1351
+ // src/dbi-iterator.ts
1352
+ const { ITERATOR_RESULT_DONE, ITERATOR_RESULT_FAST, ITERATOR_RESULT_RELEASED } = constants;
1353
+ const transactionsToTrim = /* @__PURE__ */ new Set();
1354
+ function trimIdleIterators() {
1355
+ try {
1356
+ for (const transaction of transactionsToTrim) transaction.trimIterators();
1357
+ } finally {
1358
+ transactionsToTrim.clear();
1359
+ }
1360
+ }
1361
+ function scheduleTrim(transaction) {
1362
+ if (transaction !== void 0) {
1363
+ if (transactionsToTrim.size === 0) setImmediate(trimIdleIterators);
1364
+ transactionsToTrim.add(transaction);
1365
+ }
1366
+ }
1344
1367
  const DONE_RESULT = Object.freeze({
1345
1368
  done: true,
1346
1369
  value: void 0
@@ -1360,10 +1383,12 @@ var DBIterator = class {
1360
1383
  #includeValues;
1361
1384
  #limit;
1362
1385
  #yielded = 0;
1363
- constructor(iterator, store, includeValues, limit) {
1386
+ #transaction;
1387
+ constructor(iterator, store, includeValues, limit, transaction) {
1364
1388
  this.iterator = iterator;
1365
1389
  this.store = store;
1366
1390
  this.#includeValues = includeValues;
1391
+ this.#transaction = transaction;
1367
1392
  this.#limit = limit ?? void 0;
1368
1393
  }
1369
1394
  [Symbol.iterator]() {
@@ -1371,7 +1396,7 @@ var DBIterator = class {
1371
1396
  }
1372
1397
  next() {
1373
1398
  if (this.#limit !== void 0 && this.#yielded >= this.#limit) {
1374
- this.iterator.return?.();
1399
+ this.#release(this.iterator.return());
1375
1400
  return DONE_RESULT;
1376
1401
  }
1377
1402
  let result;
@@ -1379,11 +1404,15 @@ var DBIterator = class {
1379
1404
  result = this.iterator.next();
1380
1405
  } catch (err) {
1381
1406
  try {
1382
- this.iterator.return?.();
1407
+ this.#release(this.iterator.return());
1383
1408
  } catch {}
1384
1409
  throw err;
1385
1410
  }
1386
1411
  if (result === ITERATOR_RESULT_DONE) return DONE_RESULT;
1412
+ if (result === ITERATOR_RESULT_RELEASED) {
1413
+ scheduleTrim(this.#transaction);
1414
+ return DONE_RESULT;
1415
+ }
1387
1416
  this.#yielded++;
1388
1417
  const includeValues = this.#includeValues;
1389
1418
  const value = {};
@@ -1404,20 +1433,23 @@ var DBIterator = class {
1404
1433
  };
1405
1434
  }
1406
1435
  return(value) {
1407
- this.iterator.return?.();
1436
+ this.#release(this.iterator.return());
1408
1437
  return {
1409
1438
  done: true,
1410
1439
  value
1411
1440
  };
1412
1441
  }
1413
1442
  throw(err) {
1414
- this.iterator.throw?.(err);
1443
+ this.#release(this.iterator.throw(err));
1415
1444
  throw err;
1416
1445
  }
1446
+ #release(pooled) {
1447
+ if (pooled) scheduleTrim(this.#transaction);
1448
+ this.#transaction = void 0;
1449
+ }
1417
1450
  };
1418
1451
 
1419
- //#endregion
1420
- //#region src/encoding.ts
1452
+ // src/encoding.ts
1421
1453
  /**
1422
1454
  * Initializes the key encoder functions.
1423
1455
  *
@@ -1484,8 +1516,7 @@ function createFixedBuffer(size) {
1484
1516
  return buffer;
1485
1517
  }
1486
1518
 
1487
- //#endregion
1488
- //#region src/store.ts
1519
+ // src/store.ts
1489
1520
  const { ONLY_IF_IN_MEMORY_CACHE_FLAG, NOT_IN_MEMORY_CACHE_FLAG, ALWAYS_CREATE_NEW_BUFFER_FLAG, FRESH_VERSION_FLAG, POPULATE_VERSION_FLAG, ITERATOR_REVERSE_FLAG, ITERATOR_INCLUSIVE_END_FLAG, ITERATOR_EXCLUSIVE_START_FLAG, ITERATOR_INCLUDE_VALUES_FLAG, ITERATOR_NEEDS_STABLE_VALUE_BUFFER_FLAG, ITERATOR_CONTEXT_IS_TRANSACTION_FLAG, ITERATOR_HAS_TRANSACTION_ID_FLAG } = constants;
1490
1521
  const KEY_BUFFER_SIZE = 4096;
1491
1522
  const KEY_BUFFER = createFixedBuffer(KEY_BUFFER_SIZE);
@@ -1786,6 +1817,10 @@ var Store = class {
1786
1817
  */
1787
1818
  writeBufferSize;
1788
1819
  /**
1820
+ * The memtable arena block size in bytes; `0` or unset is automatic.
1821
+ */
1822
+ arenaBlockSize;
1823
+ /**
1789
1824
  * The function used to encode keys using the shared `keyBuffer`.
1790
1825
  */
1791
1826
  writeKey;
@@ -1838,6 +1873,7 @@ var Store = class {
1838
1873
  this.timestampFloorLog = options?.timestampFloorLog;
1839
1874
  this.verificationTable = options?.verificationTable;
1840
1875
  this.writeBufferSize = options?.writeBufferSize;
1876
+ this.arenaBlockSize = options?.arenaBlockSize;
1841
1877
  this.writeKey = writeKey;
1842
1878
  this.#columnFamilyOptions = options ? { ...options } : void 0;
1843
1879
  }
@@ -2088,7 +2124,9 @@ var Store = class {
2088
2124
  if (context !== this.db) flags |= ITERATOR_CONTEXT_IS_TRANSACTION_FLAG;
2089
2125
  if (txnId !== void 0) flags |= ITERATOR_HAS_TRANSACTION_ID_FLAG;
2090
2126
  const advancedOptions = options !== void 0 && (options.adaptiveReadahead !== void 0 || options.asyncIO !== void 0 || options.autoReadaheadSize !== void 0 || options.backgroundPurgeOnIteratorCleanup !== void 0 || options.fillCache !== void 0 || options.readaheadSize !== void 0 || options.tailing !== void 0) ? options : void 0;
2091
- return new _harperfast_extended_iterable.ExtendedIterable(new DBIterator(new NativeIterator(context, flags, startKeyEnd, endKeyStart, endKeyEnd, advancedOptions, txnId), this, includeValues, options?.limit));
2127
+ const nativeIterator = new NativeIterator(context, flags, startKeyEnd, endKeyStart, endKeyEnd, advancedOptions, txnId);
2128
+ const transaction = context !== this.db ? context : options?.transaction?._context;
2129
+ return new _harperfast_extended_iterable.ExtendedIterable(new DBIterator(nativeIterator, this, includeValues, options?.limit, transaction));
2092
2130
  }
2093
2131
  getSync(context, key, alwaysCreateNewBuffer = false, options) {
2094
2132
  const keyParam = getKeyParam(this.encodeKey(key));
@@ -2202,7 +2240,8 @@ var Store = class {
2202
2240
  transactionLogsPath: this.transactionLogsPath,
2203
2241
  timestampFloorLog: this.timestampFloorLog,
2204
2242
  verificationTable: this.verificationTable,
2205
- writeBufferSize: this.writeBufferSize
2243
+ writeBufferSize: this.writeBufferSize,
2244
+ arenaBlockSize: this.arenaBlockSize
2206
2245
  });
2207
2246
  this.identityPath = this.db.identityPath;
2208
2247
  return false;
@@ -2308,8 +2347,7 @@ function getKeyParam(keyBuffer) {
2308
2347
  return keyBuffer.length;
2309
2348
  }
2310
2349
 
2311
- //#endregion
2312
- //#region src/transaction.ts
2350
+ // src/transaction.ts
2313
2351
  /**
2314
2352
  * Sentinel value returned by `commit()` when `coordinatedRetry: true` and the
2315
2353
  * transaction encountered an IsBusy conflict. The native layer parks on VT
@@ -2333,10 +2371,18 @@ var TransactionAlreadyAbortedError = class extends Error {
2333
2371
  */
2334
2372
  var TransactionRetryableError = class extends Error {
2335
2373
  hasLog;
2374
+ /**
2375
+ * The attempt was refused before writing its transaction-log batch (`transactionLogAbortMarkers`):
2376
+ * an older transaction held a write intent on one of its keys (`transactionLogWriteIntents`), or, on a retry, RocksDB's conflict
2377
+ * check found a write that already committed (see `lastConflictKind()`). Nothing of it is in the
2378
+ * log, so a retry may stage its batch afresh.
2379
+ */
2380
+ refusedBeforeLog;
2336
2381
  txn;
2337
2382
  constructor(error, txn) {
2338
2383
  super(error.message);
2339
2384
  this.hasLog = error.hasLog ?? false;
2385
+ this.refusedBeforeLog = error.refusedBeforeLog ?? false;
2340
2386
  this.txn = txn;
2341
2387
  }
2342
2388
  };
@@ -2366,6 +2412,38 @@ var TransactionAbandonedError = class extends Error {
2366
2412
  }
2367
2413
  };
2368
2414
  /**
2415
+ * `abort()` of a transaction whose transaction-log batch could not be marked aborted
2416
+ * (`transactionLogAbortMarkers`). The batch stays unresolved, so committed readers do not pass it and
2417
+ * the log store refuses appends, until a retry on the next append, database flush or store close
2418
+ * marks it.
2419
+ */
2420
+ var TransactionAbortUnresolvedError = class extends Error {
2421
+ code = "ERR_TRANSACTION_ABORT_UNRESOLVED";
2422
+ txn;
2423
+ constructor(error, txn) {
2424
+ super(error.message);
2425
+ this.txn = txn;
2426
+ }
2427
+ };
2428
+ /**
2429
+ * Why a transaction's most recent attempt was refused or retried, from {@link Transaction.lastConflictKind}.
2430
+ */
2431
+ const ConflictKind = {
2432
+ none: 0,
2433
+ /** Refused on an older transaction's write intent before writing its log batch. */
2434
+ intentBeforeLog: 1,
2435
+ /** Refused on an older transaction's write intent after its log batch was written. */
2436
+ intentAfterLog: 2,
2437
+ /** RocksDB found a conflict at commit. */
2438
+ atCommit: 3,
2439
+ /**
2440
+ * RocksDB's conflict check, run before the log write, found a write that already committed, or,
2441
+ * as at commit, could not validate because the memtable history it needs was flushed (needs a
2442
+ * RocksDB build that exports the check).
2443
+ */
2444
+ checkBeforeLog: 4
2445
+ };
2446
+ /**
2369
2447
  * Provides transaction level operations to a transaction callback.
2370
2448
  */
2371
2449
  var Transaction = class extends DBI {
@@ -2380,9 +2458,10 @@ var Transaction = class extends DBI {
2380
2458
  if (store.readOnly) {
2381
2459
  super(store);
2382
2460
  this.#txn = { id: 0 };
2383
- this.abandonWrites = this.abort = this.commitSync = this.setTimestamp = () => {};
2461
+ this.abandonWrites = this.abort = this.commitSync = this.setTimestamp = this.setParkDeadline = () => {};
2384
2462
  this.commit = async () => {};
2385
- this.getTimestamp = () => 0;
2463
+ this.getTimestamp = this.lastConflictKind = () => 0;
2464
+ this.logWritten = () => false;
2386
2465
  } else {
2387
2466
  const txn = new NativeTransaction(store.db, options);
2388
2467
  super(store, txn);
@@ -2408,6 +2487,7 @@ var Transaction = class extends DBI {
2408
2487
  this.#txn.abort();
2409
2488
  } catch (err) {
2410
2489
  if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABANDONED") throw new TransactionAbandonedError(err, this);
2490
+ if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABORT_UNRESOLVED") throw new TransactionAbortUnresolvedError(err, this);
2411
2491
  throw err;
2412
2492
  }
2413
2493
  }
@@ -2476,6 +2556,28 @@ var Transaction = class extends DBI {
2476
2556
  return this.#txn.getTimestamp();
2477
2557
  }
2478
2558
  /**
2559
+ * Whether this transaction's batch is in the transaction log. A commit conflict after the batch
2560
+ * was written keeps it for the retry, which must not stage it again; an attempt refused before
2561
+ * writing it (`refusedBeforeLog`) leaves nothing.
2562
+ */
2563
+ logWritten() {
2564
+ return this.#txn.logWritten();
2565
+ }
2566
+ /**
2567
+ * Why the most recent attempt was refused or retried; one of {@link ConflictKind}.
2568
+ */
2569
+ lastConflictKind() {
2570
+ return this.#txn.lastConflictKind();
2571
+ }
2572
+ /**
2573
+ * Bounds coordinated-retry parks: past `deadline` (epoch milliseconds) a parked commit resolves
2574
+ * `RETRY_NOW` at once, so a caller enforcing its own deadline can abort instead of waiting out
2575
+ * the default park bound. 0 clears it.
2576
+ */
2577
+ setParkDeadline(deadline) {
2578
+ this.#txn.setParkDeadline(deadline);
2579
+ }
2580
+ /**
2479
2581
  * Get the transaction id.
2480
2582
  */
2481
2583
  get id() {
@@ -2496,8 +2598,8 @@ var Transaction = class extends DBI {
2496
2598
  }
2497
2599
  };
2498
2600
 
2499
- //#endregion
2500
- //#region src/database.ts
2601
+ // src/database.ts
2602
+ const backoffCell = new Int32Array(new SharedArrayBuffer(4));
2501
2603
  const BACKGROUND_ERROR_SEVERITY_NAMES = [
2502
2604
  "none",
2503
2605
  "soft",
@@ -2907,6 +3009,19 @@ var RocksDatabase = class RocksDatabase extends DBI {
2907
3009
  return this.store.db.flushSync(options);
2908
3010
  }
2909
3011
  /**
3012
+ * Syncs the write-ahead log to stable storage (`DB::SyncWAL`), so every WAL-backed write
3013
+ * committed before the call survives a power loss afterwards. Unlike {@link flush} it writes
3014
+ * no SST file and never stalls writers. Not covered: writes that skipped the WAL
3015
+ * (`disableWAL`), and transaction-log files, which keep their own durability. Runs on the
3016
+ * libuv threadpool; RocksDB serializes concurrent syncs of one WAL, so keep one in flight
3017
+ * per database rather than parking pool workers on a queue of them.
3018
+ *
3019
+ * On a read-only database the returned promise resolves without syncing.
3020
+ */
3021
+ syncWal() {
3022
+ return new Promise((resolve, reject) => this.store.db.syncWal(resolve, reject));
3023
+ }
3024
+ /**
2910
3025
  * Returns the most recent {@link BackgroundError} observed on this database, or
2911
3026
  * `null` when none has occurred. The `'error'` event is the push counterpart;
2912
3027
  * this is the pull equivalent for on-demand checks (e.g. a health probe) and
@@ -3375,7 +3490,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
3375
3490
  async transaction(callback, options) {
3376
3491
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
3377
3492
  const maxRetries = options?.maxRetries ?? 3;
3378
- const txn = new Transaction(this.store, options);
3493
+ const txn = new Transaction(this.store, options?.retryOnBusy === false && options.coordinatedRetry ? {
3494
+ ...options,
3495
+ coordinatedRetry: false
3496
+ } : options);
3379
3497
  let result;
3380
3498
  this.notify("begin-transaction");
3381
3499
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -3389,7 +3507,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3389
3507
  commitResult = await txn.commit();
3390
3508
  } catch (commitErr) {
3391
3509
  if (commitErr instanceof TransactionAlreadyAbortedError) return;
3392
- if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? commitErr.hasLog) && attempt < maxRetries) continue;
3510
+ if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? (commitErr.hasLog || commitErr.refusedBeforeLog)) && attempt < maxRetries) continue;
3393
3511
  this.#abandonTransaction(txn, commitErr);
3394
3512
  return;
3395
3513
  }
@@ -3421,10 +3539,13 @@ var RocksDatabase = class RocksDatabase extends DBI {
3421
3539
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
3422
3540
  const maxRetries = options?.maxRetries ?? 3;
3423
3541
  const isRetryable = (err, attempt) => {
3424
- return err instanceof TransactionRetryableError && (options?.retryOnBusy ?? err.hasLog) && attempt <= maxRetries;
3542
+ if (!(err instanceof TransactionRetryableError && (options?.retryOnBusy ?? (err.hasLog || err.refusedBeforeLog)) && attempt <= maxRetries)) return false;
3543
+ const kind = txn.lastConflictKind();
3544
+ if (kind === ConflictKind.intentBeforeLog || kind === ConflictKind.intentAfterLog) Atomics.wait(backoffCell, 0, 0, Math.min(2 ** attempt, 50));
3545
+ return true;
3425
3546
  };
3547
+ const txn = new Transaction(this.store, options);
3426
3548
  const runAttempt = (attempt) => {
3427
- const txn = new Transaction(this.store, options);
3428
3549
  let result;
3429
3550
  try {
3430
3551
  result = callback(txn, attempt);
@@ -3440,7 +3561,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3440
3561
  if (isRetryable(commitErr, attempt)) return runAttempt(attempt + 1);
3441
3562
  this.#abandonTransaction(txn, commitErr);
3442
3563
  }
3443
- });
3564
+ }, (callbackErr) => this.#abortTransaction(txn, callbackErr));
3444
3565
  try {
3445
3566
  txn.commitSync();
3446
3567
  return result;
@@ -3465,7 +3586,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3465
3586
  try {
3466
3587
  txn.abort();
3467
3588
  } catch (abortErr) {
3468
- if (abortErr instanceof TransactionAbandonedError) throw abortErr;
3589
+ if (abortErr instanceof TransactionAbandonedError || abortErr instanceof TransactionAbortUnresolvedError) throw abortErr;
3469
3590
  }
3470
3591
  throw commitErr;
3471
3592
  }
@@ -3580,10 +3701,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
3580
3701
  }
3581
3702
  };
3582
3703
 
3583
- //#endregion
3584
- //#region src/parse-transaction-log.ts
3585
- const { TRANSACTION_LOG_TOKEN, TRANSACTION_LOG_ENTRY_HEADER_SIZE: TRANSACTION_LOG_ENTRY_HEADER_SIZE$1 } = constants;
3704
+ // src/parse-transaction-log.ts
3705
+ const { TRANSACTION_LOG_TOKEN, TRANSACTION_LOG_ENTRY_HEADER_SIZE: TRANSACTION_LOG_ENTRY_HEADER_SIZE$1, TRANSACTION_LOG_ENTRY_LAST_FLAG: TRANSACTION_LOG_ENTRY_LAST_FLAG$1, TRANSACTION_LOG_ENTRY_ABORTED_FLAG: TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1 } = constants;
3586
3706
  const MIN_VALID_TIMESTAMP = Date.UTC(2026, 0, 27);
3707
+ const VALID_FLAGS_MASK = TRANSACTION_LOG_ENTRY_LAST_FLAG$1 | TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1;
3587
3708
  /**
3588
3709
  * Loads an entire transaction log file into memory.
3589
3710
  * @param path - The path to the transaction log file.
@@ -3621,6 +3742,8 @@ function parseTransactionLog(path, options = {}) {
3621
3742
  if (!Number.isFinite(timestamp) || timestamp < MIN_VALID_TIMESTAMP) anomalies.push(`Header timestamp ${timestamp} predates 2026-01-27 (possible corruption)`);
3622
3743
  const entries = [];
3623
3744
  let entryAnomalyCount = 0;
3745
+ let previousEndedTransaction = true;
3746
+ let previousTimestamp = NaN;
3624
3747
  while (fileOffset < size) {
3625
3748
  const timestamp = read(8).readDoubleBE(0);
3626
3749
  if (timestamp === 0) {
@@ -3637,7 +3760,11 @@ function parseTransactionLog(path, options = {}) {
3637
3760
  const data = read(length);
3638
3761
  const entryAnomalies = [];
3639
3762
  if (!Number.isFinite(timestamp) || timestamp < MIN_VALID_TIMESTAMP) entryAnomalies.push(`timestamp ${timestamp} predates 2026-01-27 (possible corruption)`);
3640
- if ((flags & -2) !== 0) entryAnomalies.push(`flags 0x${flags.toString(16).padStart(2, "0")} contains undefined bits (expected 0x00 or 0x01)`);
3763
+ if ((flags & ~VALID_FLAGS_MASK) !== 0) entryAnomalies.push(`flags 0x${flags.toString(16).padStart(2, "0")} contains undefined bits (expected 0x00-0x03)`);
3764
+ const startsTransaction = previousEndedTransaction || timestamp !== previousTimestamp;
3765
+ if ((flags & TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1) !== 0 && !startsTransaction) entryAnomalies.push("abort flag on an entry that does not start a transaction");
3766
+ previousEndedTransaction = (flags & TRANSACTION_LOG_ENTRY_LAST_FLAG$1) !== 0;
3767
+ previousTimestamp = timestamp;
3641
3768
  const entry = {
3642
3769
  timestamp,
3643
3770
  length,
@@ -3666,11 +3793,11 @@ function parseTransactionLog(path, options = {}) {
3666
3793
  }
3667
3794
  }
3668
3795
 
3669
- //#endregion
3670
- //#region src/transaction-log-reader.ts
3796
+ // src/transaction-log-reader.ts
3671
3797
  const FLOAT_TO_UINT32 = /* @__PURE__ */ new Float64Array(1);
3672
3798
  const UINT32_FROM_FLOAT = new Uint32Array(FLOAT_TO_UINT32.buffer);
3673
- const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE } = constants;
3799
+ const EXACT_START_SPAN = /* @__PURE__ */ new Float64Array(3);
3800
+ const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE, TRANSACTION_LOG_ENTRY_LAST_FLAG, TRANSACTION_LOG_ENTRY_ABORTED_FLAG } = constants;
3674
3801
  /**
3675
3802
  * Number of consecutive well-formed frames that, on their own, signal that real log data has
3676
3803
  * resumed after a framing break. Kept in step with `RESYNC_MIN_FRAMES` in
@@ -3706,6 +3833,32 @@ var CorruptFrameError = class extends RangeError {
3706
3833
  }
3707
3834
  };
3708
3835
  /**
3836
+ * Thrown when an iterator advances past segments it never read and that can no longer be read —
3837
+ * typically ones retention purged while the reader lagged. The entries they held cannot be
3838
+ * recovered from this log, so a consumer replicating it must not treat the stream as contiguous.
3839
+ * Missing sequence numbers prove only that continuity is unknown, not that each held entries.
3840
+ *
3841
+ * The iterator is left positioned at the start of `resumeLogId`, so a caller that accepts the
3842
+ * discontinuity calls `next()` again to continue from there.
3843
+ *
3844
+ * Not a `RangeError`: this is not broken framing, and consumers classify those by class.
3845
+ */
3846
+ var TransactionLogGapError = class extends Error {
3847
+ /** Sequence number of the last segment this iterator read up to. */
3848
+ logId;
3849
+ /** Sequence number of the segment iteration resumes in. */
3850
+ resumeLogId;
3851
+ /** Segments between `logId` and `resumeLogId` that could not be read. Empty segments are not counted. */
3852
+ unavailableLogCount;
3853
+ constructor(logId, resumeLogId, unavailableLogCount) {
3854
+ super(`Transaction log iterator skipped ${unavailableLogCount} unavailable segment(s) between log ${logId} and log ${resumeLogId}; entries in them were not read`);
3855
+ this.name = "TransactionLogGapError";
3856
+ this.logId = logId;
3857
+ this.resumeLogId = resumeLogId;
3858
+ this.unavailableLogCount = unavailableLogCount;
3859
+ }
3860
+ };
3861
+ /**
3709
3862
  * Whether a complete, in-bounds frame begins at `pos`. The only sane bound on an entry's length is
3710
3863
  * the written extent: a single entry can legitimately exceed the rotation threshold (the first
3711
3864
  * entry written to a fresh file is always written in full). A zero timestamp is the
@@ -3781,6 +3934,14 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3781
3934
  let dataView;
3782
3935
  let logBuffer = this._currentLogBuffer?.deref();
3783
3936
  let foundExactStart = false;
3937
+ let stopLogId = 0;
3938
+ let stopPosition = 0;
3939
+ let tailLogId = 0;
3940
+ let tailPosition = 0;
3941
+ let segmentRemovals = 0;
3942
+ let previousEndedTransaction = true;
3943
+ let previousTimestamp = NaN;
3944
+ let skippingAborted = false;
3784
3945
  if (start === void 0 && !startFromLastFlushed) {
3785
3946
  position = size;
3786
3947
  if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
@@ -3790,11 +3951,32 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3790
3951
  FLOAT_TO_UINT32[0] = this._getLastFlushed();
3791
3952
  if (FLOAT_TO_UINT32[0] === 0) FLOAT_TO_UINT32[0] = this._findPosition(0);
3792
3953
  start ??= 0;
3954
+ } else if (exactStart) {
3955
+ const startPosition = this._findExactStart(start, EXACT_START_SPAN);
3956
+ FLOAT_TO_UINT32[0] = EXACT_START_SPAN[0];
3957
+ stopLogId = UINT32_FROM_FLOAT[1];
3958
+ stopPosition = UINT32_FROM_FLOAT[0];
3959
+ FLOAT_TO_UINT32[0] = EXACT_START_SPAN[1];
3960
+ tailLogId = UINT32_FROM_FLOAT[1];
3961
+ tailPosition = UINT32_FROM_FLOAT[0];
3962
+ segmentRemovals = EXACT_START_SPAN[2];
3963
+ FLOAT_TO_UINT32[0] = startPosition;
3793
3964
  } else FLOAT_TO_UINT32[0] = this._findPosition(start);
3794
3965
  logId = UINT32_FROM_FLOAT[1];
3795
3966
  position = UINT32_FROM_FLOAT[0];
3796
3967
  if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3797
3968
  }
3969
+ const positionedAt = position;
3970
+ if (stopLogId !== 0 && pastStop(logId, position, stopLogId, stopPosition)) {
3971
+ stopLogId = 0;
3972
+ if (skipToTail(this, logId, position, tailLogId, tailPosition, segmentRemovals)) {
3973
+ logId = tailLogId;
3974
+ position = tailPosition;
3975
+ logBuffer = tailLogBuffer ?? logBuffer;
3976
+ }
3977
+ tailLogBuffer = void 0;
3978
+ }
3979
+ let gapFloor = logId;
3798
3980
  if (logBuffer === void 0 || logBuffer.logId !== logId) {
3799
3981
  logBuffer = getLogMemoryMap(this, logId);
3800
3982
  if (logBuffer && latestLogId === logId && !readUncommitted && this._currentLogBuffer?.deref() !== logBuffer) this._currentLogBuffer = new WeakRef(logBuffer);
@@ -3804,6 +3986,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3804
3986
  logBuffer.size = 0;
3805
3987
  logBuffer.dataView = new DataView(logBuffer.buffer);
3806
3988
  size = 0;
3989
+ if (positionedAt <= TRANSACTION_LOG_FILE_HEADER_SIZE || logId > 0 && this.getLogFileSize(logId) > 0) gapFloor = logId - 1;
3807
3990
  }
3808
3991
  }
3809
3992
  dataView = logBuffer.dataView;
@@ -3813,7 +3996,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3813
3996
  if (cachedSize === void 0) size = logBuffer.size = readableExtent(logBuffer);
3814
3997
  else size = cachedSize;
3815
3998
  }
3816
- return {
3999
+ const iterator = {
3817
4000
  [Symbol.iterator]() {
3818
4001
  return this;
3819
4002
  },
@@ -3825,13 +4008,17 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3825
4008
  if (latestLogId > logBuffer.logId) {
3826
4009
  size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3827
4010
  if (position >= size) {
3828
- const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
4011
+ const fromLogId = logBuffer.logId;
4012
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor);
3829
4013
  if (nextLogBuffer) {
4014
+ const unavailable = unavailableLogCount;
3830
4015
  dataView = nextLogBuffer.dataView;
3831
4016
  logBuffer = nextLogBuffer;
3832
4017
  if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3833
4018
  else size = latestSize;
3834
4019
  position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4020
+ previousEndedTransaction = true;
4021
+ if (unavailable > 0) throw new TransactionLogGapError(Math.max(fromLogId, gapFloor), logBuffer.logId, unavailable);
3835
4022
  }
3836
4023
  }
3837
4024
  }
@@ -3851,30 +4038,55 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3851
4038
  if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE > limit) {
3852
4039
  const broken = corruptFrame(`Corrupt transaction log: truncated entry header at position ${position.toString(16)} of log ${logBuffer.logId} (available=${limit - position})`, transactionLog, dataView, logBuffer, position, limit, size, !!readUncommitted);
3853
4040
  position = broken.nextPosition;
4041
+ previousEndedTransaction = true;
3854
4042
  throw broken.error;
3855
4043
  }
3856
4044
  const length = dataView.getUint32(position + 8);
3857
4045
  if (length === 0 || position + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length > limit) {
3858
4046
  const broken = corruptFrame(length === 0 ? `Corrupt transaction log entry at position ${position.toString(16)} of log ${logBuffer.logId}: zero-length entry` : `Corrupt transaction log entry at position ${position.toString(16)} of log ${logBuffer.logId}: declared length ${length} overruns the log (limit=${limit})`, transactionLog, dataView, logBuffer, position, limit, size, !!readUncommitted);
3859
4047
  position = broken.nextPosition;
4048
+ previousEndedTransaction = true;
3860
4049
  throw broken.error;
3861
4050
  }
3862
4051
  position += TRANSACTION_LOG_ENTRY_HEADER_SIZE;
4052
+ const flags = logBuffer[position - 1];
4053
+ const aborted = (flags & TRANSACTION_LOG_ENTRY_ABORTED_FLAG) !== 0;
4054
+ if (previousEndedTransaction || timestamp !== previousTimestamp || aborted) skippingAborted = aborted;
4055
+ previousEndedTransaction = (flags & TRANSACTION_LOG_ENTRY_LAST_FLAG) !== 0;
4056
+ previousTimestamp = timestamp;
3863
4057
  let matchesRange;
3864
- if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
4058
+ if (skippingAborted) matchesRange = false;
4059
+ else if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
3865
4060
  else if (exactStart) {
4061
+ matchesRange = false;
3866
4062
  if (timestamp === start) {
3867
4063
  matchesRange = !exclusiveStart;
3868
4064
  foundExactStart = true;
3869
- } else matchesRange = false;
4065
+ stopLogId = 0;
4066
+ }
3870
4067
  } else matchesRange = (exclusiveStart ? timestamp > start : timestamp >= start) && timestamp < end;
4068
+ if (!matchesRange && stopLogId !== 0 && pastStop(logBuffer.logId, position + length, stopLogId, stopPosition)) {
4069
+ stopLogId = 0;
4070
+ position += length;
4071
+ if (skipToTail(transactionLog, logBuffer.logId, position, tailLogId, tailPosition, segmentRemovals)) {
4072
+ if (tailLogBuffer) {
4073
+ logBuffer = tailLogBuffer;
4074
+ dataView = tailLogBuffer.dataView;
4075
+ }
4076
+ position = tailPosition;
4077
+ previousEndedTransaction = true;
4078
+ size = 0;
4079
+ }
4080
+ tailLogBuffer = void 0;
4081
+ return iterator.next();
4082
+ }
3871
4083
  const entryStart = position;
3872
4084
  position += length;
3873
4085
  if (matchesRange) return {
3874
4086
  done: false,
3875
4087
  value: {
3876
4088
  timestamp,
3877
- endTxn: Boolean(logBuffer[entryStart - 1] & 1),
4089
+ endTxn: previousEndedTransaction,
3878
4090
  data: logBuffer.subarray(entryStart, position)
3879
4091
  }
3880
4092
  };
@@ -3885,11 +4097,13 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3885
4097
  size = logBuffer.size ?? readableExtent(logBuffer);
3886
4098
  if (!readUncommitted) logBuffer.size = size;
3887
4099
  if (position < size) continue;
3888
- const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
4100
+ const fromLogId = logBuffer.logId;
4101
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor);
3889
4102
  if (!nextLogBuffer) return {
3890
4103
  done: true,
3891
4104
  value: void 0
3892
4105
  };
4106
+ const unavailable = unavailableLogCount;
3893
4107
  logBuffer = nextLogBuffer;
3894
4108
  dataView = logBuffer.dataView;
3895
4109
  if (latestLogId === logBuffer.logId) size = latestSize;
@@ -3901,6 +4115,8 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3901
4115
  } else size = cachedSize;
3902
4116
  }
3903
4117
  position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4118
+ previousEndedTransaction = true;
4119
+ if (unavailable > 0) throw new TransactionLogGapError(Math.max(fromLogId, gapFloor), logBuffer.logId, unavailable);
3904
4120
  }
3905
4121
  }
3906
4122
  }
@@ -3910,7 +4126,29 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3910
4126
  };
3911
4127
  }
3912
4128
  };
4129
+ return iterator;
3913
4130
  } });
4131
+ function pastStop(logId, position, stopLogId, stopPosition) {
4132
+ return logId > stopLogId || logId === stopLogId && position >= stopPosition;
4133
+ }
4134
+ let tailLogBuffer;
4135
+ /**
4136
+ * Whether an exact-start lookup at `position` of `fromLogId` may take its one-shot jump to the tail.
4137
+ * next() calls it on a frame boundary, so a mapping error leaves a resumable cursor; from query()
4138
+ * the error escapes before any iterator exists. The removal
4139
+ * check follows the mapping and holds the store's lock, so a purge either declines the jump (the
4140
+ * scan then reports the gap) or follows it like a purge of segments already read.
4141
+ */
4142
+ function skipToTail(transactionLog, fromLogId, position, tailLogId, tailPosition, segmentRemovals) {
4143
+ tailLogBuffer = void 0;
4144
+ if (tailLogId === fromLogId) return tailPosition > position;
4145
+ if (tailLogId < fromLogId) return false;
4146
+ const mapped = getLogMemoryMap(transactionLog, tailLogId);
4147
+ if (mapped === void 0 || transactionLog._segmentRemovals() !== segmentRemovals) return false;
4148
+ tailLogBuffer = mapped;
4149
+ return true;
4150
+ }
4151
+ let unavailableLogCount = 0;
3914
4152
  /**
3915
4153
  * Maps the next readable segment after `fromLogId`, skipping a run retention has deleted —
3916
4154
  * stopping at the hole wedges the iterator there for every later poll (invariant 30).
@@ -3919,15 +4157,22 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3919
4157
  * `_findPosition(0)`, which names the bottom of the contiguous run ending at the current segment
3920
4158
  * and so lands *past* a survivor sitting between two holes.
3921
4159
  */
3922
- function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId) {
4160
+ function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor) {
4161
+ unavailableLogCount = 0;
3923
4162
  let candidateLogId = fromLogId + 1;
3924
4163
  while (candidateLogId <= latestLogId) {
3925
4164
  const logBuffer = getLogMemoryMap(transactionLog, candidateLogId);
3926
4165
  if (logBuffer) {
3927
4166
  if (candidateLogId === latestLogId || readableExtent(logBuffer) > TRANSACTION_LOG_FILE_HEADER_SIZE) return logBuffer;
3928
- } else if (transactionLog.getLogFileSize(candidateLogId) > TRANSACTION_LOG_FILE_HEADER_SIZE) return;
3929
- candidateLogId = transactionLog._nextLogId(candidateLogId);
3930
- if (candidateLogId === 0) return;
4167
+ } else {
4168
+ const fileSize = transactionLog.getLogFileSize(candidateLogId);
4169
+ if (fileSize > TRANSACTION_LOG_FILE_HEADER_SIZE) return;
4170
+ if (fileSize === 0 && candidateLogId > gapFloor) unavailableLogCount++;
4171
+ }
4172
+ const nextLogId = transactionLog._nextLogId(candidateLogId);
4173
+ if (nextLogId === 0) return;
4174
+ unavailableLogCount += Math.max(0, nextLogId - 1 - Math.max(candidateLogId, gapFloor));
4175
+ candidateLogId = nextLogId;
3931
4176
  }
3932
4177
  }
3933
4178
  /**
@@ -3981,15 +4226,14 @@ function loadLastPosition(transactionLog, readUncommitted) {
3981
4226
  };
3982
4227
  }
3983
4228
 
3984
- //#endregion
3985
- //#region src/index.ts
4229
+ // src/index.ts
3986
4230
  const versions = {
3987
4231
  rocksdb: version,
3988
- "rocksdb-js": "2.12.0"
4232
+ "rocksdb-js": "2.13.0"
3989
4233
  };
3990
4234
 
3991
- //#endregion
3992
4235
  exports.BackgroundError = BackgroundError;
4236
+ exports.ConflictKind = ConflictKind;
3993
4237
  exports.CorruptFrameError = CorruptFrameError;
3994
4238
  exports.CountEstimator = CountEstimator;
3995
4239
  exports.DBI = DBI;
@@ -3997,7 +4241,9 @@ exports.DBIterator = DBIterator;
3997
4241
  exports.RocksDatabase = RocksDatabase;
3998
4242
  exports.Store = Store;
3999
4243
  exports.Transaction = Transaction;
4244
+ exports.TransactionAbortUnresolvedError = TransactionAbortUnresolvedError;
4000
4245
  exports.TransactionLog = TransactionLog;
4246
+ exports.TransactionLogGapError = TransactionLogGapError;
4001
4247
  exports.backups = backups;
4002
4248
  exports.constants = constants;
4003
4249
  exports.coolTransactionLogs = coolTransactionLogs;