@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.mjs CHANGED
@@ -8,11 +8,10 @@ import * as orderedBinary from "ordered-binary";
8
8
  import { ExtendedIterable } from "@harperfast/extended-iterable";
9
9
  import { Encoder } from "msgpackr";
10
10
 
11
- //#region \0rolldown/runtime.js
11
+ // \0rolldown/runtime.js
12
12
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
13
13
 
14
- //#endregion
15
- //#region src/load-binding.ts
14
+ // src/load-binding.ts
16
15
  const nativeExtRE = /\.node$/;
17
16
  const req = createRequire(import.meta.url);
18
17
  /**
@@ -147,7 +146,7 @@ const currentThreadId = binding.currentThreadId;
147
146
  * Reads the process-wide steady clock: `std::chrono::steady_clock` as
148
147
  * fractional milliseconds from an unspecified origin that is fixed for the life
149
148
  * of the process. Every sample in the process — main thread and every
150
- * `worker_threads` worker, whenever started or restarted, on Node, Bun and Deno
149
+ * `worker_threads` worker, whenever started or restarted, on Node and Bun
151
150
  * — is in one domain, and the difference between two samples is real elapsed
152
151
  * time unaffected by wall-clock steps. Non-decreasing but not unique (two
153
152
  * samples may be equal; treat `==` as "not later"). Windows QPC samples from
@@ -185,12 +184,30 @@ const transactionLogMapCount = binding.transactionLogMapCount;
185
184
  */
186
185
  const lockWakeCallbackCount = binding.lockWakeCallbackCount;
187
186
  /**
187
+ * The calling thread's counters for the per-transaction iterator pool: pooled
188
+ * iterators created, and idle ones reused by a later range. Internal — used by
189
+ * tests to prove a range was served by a reused iterator.
190
+ */
191
+ const iteratorPoolStats = binding.iteratorPoolStats;
192
+ /**
193
+ * Whether transaction `getRange()` reuses pooled native iterators (off with
194
+ * `ROCKSDB_JS_ITERATOR_POOL=0`). Internal.
195
+ */
196
+ const iteratorPoolEnabled = binding.iteratorPoolEnabled;
197
+ /**
188
198
  * Test-only: force the next `count` transaction commits to fail with TryAgain (rolled back, so
189
199
  * no data is committed), reproducing a stranded-snapshot conflict deterministically. Pass 0 to
190
200
  * disarm. Used by the ERR_TRY_AGAIN retry regression test.
191
201
  */
192
202
  const forceTryAgainForTesting = binding.forceTryAgainForTesting;
193
203
  /**
204
+ * Test-only: turn the RocksDB conflict check before the transaction-log write off (`true`) or back
205
+ * on, so a test can make a logged batch conflict at commit from a stale snapshot.
206
+ */
207
+ const disablePrelogConflictCheckForTesting = binding.disablePrelogConflictCheckForTesting;
208
+ /** Test-only: run the pre-log conflict check on retries alone (`true`, the default) or on every attempt. */
209
+ const setPrelogConflictCheckRetriesOnlyForTesting = binding.setPrelogConflictCheckRetriesOnlyForTesting;
210
+ /**
194
211
  * Test-only seam: makes the physical column-family drop report failure.
195
212
  * `0` = inert, `1` = fail without dropping (the retry performs the drop),
196
213
  * `2` = drop for real and then report failure (the retry resolves as
@@ -233,8 +250,7 @@ const nativeValidateTransactionLog = binding.validateTransactionLog;
233
250
  const stats = binding.stats;
234
251
  const version = binding.version;
235
252
 
236
- //#endregion
237
- //#region src/validate-transaction-log.ts
253
+ // src/validate-transaction-log.ts
238
254
  /**
239
255
  * Validates a transaction log store directory: every `<sequence>.txnlog` file
240
256
  * is checked for a valid header (token, version) and intact v1 entry framing
@@ -274,8 +290,7 @@ function validateTransactionLogStore(path, options) {
274
290
  return new Promise((resolve, reject) => nativeValidateTransactionLog(resolve, reject, path, options?.strict ?? false));
275
291
  }
276
292
 
277
- //#endregion
278
- //#region src/backup.ts
293
+ // src/backup.ts
279
294
  /** Subdirectory (under a backup directory) holding per-backup transaction log snapshots. */
280
295
  const TRANSACTION_LOGS_DIRNAME = "transaction_logs";
281
296
  /** Non-blocking existence check (`fs.existsSync` would block the event loop). */
@@ -559,8 +574,7 @@ const backups = {
559
574
  }
560
575
  };
561
576
 
562
- //#endregion
563
- //#region src/count-estimator.ts
577
+ // src/count-estimator.ts
564
578
  /**
565
579
  * Below this many traversed entries the observed density is too noisy to
566
580
  * calibrate with, so `estimate()` returns the raw statistical estimate.
@@ -703,8 +717,7 @@ var CountEstimator = class {
703
717
  }
704
718
  };
705
719
 
706
- //#endregion
707
- //#region src/util.ts
720
+ // src/util.ts
708
721
  /**
709
722
  * Parses a duration string into milliseconds.
710
723
  *
@@ -775,8 +788,7 @@ function when(subject, callback, errback) {
775
788
  }
776
789
  }
777
790
 
778
- //#endregion
779
- //#region src/dbi.ts
791
+ // src/dbi.ts
780
792
  /**
781
793
  * The base class for all database operations. This base class is shared by
782
794
  * `RocksDatabase` and `Transaction`.
@@ -1061,8 +1073,7 @@ var DBI = class DBI {
1061
1073
  }
1062
1074
  };
1063
1075
 
1064
- //#endregion
1065
- //#region src/tar.ts
1076
+ // src/tar.ts
1066
1077
  /**
1067
1078
  * A minimal, dependency-free streaming USTAR (POSIX tar) encoder.
1068
1079
  *
@@ -1268,8 +1279,7 @@ var TarEncoder = class {
1268
1279
  }
1269
1280
  };
1270
1281
 
1271
- //#endregion
1272
- //#region src/backup-stream.ts
1282
+ // src/backup-stream.ts
1273
1283
  /** Native event discriminator: a new file header (vs. a payload chunk). */
1274
1284
  const EVENT_FILE = 0;
1275
1285
  /**
@@ -1313,9 +1323,22 @@ async function backupToStream(db, stream, options) {
1313
1323
  }
1314
1324
  }
1315
1325
 
1316
- //#endregion
1317
- //#region src/dbi-iterator.ts
1318
- const { ITERATOR_RESULT_DONE, ITERATOR_RESULT_FAST } = constants;
1326
+ // src/dbi-iterator.ts
1327
+ const { ITERATOR_RESULT_DONE, ITERATOR_RESULT_FAST, ITERATOR_RESULT_RELEASED } = constants;
1328
+ const transactionsToTrim = /* @__PURE__ */ new Set();
1329
+ function trimIdleIterators() {
1330
+ try {
1331
+ for (const transaction of transactionsToTrim) transaction.trimIterators();
1332
+ } finally {
1333
+ transactionsToTrim.clear();
1334
+ }
1335
+ }
1336
+ function scheduleTrim(transaction) {
1337
+ if (transaction !== void 0) {
1338
+ if (transactionsToTrim.size === 0) setImmediate(trimIdleIterators);
1339
+ transactionsToTrim.add(transaction);
1340
+ }
1341
+ }
1319
1342
  const DONE_RESULT = Object.freeze({
1320
1343
  done: true,
1321
1344
  value: void 0
@@ -1335,10 +1358,12 @@ var DBIterator = class {
1335
1358
  #includeValues;
1336
1359
  #limit;
1337
1360
  #yielded = 0;
1338
- constructor(iterator, store, includeValues, limit) {
1361
+ #transaction;
1362
+ constructor(iterator, store, includeValues, limit, transaction) {
1339
1363
  this.iterator = iterator;
1340
1364
  this.store = store;
1341
1365
  this.#includeValues = includeValues;
1366
+ this.#transaction = transaction;
1342
1367
  this.#limit = limit ?? void 0;
1343
1368
  }
1344
1369
  [Symbol.iterator]() {
@@ -1346,7 +1371,7 @@ var DBIterator = class {
1346
1371
  }
1347
1372
  next() {
1348
1373
  if (this.#limit !== void 0 && this.#yielded >= this.#limit) {
1349
- this.iterator.return?.();
1374
+ this.#release(this.iterator.return());
1350
1375
  return DONE_RESULT;
1351
1376
  }
1352
1377
  let result;
@@ -1354,11 +1379,15 @@ var DBIterator = class {
1354
1379
  result = this.iterator.next();
1355
1380
  } catch (err) {
1356
1381
  try {
1357
- this.iterator.return?.();
1382
+ this.#release(this.iterator.return());
1358
1383
  } catch {}
1359
1384
  throw err;
1360
1385
  }
1361
1386
  if (result === ITERATOR_RESULT_DONE) return DONE_RESULT;
1387
+ if (result === ITERATOR_RESULT_RELEASED) {
1388
+ scheduleTrim(this.#transaction);
1389
+ return DONE_RESULT;
1390
+ }
1362
1391
  this.#yielded++;
1363
1392
  const includeValues = this.#includeValues;
1364
1393
  const value = {};
@@ -1379,20 +1408,23 @@ var DBIterator = class {
1379
1408
  };
1380
1409
  }
1381
1410
  return(value) {
1382
- this.iterator.return?.();
1411
+ this.#release(this.iterator.return());
1383
1412
  return {
1384
1413
  done: true,
1385
1414
  value
1386
1415
  };
1387
1416
  }
1388
1417
  throw(err) {
1389
- this.iterator.throw?.(err);
1418
+ this.#release(this.iterator.throw(err));
1390
1419
  throw err;
1391
1420
  }
1421
+ #release(pooled) {
1422
+ if (pooled) scheduleTrim(this.#transaction);
1423
+ this.#transaction = void 0;
1424
+ }
1392
1425
  };
1393
1426
 
1394
- //#endregion
1395
- //#region src/encoding.ts
1427
+ // src/encoding.ts
1396
1428
  /**
1397
1429
  * Initializes the key encoder functions.
1398
1430
  *
@@ -1459,8 +1491,7 @@ function createFixedBuffer(size) {
1459
1491
  return buffer;
1460
1492
  }
1461
1493
 
1462
- //#endregion
1463
- //#region src/store.ts
1494
+ // src/store.ts
1464
1495
  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;
1465
1496
  const KEY_BUFFER_SIZE = 4096;
1466
1497
  const KEY_BUFFER = createFixedBuffer(KEY_BUFFER_SIZE);
@@ -1761,6 +1792,10 @@ var Store = class {
1761
1792
  */
1762
1793
  writeBufferSize;
1763
1794
  /**
1795
+ * The memtable arena block size in bytes; `0` or unset is automatic.
1796
+ */
1797
+ arenaBlockSize;
1798
+ /**
1764
1799
  * The function used to encode keys using the shared `keyBuffer`.
1765
1800
  */
1766
1801
  writeKey;
@@ -1813,6 +1848,7 @@ var Store = class {
1813
1848
  this.timestampFloorLog = options?.timestampFloorLog;
1814
1849
  this.verificationTable = options?.verificationTable;
1815
1850
  this.writeBufferSize = options?.writeBufferSize;
1851
+ this.arenaBlockSize = options?.arenaBlockSize;
1816
1852
  this.writeKey = writeKey;
1817
1853
  this.#columnFamilyOptions = options ? { ...options } : void 0;
1818
1854
  }
@@ -2063,7 +2099,9 @@ var Store = class {
2063
2099
  if (context !== this.db) flags |= ITERATOR_CONTEXT_IS_TRANSACTION_FLAG;
2064
2100
  if (txnId !== void 0) flags |= ITERATOR_HAS_TRANSACTION_ID_FLAG;
2065
2101
  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;
2066
- return new ExtendedIterable(new DBIterator(new NativeIterator(context, flags, startKeyEnd, endKeyStart, endKeyEnd, advancedOptions, txnId), this, includeValues, options?.limit));
2102
+ const nativeIterator = new NativeIterator(context, flags, startKeyEnd, endKeyStart, endKeyEnd, advancedOptions, txnId);
2103
+ const transaction = context !== this.db ? context : options?.transaction?._context;
2104
+ return new ExtendedIterable(new DBIterator(nativeIterator, this, includeValues, options?.limit, transaction));
2067
2105
  }
2068
2106
  getSync(context, key, alwaysCreateNewBuffer = false, options) {
2069
2107
  const keyParam = getKeyParam(this.encodeKey(key));
@@ -2177,7 +2215,8 @@ var Store = class {
2177
2215
  transactionLogsPath: this.transactionLogsPath,
2178
2216
  timestampFloorLog: this.timestampFloorLog,
2179
2217
  verificationTable: this.verificationTable,
2180
- writeBufferSize: this.writeBufferSize
2218
+ writeBufferSize: this.writeBufferSize,
2219
+ arenaBlockSize: this.arenaBlockSize
2181
2220
  });
2182
2221
  this.identityPath = this.db.identityPath;
2183
2222
  return false;
@@ -2283,8 +2322,7 @@ function getKeyParam(keyBuffer) {
2283
2322
  return keyBuffer.length;
2284
2323
  }
2285
2324
 
2286
- //#endregion
2287
- //#region src/transaction.ts
2325
+ // src/transaction.ts
2288
2326
  /**
2289
2327
  * Sentinel value returned by `commit()` when `coordinatedRetry: true` and the
2290
2328
  * transaction encountered an IsBusy conflict. The native layer parks on VT
@@ -2308,10 +2346,18 @@ var TransactionAlreadyAbortedError = class extends Error {
2308
2346
  */
2309
2347
  var TransactionRetryableError = class extends Error {
2310
2348
  hasLog;
2349
+ /**
2350
+ * The attempt was refused before writing its transaction-log batch (`transactionLogAbortMarkers`):
2351
+ * an older transaction held a write intent on one of its keys (`transactionLogWriteIntents`), or, on a retry, RocksDB's conflict
2352
+ * check found a write that already committed (see `lastConflictKind()`). Nothing of it is in the
2353
+ * log, so a retry may stage its batch afresh.
2354
+ */
2355
+ refusedBeforeLog;
2311
2356
  txn;
2312
2357
  constructor(error, txn) {
2313
2358
  super(error.message);
2314
2359
  this.hasLog = error.hasLog ?? false;
2360
+ this.refusedBeforeLog = error.refusedBeforeLog ?? false;
2315
2361
  this.txn = txn;
2316
2362
  }
2317
2363
  };
@@ -2341,6 +2387,38 @@ var TransactionAbandonedError = class extends Error {
2341
2387
  }
2342
2388
  };
2343
2389
  /**
2390
+ * `abort()` of a transaction whose transaction-log batch could not be marked aborted
2391
+ * (`transactionLogAbortMarkers`). The batch stays unresolved, so committed readers do not pass it and
2392
+ * the log store refuses appends, until a retry on the next append, database flush or store close
2393
+ * marks it.
2394
+ */
2395
+ var TransactionAbortUnresolvedError = class extends Error {
2396
+ code = "ERR_TRANSACTION_ABORT_UNRESOLVED";
2397
+ txn;
2398
+ constructor(error, txn) {
2399
+ super(error.message);
2400
+ this.txn = txn;
2401
+ }
2402
+ };
2403
+ /**
2404
+ * Why a transaction's most recent attempt was refused or retried, from {@link Transaction.lastConflictKind}.
2405
+ */
2406
+ const ConflictKind = {
2407
+ none: 0,
2408
+ /** Refused on an older transaction's write intent before writing its log batch. */
2409
+ intentBeforeLog: 1,
2410
+ /** Refused on an older transaction's write intent after its log batch was written. */
2411
+ intentAfterLog: 2,
2412
+ /** RocksDB found a conflict at commit. */
2413
+ atCommit: 3,
2414
+ /**
2415
+ * RocksDB's conflict check, run before the log write, found a write that already committed, or,
2416
+ * as at commit, could not validate because the memtable history it needs was flushed (needs a
2417
+ * RocksDB build that exports the check).
2418
+ */
2419
+ checkBeforeLog: 4
2420
+ };
2421
+ /**
2344
2422
  * Provides transaction level operations to a transaction callback.
2345
2423
  */
2346
2424
  var Transaction = class extends DBI {
@@ -2355,9 +2433,10 @@ var Transaction = class extends DBI {
2355
2433
  if (store.readOnly) {
2356
2434
  super(store);
2357
2435
  this.#txn = { id: 0 };
2358
- this.abandonWrites = this.abort = this.commitSync = this.setTimestamp = () => {};
2436
+ this.abandonWrites = this.abort = this.commitSync = this.setTimestamp = this.setParkDeadline = () => {};
2359
2437
  this.commit = async () => {};
2360
- this.getTimestamp = () => 0;
2438
+ this.getTimestamp = this.lastConflictKind = () => 0;
2439
+ this.logWritten = () => false;
2361
2440
  } else {
2362
2441
  const txn = new NativeTransaction(store.db, options);
2363
2442
  super(store, txn);
@@ -2383,6 +2462,7 @@ var Transaction = class extends DBI {
2383
2462
  this.#txn.abort();
2384
2463
  } catch (err) {
2385
2464
  if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABANDONED") throw new TransactionAbandonedError(err, this);
2465
+ if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABORT_UNRESOLVED") throw new TransactionAbortUnresolvedError(err, this);
2386
2466
  throw err;
2387
2467
  }
2388
2468
  }
@@ -2451,6 +2531,28 @@ var Transaction = class extends DBI {
2451
2531
  return this.#txn.getTimestamp();
2452
2532
  }
2453
2533
  /**
2534
+ * Whether this transaction's batch is in the transaction log. A commit conflict after the batch
2535
+ * was written keeps it for the retry, which must not stage it again; an attempt refused before
2536
+ * writing it (`refusedBeforeLog`) leaves nothing.
2537
+ */
2538
+ logWritten() {
2539
+ return this.#txn.logWritten();
2540
+ }
2541
+ /**
2542
+ * Why the most recent attempt was refused or retried; one of {@link ConflictKind}.
2543
+ */
2544
+ lastConflictKind() {
2545
+ return this.#txn.lastConflictKind();
2546
+ }
2547
+ /**
2548
+ * Bounds coordinated-retry parks: past `deadline` (epoch milliseconds) a parked commit resolves
2549
+ * `RETRY_NOW` at once, so a caller enforcing its own deadline can abort instead of waiting out
2550
+ * the default park bound. 0 clears it.
2551
+ */
2552
+ setParkDeadline(deadline) {
2553
+ this.#txn.setParkDeadline(deadline);
2554
+ }
2555
+ /**
2454
2556
  * Get the transaction id.
2455
2557
  */
2456
2558
  get id() {
@@ -2471,8 +2573,8 @@ var Transaction = class extends DBI {
2471
2573
  }
2472
2574
  };
2473
2575
 
2474
- //#endregion
2475
- //#region src/database.ts
2576
+ // src/database.ts
2577
+ const backoffCell = new Int32Array(new SharedArrayBuffer(4));
2476
2578
  const BACKGROUND_ERROR_SEVERITY_NAMES = [
2477
2579
  "none",
2478
2580
  "soft",
@@ -2882,6 +2984,19 @@ var RocksDatabase = class RocksDatabase extends DBI {
2882
2984
  return this.store.db.flushSync(options);
2883
2985
  }
2884
2986
  /**
2987
+ * Syncs the write-ahead log to stable storage (`DB::SyncWAL`), so every WAL-backed write
2988
+ * committed before the call survives a power loss afterwards. Unlike {@link flush} it writes
2989
+ * no SST file and never stalls writers. Not covered: writes that skipped the WAL
2990
+ * (`disableWAL`), and transaction-log files, which keep their own durability. Runs on the
2991
+ * libuv threadpool; RocksDB serializes concurrent syncs of one WAL, so keep one in flight
2992
+ * per database rather than parking pool workers on a queue of them.
2993
+ *
2994
+ * On a read-only database the returned promise resolves without syncing.
2995
+ */
2996
+ syncWal() {
2997
+ return new Promise((resolve, reject) => this.store.db.syncWal(resolve, reject));
2998
+ }
2999
+ /**
2885
3000
  * Returns the most recent {@link BackgroundError} observed on this database, or
2886
3001
  * `null` when none has occurred. The `'error'` event is the push counterpart;
2887
3002
  * this is the pull equivalent for on-demand checks (e.g. a health probe) and
@@ -3350,7 +3465,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
3350
3465
  async transaction(callback, options) {
3351
3466
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
3352
3467
  const maxRetries = options?.maxRetries ?? 3;
3353
- const txn = new Transaction(this.store, options);
3468
+ const txn = new Transaction(this.store, options?.retryOnBusy === false && options.coordinatedRetry ? {
3469
+ ...options,
3470
+ coordinatedRetry: false
3471
+ } : options);
3354
3472
  let result;
3355
3473
  this.notify("begin-transaction");
3356
3474
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -3364,7 +3482,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3364
3482
  commitResult = await txn.commit();
3365
3483
  } catch (commitErr) {
3366
3484
  if (commitErr instanceof TransactionAlreadyAbortedError) return;
3367
- if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? commitErr.hasLog) && attempt < maxRetries) continue;
3485
+ if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? (commitErr.hasLog || commitErr.refusedBeforeLog)) && attempt < maxRetries) continue;
3368
3486
  this.#abandonTransaction(txn, commitErr);
3369
3487
  return;
3370
3488
  }
@@ -3396,10 +3514,13 @@ var RocksDatabase = class RocksDatabase extends DBI {
3396
3514
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
3397
3515
  const maxRetries = options?.maxRetries ?? 3;
3398
3516
  const isRetryable = (err, attempt) => {
3399
- return err instanceof TransactionRetryableError && (options?.retryOnBusy ?? err.hasLog) && attempt <= maxRetries;
3517
+ if (!(err instanceof TransactionRetryableError && (options?.retryOnBusy ?? (err.hasLog || err.refusedBeforeLog)) && attempt <= maxRetries)) return false;
3518
+ const kind = txn.lastConflictKind();
3519
+ if (kind === ConflictKind.intentBeforeLog || kind === ConflictKind.intentAfterLog) Atomics.wait(backoffCell, 0, 0, Math.min(2 ** attempt, 50));
3520
+ return true;
3400
3521
  };
3522
+ const txn = new Transaction(this.store, options);
3401
3523
  const runAttempt = (attempt) => {
3402
- const txn = new Transaction(this.store, options);
3403
3524
  let result;
3404
3525
  try {
3405
3526
  result = callback(txn, attempt);
@@ -3415,7 +3536,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3415
3536
  if (isRetryable(commitErr, attempt)) return runAttempt(attempt + 1);
3416
3537
  this.#abandonTransaction(txn, commitErr);
3417
3538
  }
3418
- });
3539
+ }, (callbackErr) => this.#abortTransaction(txn, callbackErr));
3419
3540
  try {
3420
3541
  txn.commitSync();
3421
3542
  return result;
@@ -3440,7 +3561,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3440
3561
  try {
3441
3562
  txn.abort();
3442
3563
  } catch (abortErr) {
3443
- if (abortErr instanceof TransactionAbandonedError) throw abortErr;
3564
+ if (abortErr instanceof TransactionAbandonedError || abortErr instanceof TransactionAbortUnresolvedError) throw abortErr;
3444
3565
  }
3445
3566
  throw commitErr;
3446
3567
  }
@@ -3555,10 +3676,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
3555
3676
  }
3556
3677
  };
3557
3678
 
3558
- //#endregion
3559
- //#region src/parse-transaction-log.ts
3560
- const { TRANSACTION_LOG_TOKEN, TRANSACTION_LOG_ENTRY_HEADER_SIZE: TRANSACTION_LOG_ENTRY_HEADER_SIZE$1 } = constants;
3679
+ // src/parse-transaction-log.ts
3680
+ 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;
3561
3681
  const MIN_VALID_TIMESTAMP = Date.UTC(2026, 0, 27);
3682
+ const VALID_FLAGS_MASK = TRANSACTION_LOG_ENTRY_LAST_FLAG$1 | TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1;
3562
3683
  /**
3563
3684
  * Loads an entire transaction log file into memory.
3564
3685
  * @param path - The path to the transaction log file.
@@ -3596,6 +3717,8 @@ function parseTransactionLog(path, options = {}) {
3596
3717
  if (!Number.isFinite(timestamp) || timestamp < MIN_VALID_TIMESTAMP) anomalies.push(`Header timestamp ${timestamp} predates 2026-01-27 (possible corruption)`);
3597
3718
  const entries = [];
3598
3719
  let entryAnomalyCount = 0;
3720
+ let previousEndedTransaction = true;
3721
+ let previousTimestamp = NaN;
3599
3722
  while (fileOffset < size) {
3600
3723
  const timestamp = read(8).readDoubleBE(0);
3601
3724
  if (timestamp === 0) {
@@ -3612,7 +3735,11 @@ function parseTransactionLog(path, options = {}) {
3612
3735
  const data = read(length);
3613
3736
  const entryAnomalies = [];
3614
3737
  if (!Number.isFinite(timestamp) || timestamp < MIN_VALID_TIMESTAMP) entryAnomalies.push(`timestamp ${timestamp} predates 2026-01-27 (possible corruption)`);
3615
- if ((flags & -2) !== 0) entryAnomalies.push(`flags 0x${flags.toString(16).padStart(2, "0")} contains undefined bits (expected 0x00 or 0x01)`);
3738
+ if ((flags & ~VALID_FLAGS_MASK) !== 0) entryAnomalies.push(`flags 0x${flags.toString(16).padStart(2, "0")} contains undefined bits (expected 0x00-0x03)`);
3739
+ const startsTransaction = previousEndedTransaction || timestamp !== previousTimestamp;
3740
+ if ((flags & TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1) !== 0 && !startsTransaction) entryAnomalies.push("abort flag on an entry that does not start a transaction");
3741
+ previousEndedTransaction = (flags & TRANSACTION_LOG_ENTRY_LAST_FLAG$1) !== 0;
3742
+ previousTimestamp = timestamp;
3616
3743
  const entry = {
3617
3744
  timestamp,
3618
3745
  length,
@@ -3641,11 +3768,11 @@ function parseTransactionLog(path, options = {}) {
3641
3768
  }
3642
3769
  }
3643
3770
 
3644
- //#endregion
3645
- //#region src/transaction-log-reader.ts
3771
+ // src/transaction-log-reader.ts
3646
3772
  const FLOAT_TO_UINT32 = /* @__PURE__ */ new Float64Array(1);
3647
3773
  const UINT32_FROM_FLOAT = new Uint32Array(FLOAT_TO_UINT32.buffer);
3648
- const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE } = constants;
3774
+ const EXACT_START_SPAN = /* @__PURE__ */ new Float64Array(3);
3775
+ const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE, TRANSACTION_LOG_ENTRY_LAST_FLAG, TRANSACTION_LOG_ENTRY_ABORTED_FLAG } = constants;
3649
3776
  /**
3650
3777
  * Number of consecutive well-formed frames that, on their own, signal that real log data has
3651
3778
  * resumed after a framing break. Kept in step with `RESYNC_MIN_FRAMES` in
@@ -3681,6 +3808,32 @@ var CorruptFrameError = class extends RangeError {
3681
3808
  }
3682
3809
  };
3683
3810
  /**
3811
+ * Thrown when an iterator advances past segments it never read and that can no longer be read —
3812
+ * typically ones retention purged while the reader lagged. The entries they held cannot be
3813
+ * recovered from this log, so a consumer replicating it must not treat the stream as contiguous.
3814
+ * Missing sequence numbers prove only that continuity is unknown, not that each held entries.
3815
+ *
3816
+ * The iterator is left positioned at the start of `resumeLogId`, so a caller that accepts the
3817
+ * discontinuity calls `next()` again to continue from there.
3818
+ *
3819
+ * Not a `RangeError`: this is not broken framing, and consumers classify those by class.
3820
+ */
3821
+ var TransactionLogGapError = class extends Error {
3822
+ /** Sequence number of the last segment this iterator read up to. */
3823
+ logId;
3824
+ /** Sequence number of the segment iteration resumes in. */
3825
+ resumeLogId;
3826
+ /** Segments between `logId` and `resumeLogId` that could not be read. Empty segments are not counted. */
3827
+ unavailableLogCount;
3828
+ constructor(logId, resumeLogId, unavailableLogCount) {
3829
+ super(`Transaction log iterator skipped ${unavailableLogCount} unavailable segment(s) between log ${logId} and log ${resumeLogId}; entries in them were not read`);
3830
+ this.name = "TransactionLogGapError";
3831
+ this.logId = logId;
3832
+ this.resumeLogId = resumeLogId;
3833
+ this.unavailableLogCount = unavailableLogCount;
3834
+ }
3835
+ };
3836
+ /**
3684
3837
  * Whether a complete, in-bounds frame begins at `pos`. The only sane bound on an entry's length is
3685
3838
  * the written extent: a single entry can legitimately exceed the rotation threshold (the first
3686
3839
  * entry written to a fresh file is always written in full). A zero timestamp is the
@@ -3756,6 +3909,14 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3756
3909
  let dataView;
3757
3910
  let logBuffer = this._currentLogBuffer?.deref();
3758
3911
  let foundExactStart = false;
3912
+ let stopLogId = 0;
3913
+ let stopPosition = 0;
3914
+ let tailLogId = 0;
3915
+ let tailPosition = 0;
3916
+ let segmentRemovals = 0;
3917
+ let previousEndedTransaction = true;
3918
+ let previousTimestamp = NaN;
3919
+ let skippingAborted = false;
3759
3920
  if (start === void 0 && !startFromLastFlushed) {
3760
3921
  position = size;
3761
3922
  if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
@@ -3765,11 +3926,32 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3765
3926
  FLOAT_TO_UINT32[0] = this._getLastFlushed();
3766
3927
  if (FLOAT_TO_UINT32[0] === 0) FLOAT_TO_UINT32[0] = this._findPosition(0);
3767
3928
  start ??= 0;
3929
+ } else if (exactStart) {
3930
+ const startPosition = this._findExactStart(start, EXACT_START_SPAN);
3931
+ FLOAT_TO_UINT32[0] = EXACT_START_SPAN[0];
3932
+ stopLogId = UINT32_FROM_FLOAT[1];
3933
+ stopPosition = UINT32_FROM_FLOAT[0];
3934
+ FLOAT_TO_UINT32[0] = EXACT_START_SPAN[1];
3935
+ tailLogId = UINT32_FROM_FLOAT[1];
3936
+ tailPosition = UINT32_FROM_FLOAT[0];
3937
+ segmentRemovals = EXACT_START_SPAN[2];
3938
+ FLOAT_TO_UINT32[0] = startPosition;
3768
3939
  } else FLOAT_TO_UINT32[0] = this._findPosition(start);
3769
3940
  logId = UINT32_FROM_FLOAT[1];
3770
3941
  position = UINT32_FROM_FLOAT[0];
3771
3942
  if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3772
3943
  }
3944
+ const positionedAt = position;
3945
+ if (stopLogId !== 0 && pastStop(logId, position, stopLogId, stopPosition)) {
3946
+ stopLogId = 0;
3947
+ if (skipToTail(this, logId, position, tailLogId, tailPosition, segmentRemovals)) {
3948
+ logId = tailLogId;
3949
+ position = tailPosition;
3950
+ logBuffer = tailLogBuffer ?? logBuffer;
3951
+ }
3952
+ tailLogBuffer = void 0;
3953
+ }
3954
+ let gapFloor = logId;
3773
3955
  if (logBuffer === void 0 || logBuffer.logId !== logId) {
3774
3956
  logBuffer = getLogMemoryMap(this, logId);
3775
3957
  if (logBuffer && latestLogId === logId && !readUncommitted && this._currentLogBuffer?.deref() !== logBuffer) this._currentLogBuffer = new WeakRef(logBuffer);
@@ -3779,6 +3961,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3779
3961
  logBuffer.size = 0;
3780
3962
  logBuffer.dataView = new DataView(logBuffer.buffer);
3781
3963
  size = 0;
3964
+ if (positionedAt <= TRANSACTION_LOG_FILE_HEADER_SIZE || logId > 0 && this.getLogFileSize(logId) > 0) gapFloor = logId - 1;
3782
3965
  }
3783
3966
  }
3784
3967
  dataView = logBuffer.dataView;
@@ -3788,7 +3971,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3788
3971
  if (cachedSize === void 0) size = logBuffer.size = readableExtent(logBuffer);
3789
3972
  else size = cachedSize;
3790
3973
  }
3791
- return {
3974
+ const iterator = {
3792
3975
  [Symbol.iterator]() {
3793
3976
  return this;
3794
3977
  },
@@ -3800,13 +3983,17 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3800
3983
  if (latestLogId > logBuffer.logId) {
3801
3984
  size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3802
3985
  if (position >= size) {
3803
- const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3986
+ const fromLogId = logBuffer.logId;
3987
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor);
3804
3988
  if (nextLogBuffer) {
3989
+ const unavailable = unavailableLogCount;
3805
3990
  dataView = nextLogBuffer.dataView;
3806
3991
  logBuffer = nextLogBuffer;
3807
3992
  if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3808
3993
  else size = latestSize;
3809
3994
  position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3995
+ previousEndedTransaction = true;
3996
+ if (unavailable > 0) throw new TransactionLogGapError(Math.max(fromLogId, gapFloor), logBuffer.logId, unavailable);
3810
3997
  }
3811
3998
  }
3812
3999
  }
@@ -3826,30 +4013,55 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3826
4013
  if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE > limit) {
3827
4014
  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);
3828
4015
  position = broken.nextPosition;
4016
+ previousEndedTransaction = true;
3829
4017
  throw broken.error;
3830
4018
  }
3831
4019
  const length = dataView.getUint32(position + 8);
3832
4020
  if (length === 0 || position + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length > limit) {
3833
4021
  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);
3834
4022
  position = broken.nextPosition;
4023
+ previousEndedTransaction = true;
3835
4024
  throw broken.error;
3836
4025
  }
3837
4026
  position += TRANSACTION_LOG_ENTRY_HEADER_SIZE;
4027
+ const flags = logBuffer[position - 1];
4028
+ const aborted = (flags & TRANSACTION_LOG_ENTRY_ABORTED_FLAG) !== 0;
4029
+ if (previousEndedTransaction || timestamp !== previousTimestamp || aborted) skippingAborted = aborted;
4030
+ previousEndedTransaction = (flags & TRANSACTION_LOG_ENTRY_LAST_FLAG) !== 0;
4031
+ previousTimestamp = timestamp;
3838
4032
  let matchesRange;
3839
- if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
4033
+ if (skippingAborted) matchesRange = false;
4034
+ else if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
3840
4035
  else if (exactStart) {
4036
+ matchesRange = false;
3841
4037
  if (timestamp === start) {
3842
4038
  matchesRange = !exclusiveStart;
3843
4039
  foundExactStart = true;
3844
- } else matchesRange = false;
4040
+ stopLogId = 0;
4041
+ }
3845
4042
  } else matchesRange = (exclusiveStart ? timestamp > start : timestamp >= start) && timestamp < end;
4043
+ if (!matchesRange && stopLogId !== 0 && pastStop(logBuffer.logId, position + length, stopLogId, stopPosition)) {
4044
+ stopLogId = 0;
4045
+ position += length;
4046
+ if (skipToTail(transactionLog, logBuffer.logId, position, tailLogId, tailPosition, segmentRemovals)) {
4047
+ if (tailLogBuffer) {
4048
+ logBuffer = tailLogBuffer;
4049
+ dataView = tailLogBuffer.dataView;
4050
+ }
4051
+ position = tailPosition;
4052
+ previousEndedTransaction = true;
4053
+ size = 0;
4054
+ }
4055
+ tailLogBuffer = void 0;
4056
+ return iterator.next();
4057
+ }
3846
4058
  const entryStart = position;
3847
4059
  position += length;
3848
4060
  if (matchesRange) return {
3849
4061
  done: false,
3850
4062
  value: {
3851
4063
  timestamp,
3852
- endTxn: Boolean(logBuffer[entryStart - 1] & 1),
4064
+ endTxn: previousEndedTransaction,
3853
4065
  data: logBuffer.subarray(entryStart, position)
3854
4066
  }
3855
4067
  };
@@ -3860,11 +4072,13 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3860
4072
  size = logBuffer.size ?? readableExtent(logBuffer);
3861
4073
  if (!readUncommitted) logBuffer.size = size;
3862
4074
  if (position < size) continue;
3863
- const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
4075
+ const fromLogId = logBuffer.logId;
4076
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor);
3864
4077
  if (!nextLogBuffer) return {
3865
4078
  done: true,
3866
4079
  value: void 0
3867
4080
  };
4081
+ const unavailable = unavailableLogCount;
3868
4082
  logBuffer = nextLogBuffer;
3869
4083
  dataView = logBuffer.dataView;
3870
4084
  if (latestLogId === logBuffer.logId) size = latestSize;
@@ -3876,6 +4090,8 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3876
4090
  } else size = cachedSize;
3877
4091
  }
3878
4092
  position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4093
+ previousEndedTransaction = true;
4094
+ if (unavailable > 0) throw new TransactionLogGapError(Math.max(fromLogId, gapFloor), logBuffer.logId, unavailable);
3879
4095
  }
3880
4096
  }
3881
4097
  }
@@ -3885,7 +4101,29 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3885
4101
  };
3886
4102
  }
3887
4103
  };
4104
+ return iterator;
3888
4105
  } });
4106
+ function pastStop(logId, position, stopLogId, stopPosition) {
4107
+ return logId > stopLogId || logId === stopLogId && position >= stopPosition;
4108
+ }
4109
+ let tailLogBuffer;
4110
+ /**
4111
+ * Whether an exact-start lookup at `position` of `fromLogId` may take its one-shot jump to the tail.
4112
+ * next() calls it on a frame boundary, so a mapping error leaves a resumable cursor; from query()
4113
+ * the error escapes before any iterator exists. The removal
4114
+ * check follows the mapping and holds the store's lock, so a purge either declines the jump (the
4115
+ * scan then reports the gap) or follows it like a purge of segments already read.
4116
+ */
4117
+ function skipToTail(transactionLog, fromLogId, position, tailLogId, tailPosition, segmentRemovals) {
4118
+ tailLogBuffer = void 0;
4119
+ if (tailLogId === fromLogId) return tailPosition > position;
4120
+ if (tailLogId < fromLogId) return false;
4121
+ const mapped = getLogMemoryMap(transactionLog, tailLogId);
4122
+ if (mapped === void 0 || transactionLog._segmentRemovals() !== segmentRemovals) return false;
4123
+ tailLogBuffer = mapped;
4124
+ return true;
4125
+ }
4126
+ let unavailableLogCount = 0;
3889
4127
  /**
3890
4128
  * Maps the next readable segment after `fromLogId`, skipping a run retention has deleted —
3891
4129
  * stopping at the hole wedges the iterator there for every later poll (invariant 30).
@@ -3894,15 +4132,22 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3894
4132
  * `_findPosition(0)`, which names the bottom of the contiguous run ending at the current segment
3895
4133
  * and so lands *past* a survivor sitting between two holes.
3896
4134
  */
3897
- function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId) {
4135
+ function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor) {
4136
+ unavailableLogCount = 0;
3898
4137
  let candidateLogId = fromLogId + 1;
3899
4138
  while (candidateLogId <= latestLogId) {
3900
4139
  const logBuffer = getLogMemoryMap(transactionLog, candidateLogId);
3901
4140
  if (logBuffer) {
3902
4141
  if (candidateLogId === latestLogId || readableExtent(logBuffer) > TRANSACTION_LOG_FILE_HEADER_SIZE) return logBuffer;
3903
- } else if (transactionLog.getLogFileSize(candidateLogId) > TRANSACTION_LOG_FILE_HEADER_SIZE) return;
3904
- candidateLogId = transactionLog._nextLogId(candidateLogId);
3905
- if (candidateLogId === 0) return;
4142
+ } else {
4143
+ const fileSize = transactionLog.getLogFileSize(candidateLogId);
4144
+ if (fileSize > TRANSACTION_LOG_FILE_HEADER_SIZE) return;
4145
+ if (fileSize === 0 && candidateLogId > gapFloor) unavailableLogCount++;
4146
+ }
4147
+ const nextLogId = transactionLog._nextLogId(candidateLogId);
4148
+ if (nextLogId === 0) return;
4149
+ unavailableLogCount += Math.max(0, nextLogId - 1 - Math.max(candidateLogId, gapFloor));
4150
+ candidateLogId = nextLogId;
3906
4151
  }
3907
4152
  }
3908
4153
  /**
@@ -3956,13 +4201,11 @@ function loadLastPosition(transactionLog, readUncommitted) {
3956
4201
  };
3957
4202
  }
3958
4203
 
3959
- //#endregion
3960
- //#region src/index.ts
4204
+ // src/index.ts
3961
4205
  const versions = {
3962
4206
  rocksdb: version,
3963
- "rocksdb-js": "2.12.0"
4207
+ "rocksdb-js": "2.13.0"
3964
4208
  };
3965
4209
 
3966
- //#endregion
3967
- export { BackgroundError, CorruptFrameError, CountEstimator, DBI, DBIterator, RocksDatabase, Store, Transaction, TransactionLog, backups, constants, coolTransactionLogs, currentThreadId, fileLockRelease, getRegistryStatus, getWriteBufferManagerStats, parseTransactionLog, registryStatus, shutdown, stats, steadyClockNow, supportedCompression, tryFileLock, validateTransactionLogStore, versions };
4210
+ export { BackgroundError, ConflictKind, CorruptFrameError, CountEstimator, DBI, DBIterator, RocksDatabase, Store, Transaction, TransactionAbortUnresolvedError, TransactionLog, TransactionLogGapError, backups, constants, coolTransactionLogs, currentThreadId, fileLockRelease, getRegistryStatus, getWriteBufferManagerStats, parseTransactionLog, registryStatus, shutdown, stats, steadyClockNow, supportedCompression, tryFileLock, validateTransactionLogStore, versions };
3968
4211
  //# sourceMappingURL=index.mjs.map