@harperfast/rocksdb-js 2.12.0 → 2.14.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,19 +25,19 @@ 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");
32
31
  let node_path = require("node:path");
33
32
  let node_url = require("node:url");
33
+ let node_worker_threads = require("node:worker_threads");
34
34
  let node_fs_promises = require("node:fs/promises");
35
35
  let ordered_binary = require("ordered-binary");
36
36
  ordered_binary = __toESM(ordered_binary, 1);
37
37
  let _harperfast_extended_iterable = require("@harperfast/extended-iterable");
38
38
  let msgpackr = require("msgpackr");
39
39
 
40
- //#region src/load-binding.ts
40
+ // src/load-binding.ts
41
41
  const nativeExtRE = /\.node$/;
42
42
  const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
43
43
  /**
@@ -80,6 +80,18 @@ function locateBinding() {
80
80
  throw new Error("Unable to locate rocksdb-js native binding");
81
81
  }
82
82
  const binding = req(locateBinding());
83
+ if (node_worker_threads.isMainThread) {
84
+ let exitShutdownRan = false;
85
+ const shutdownOnExit = () => {
86
+ if (exitShutdownRan) return;
87
+ exitShutdownRan = true;
88
+ if (!binding.shutdownOnExit() && !process.exitCode) process.exitCode = 1;
89
+ };
90
+ process.on("exit", shutdownOnExit);
91
+ process.on("uncaughtExceptionMonitor", () => {
92
+ if (process._exiting) shutdownOnExit();
93
+ });
94
+ }
83
95
  /**
84
96
  * The native `BackgroundError` constructor (a real `Error` subclass). Exported
85
97
  * as both a value — for `err instanceof BackgroundError` — and, via declaration
@@ -142,7 +154,7 @@ const getRegistryStatus = binding.registryStatus;
142
154
  */
143
155
  const registryStatus = getRegistryStatus;
144
156
  /**
145
- * Flushes every open database and waits for outstanding compactions, then
157
+ * Flushes every open database and cancels its outstanding compactions, then
146
158
  * releases the registry.
147
159
  *
148
160
  * **Throws** the first failure it encounters: a close whose native teardown
@@ -152,19 +164,10 @@ const registryStatus = getRegistryStatus;
152
164
  * `database:closeFailed`; a timeout has no event, so the throw is its only
153
165
  * signal.
154
166
  *
155
- * A `process.on('exit')` listener must therefore wrap it: an exception thrown
156
- * from an `exit` listener skips every `exit` listener registered after it (and
157
- * sets exit code 1 unless an `uncaughtException` handler is installed).
158
- *
159
- * ```typescript
160
- * process.on('exit', () => {
161
- * try {
162
- * shutdown();
163
- * } catch (error) {
164
- * console.error('rocksdb-js shutdown failed', error);
165
- * }
166
- * });
167
- * ```
167
+ * You do not need to call it at exit: when rocksdb-js loads on the main
168
+ * thread it registers a `process.on('exit')` listener that does the same and
169
+ * reports failures to stderr and the exit code instead of throwing (see the
170
+ * `shutdownOnExit` config option). Calling it yourself as well is safe.
168
171
  */
169
172
  const shutdown = binding.shutdown;
170
173
  const currentThreadId = binding.currentThreadId;
@@ -172,7 +175,7 @@ const currentThreadId = binding.currentThreadId;
172
175
  * Reads the process-wide steady clock: `std::chrono::steady_clock` as
173
176
  * fractional milliseconds from an unspecified origin that is fixed for the life
174
177
  * 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
178
+ * `worker_threads` worker, whenever started or restarted, on Node and Bun
176
179
  * — is in one domain, and the difference between two samples is real elapsed
177
180
  * time unaffected by wall-clock steps. Non-decreasing but not unique (two
178
181
  * samples may be equal; treat `==` as "not later"). Windows QPC samples from
@@ -184,6 +187,30 @@ const currentThreadId = binding.currentThreadId;
184
187
  */
185
188
  const steadyClockNow = binding.steadyClockNow;
186
189
  /**
190
+ * Raises the process-wide monotonic timestamp clock — the one behind
191
+ * `db.getMonotonicTimestamp()`, every transaction's initial timestamp and so
192
+ * every transaction-log batch key — to at least `timestamp` (Unix epoch
193
+ * milliseconds). Every timestamp the process issues after the call is strictly
194
+ * greater than `timestamp`, across every database and `worker_threads` worker.
195
+ * Timestamps already issued and timestamps adopted with `txn.setTimestamp()`
196
+ * are unaffected. No open database is needed.
197
+ *
198
+ * Use it to carry a floor the caller persisted across a restart, which a
199
+ * backward wall-clock step would otherwise let the new process reissue. The
200
+ * floor is in memory only and is not a fence: restore it before the process
201
+ * starts any transaction it originates, and pass only floors this node
202
+ * persisted itself, since the clock stays that far ahead until wall time
203
+ * catches up.
204
+ *
205
+ * @returns `true` if the clock was raised: `timestamp` was ahead of both the
206
+ * wall clock and every timestamp already issued. `false` if the clock was
207
+ * already at or above it.
208
+ * @throws {TypeError} If `timestamp` is not a number.
209
+ * @throws {RangeError} If `timestamp` is not finite, is not greater than 0, is
210
+ * not less than 8.64e15, or is more than ten years ahead of the wall clock.
211
+ */
212
+ const raiseTimestampFloor = binding.raiseTimestampFloor;
213
+ /**
187
214
  * Advises the kernel that the file-backed pages of every mapped transaction log
188
215
  * are cold (Linux MADV_COLD), so they are reclaimed first under memory pressure
189
216
  * without being freed — useful during replication catch-up, where a full read of
@@ -210,12 +237,30 @@ const transactionLogMapCount = binding.transactionLogMapCount;
210
237
  */
211
238
  const lockWakeCallbackCount = binding.lockWakeCallbackCount;
212
239
  /**
240
+ * The calling thread's counters for the per-transaction iterator pool: pooled
241
+ * iterators created, and idle ones reused by a later range. Internal — used by
242
+ * tests to prove a range was served by a reused iterator.
243
+ */
244
+ const iteratorPoolStats = binding.iteratorPoolStats;
245
+ /**
246
+ * Whether transaction `getRange()` reuses pooled native iterators (off with
247
+ * `ROCKSDB_JS_ITERATOR_POOL=0`). Internal.
248
+ */
249
+ const iteratorPoolEnabled = binding.iteratorPoolEnabled;
250
+ /**
213
251
  * Test-only: force the next `count` transaction commits to fail with TryAgain (rolled back, so
214
252
  * no data is committed), reproducing a stranded-snapshot conflict deterministically. Pass 0 to
215
253
  * disarm. Used by the ERR_TRY_AGAIN retry regression test.
216
254
  */
217
255
  const forceTryAgainForTesting = binding.forceTryAgainForTesting;
218
256
  /**
257
+ * Test-only: turn the RocksDB conflict check before the transaction-log write off (`true`) or back
258
+ * on, so a test can make a logged batch conflict at commit from a stale snapshot.
259
+ */
260
+ const disablePrelogConflictCheckForTesting = binding.disablePrelogConflictCheckForTesting;
261
+ /** Test-only: run the pre-log conflict check on retries alone (`true`, the default) or on every attempt. */
262
+ const setPrelogConflictCheckRetriesOnlyForTesting = binding.setPrelogConflictCheckRetriesOnlyForTesting;
263
+ /**
219
264
  * Test-only seam: makes the physical column-family drop report failure.
220
265
  * `0` = inert, `1` = fail without dropping (the retry performs the drop),
221
266
  * `2` = drop for real and then report failure (the retry resolves as
@@ -258,8 +303,7 @@ const nativeValidateTransactionLog = binding.validateTransactionLog;
258
303
  const stats = binding.stats;
259
304
  const version = binding.version;
260
305
 
261
- //#endregion
262
- //#region src/validate-transaction-log.ts
306
+ // src/validate-transaction-log.ts
263
307
  /**
264
308
  * Validates a transaction log store directory: every `<sequence>.txnlog` file
265
309
  * is checked for a valid header (token, version) and intact v1 entry framing
@@ -299,8 +343,7 @@ function validateTransactionLogStore(path, options) {
299
343
  return new Promise((resolve, reject) => nativeValidateTransactionLog(resolve, reject, path, options?.strict ?? false));
300
344
  }
301
345
 
302
- //#endregion
303
- //#region src/backup.ts
346
+ // src/backup.ts
304
347
  /** Subdirectory (under a backup directory) holding per-backup transaction log snapshots. */
305
348
  const TRANSACTION_LOGS_DIRNAME = "transaction_logs";
306
349
  /** Non-blocking existence check (`fs.existsSync` would block the event loop). */
@@ -584,8 +627,7 @@ const backups = {
584
627
  }
585
628
  };
586
629
 
587
- //#endregion
588
- //#region src/count-estimator.ts
630
+ // src/count-estimator.ts
589
631
  /**
590
632
  * Below this many traversed entries the observed density is too noisy to
591
633
  * calibrate with, so `estimate()` returns the raw statistical estimate.
@@ -728,8 +770,7 @@ var CountEstimator = class {
728
770
  }
729
771
  };
730
772
 
731
- //#endregion
732
- //#region src/util.ts
773
+ // src/util.ts
733
774
  /**
734
775
  * Parses a duration string into milliseconds.
735
776
  *
@@ -800,8 +841,7 @@ function when(subject, callback, errback) {
800
841
  }
801
842
  }
802
843
 
803
- //#endregion
804
- //#region src/dbi.ts
844
+ // src/dbi.ts
805
845
  /**
806
846
  * The base class for all database operations. This base class is shared by
807
847
  * `RocksDatabase` and `Transaction`.
@@ -1086,8 +1126,7 @@ var DBI = class DBI {
1086
1126
  }
1087
1127
  };
1088
1128
 
1089
- //#endregion
1090
- //#region src/tar.ts
1129
+ // src/tar.ts
1091
1130
  /**
1092
1131
  * A minimal, dependency-free streaming USTAR (POSIX tar) encoder.
1093
1132
  *
@@ -1293,8 +1332,7 @@ var TarEncoder = class {
1293
1332
  }
1294
1333
  };
1295
1334
 
1296
- //#endregion
1297
- //#region src/backup-stream.ts
1335
+ // src/backup-stream.ts
1298
1336
  /** Native event discriminator: a new file header (vs. a payload chunk). */
1299
1337
  const EVENT_FILE = 0;
1300
1338
  /**
@@ -1338,9 +1376,22 @@ async function backupToStream(db, stream, options) {
1338
1376
  }
1339
1377
  }
1340
1378
 
1341
- //#endregion
1342
- //#region src/dbi-iterator.ts
1343
- const { ITERATOR_RESULT_DONE, ITERATOR_RESULT_FAST } = constants;
1379
+ // src/dbi-iterator.ts
1380
+ const { ITERATOR_RESULT_DONE, ITERATOR_RESULT_FAST, ITERATOR_RESULT_RELEASED } = constants;
1381
+ const transactionsToTrim = /* @__PURE__ */ new Set();
1382
+ function trimIdleIterators() {
1383
+ try {
1384
+ for (const transaction of transactionsToTrim) transaction.trimIterators();
1385
+ } finally {
1386
+ transactionsToTrim.clear();
1387
+ }
1388
+ }
1389
+ function scheduleTrim(transaction) {
1390
+ if (transaction !== void 0) {
1391
+ if (transactionsToTrim.size === 0) setImmediate(trimIdleIterators);
1392
+ transactionsToTrim.add(transaction);
1393
+ }
1394
+ }
1344
1395
  const DONE_RESULT = Object.freeze({
1345
1396
  done: true,
1346
1397
  value: void 0
@@ -1360,10 +1411,12 @@ var DBIterator = class {
1360
1411
  #includeValues;
1361
1412
  #limit;
1362
1413
  #yielded = 0;
1363
- constructor(iterator, store, includeValues, limit) {
1414
+ #transaction;
1415
+ constructor(iterator, store, includeValues, limit, transaction) {
1364
1416
  this.iterator = iterator;
1365
1417
  this.store = store;
1366
1418
  this.#includeValues = includeValues;
1419
+ this.#transaction = transaction;
1367
1420
  this.#limit = limit ?? void 0;
1368
1421
  }
1369
1422
  [Symbol.iterator]() {
@@ -1371,7 +1424,7 @@ var DBIterator = class {
1371
1424
  }
1372
1425
  next() {
1373
1426
  if (this.#limit !== void 0 && this.#yielded >= this.#limit) {
1374
- this.iterator.return?.();
1427
+ this.#release(this.iterator.return());
1375
1428
  return DONE_RESULT;
1376
1429
  }
1377
1430
  let result;
@@ -1379,11 +1432,15 @@ var DBIterator = class {
1379
1432
  result = this.iterator.next();
1380
1433
  } catch (err) {
1381
1434
  try {
1382
- this.iterator.return?.();
1435
+ this.#release(this.iterator.return());
1383
1436
  } catch {}
1384
1437
  throw err;
1385
1438
  }
1386
1439
  if (result === ITERATOR_RESULT_DONE) return DONE_RESULT;
1440
+ if (result === ITERATOR_RESULT_RELEASED) {
1441
+ scheduleTrim(this.#transaction);
1442
+ return DONE_RESULT;
1443
+ }
1387
1444
  this.#yielded++;
1388
1445
  const includeValues = this.#includeValues;
1389
1446
  const value = {};
@@ -1404,20 +1461,23 @@ var DBIterator = class {
1404
1461
  };
1405
1462
  }
1406
1463
  return(value) {
1407
- this.iterator.return?.();
1464
+ this.#release(this.iterator.return());
1408
1465
  return {
1409
1466
  done: true,
1410
1467
  value
1411
1468
  };
1412
1469
  }
1413
1470
  throw(err) {
1414
- this.iterator.throw?.(err);
1471
+ this.#release(this.iterator.throw(err));
1415
1472
  throw err;
1416
1473
  }
1474
+ #release(pooled) {
1475
+ if (pooled) scheduleTrim(this.#transaction);
1476
+ this.#transaction = void 0;
1477
+ }
1417
1478
  };
1418
1479
 
1419
- //#endregion
1420
- //#region src/encoding.ts
1480
+ // src/encoding.ts
1421
1481
  /**
1422
1482
  * Initializes the key encoder functions.
1423
1483
  *
@@ -1484,8 +1544,7 @@ function createFixedBuffer(size) {
1484
1544
  return buffer;
1485
1545
  }
1486
1546
 
1487
- //#endregion
1488
- //#region src/store.ts
1547
+ // src/store.ts
1489
1548
  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
1549
  const KEY_BUFFER_SIZE = 4096;
1491
1550
  const KEY_BUFFER = createFixedBuffer(KEY_BUFFER_SIZE);
@@ -1786,6 +1845,14 @@ var Store = class {
1786
1845
  */
1787
1846
  writeBufferSize;
1788
1847
  /**
1848
+ * The memtable arena block size in bytes; `0` or unset is automatic.
1849
+ */
1850
+ arenaBlockSize;
1851
+ /**
1852
+ * Bits per key of the whole-key filter in each SST; `0` disables filters.
1853
+ */
1854
+ bloomFilterBitsPerKey;
1855
+ /**
1789
1856
  * The function used to encode keys using the shared `keyBuffer`.
1790
1857
  */
1791
1858
  writeKey;
@@ -1838,6 +1905,8 @@ var Store = class {
1838
1905
  this.timestampFloorLog = options?.timestampFloorLog;
1839
1906
  this.verificationTable = options?.verificationTable;
1840
1907
  this.writeBufferSize = options?.writeBufferSize;
1908
+ this.arenaBlockSize = options?.arenaBlockSize;
1909
+ this.bloomFilterBitsPerKey = options?.bloomFilterBitsPerKey;
1841
1910
  this.writeKey = writeKey;
1842
1911
  this.#columnFamilyOptions = options ? { ...options } : void 0;
1843
1912
  }
@@ -1997,7 +2066,7 @@ var Store = class {
1997
2066
  const result = context.getSync(keyParam, flags | ONLY_IF_IN_MEMORY_CACHE_FLAG, txnId, expectedVersion);
1998
2067
  if (typeof result === "number") {
1999
2068
  if (result === NOT_IN_MEMORY_CACHE_FLAG) return new Promise((resolve, reject) => {
2000
- context.get(keyParam, resolve, reject, txnId, expectedVersion);
2069
+ context.get(keyParam, resolve, reject, txnId, expectedVersion, flags);
2001
2070
  });
2002
2071
  if (result === FRESH_VERSION_FLAG) return result;
2003
2072
  VALUE_BUFFER.end = result;
@@ -2088,7 +2157,9 @@ var Store = class {
2088
2157
  if (context !== this.db) flags |= ITERATOR_CONTEXT_IS_TRANSACTION_FLAG;
2089
2158
  if (txnId !== void 0) flags |= ITERATOR_HAS_TRANSACTION_ID_FLAG;
2090
2159
  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));
2160
+ const nativeIterator = new NativeIterator(context, flags, startKeyEnd, endKeyStart, endKeyEnd, advancedOptions, txnId);
2161
+ const transaction = context !== this.db ? context : options?.transaction?._context;
2162
+ return new _harperfast_extended_iterable.ExtendedIterable(new DBIterator(nativeIterator, this, includeValues, options?.limit, transaction));
2092
2163
  }
2093
2164
  getSync(context, key, alwaysCreateNewBuffer = false, options) {
2094
2165
  const keyParam = getKeyParam(this.encodeKey(key));
@@ -2202,7 +2273,9 @@ var Store = class {
2202
2273
  transactionLogsPath: this.transactionLogsPath,
2203
2274
  timestampFloorLog: this.timestampFloorLog,
2204
2275
  verificationTable: this.verificationTable,
2205
- writeBufferSize: this.writeBufferSize
2276
+ writeBufferSize: this.writeBufferSize,
2277
+ arenaBlockSize: this.arenaBlockSize,
2278
+ bloomFilterBitsPerKey: this.bloomFilterBitsPerKey
2206
2279
  });
2207
2280
  this.identityPath = this.db.identityPath;
2208
2281
  return false;
@@ -2308,8 +2381,7 @@ function getKeyParam(keyBuffer) {
2308
2381
  return keyBuffer.length;
2309
2382
  }
2310
2383
 
2311
- //#endregion
2312
- //#region src/transaction.ts
2384
+ // src/transaction.ts
2313
2385
  /**
2314
2386
  * Sentinel value returned by `commit()` when `coordinatedRetry: true` and the
2315
2387
  * transaction encountered an IsBusy conflict. The native layer parks on VT
@@ -2333,10 +2405,18 @@ var TransactionAlreadyAbortedError = class extends Error {
2333
2405
  */
2334
2406
  var TransactionRetryableError = class extends Error {
2335
2407
  hasLog;
2408
+ /**
2409
+ * The attempt was refused before writing its transaction-log batch (`transactionLogAbortMarkers`):
2410
+ * an older transaction held a write intent on one of its keys (`transactionLogWriteIntents`), or, on a retry, RocksDB's conflict
2411
+ * check found a write that already committed (see `lastConflictKind()`). Nothing of it is in the
2412
+ * log, so a retry may stage its batch afresh.
2413
+ */
2414
+ refusedBeforeLog;
2336
2415
  txn;
2337
2416
  constructor(error, txn) {
2338
2417
  super(error.message);
2339
2418
  this.hasLog = error.hasLog ?? false;
2419
+ this.refusedBeforeLog = error.refusedBeforeLog ?? false;
2340
2420
  this.txn = txn;
2341
2421
  }
2342
2422
  };
@@ -2366,6 +2446,38 @@ var TransactionAbandonedError = class extends Error {
2366
2446
  }
2367
2447
  };
2368
2448
  /**
2449
+ * `abort()` of a transaction whose transaction-log batch could not be marked aborted
2450
+ * (`transactionLogAbortMarkers`). The batch stays unresolved, so committed readers do not pass it and
2451
+ * the log store refuses appends, until a retry on the next append, database flush or store close
2452
+ * marks it.
2453
+ */
2454
+ var TransactionAbortUnresolvedError = class extends Error {
2455
+ code = "ERR_TRANSACTION_ABORT_UNRESOLVED";
2456
+ txn;
2457
+ constructor(error, txn) {
2458
+ super(error.message);
2459
+ this.txn = txn;
2460
+ }
2461
+ };
2462
+ /**
2463
+ * Why a transaction's most recent attempt was refused or retried, from {@link Transaction.lastConflictKind}.
2464
+ */
2465
+ const ConflictKind = {
2466
+ none: 0,
2467
+ /** Refused on an older transaction's write intent before writing its log batch. */
2468
+ intentBeforeLog: 1,
2469
+ /** Refused on an older transaction's write intent after its log batch was written. */
2470
+ intentAfterLog: 2,
2471
+ /** RocksDB found a conflict at commit. */
2472
+ atCommit: 3,
2473
+ /**
2474
+ * RocksDB's conflict check, run before the log write, found a write that already committed, or,
2475
+ * as at commit, could not validate because the memtable history it needs was flushed (needs a
2476
+ * RocksDB build that exports the check).
2477
+ */
2478
+ checkBeforeLog: 4
2479
+ };
2480
+ /**
2369
2481
  * Provides transaction level operations to a transaction callback.
2370
2482
  */
2371
2483
  var Transaction = class extends DBI {
@@ -2380,9 +2492,10 @@ var Transaction = class extends DBI {
2380
2492
  if (store.readOnly) {
2381
2493
  super(store);
2382
2494
  this.#txn = { id: 0 };
2383
- this.abandonWrites = this.abort = this.commitSync = this.setTimestamp = () => {};
2495
+ this.abandonWrites = this.abort = this.commitSync = this.setTimestamp = this.setParkDeadline = () => {};
2384
2496
  this.commit = async () => {};
2385
- this.getTimestamp = () => 0;
2497
+ this.getTimestamp = this.lastConflictKind = () => 0;
2498
+ this.logWritten = () => false;
2386
2499
  } else {
2387
2500
  const txn = new NativeTransaction(store.db, options);
2388
2501
  super(store, txn);
@@ -2408,6 +2521,7 @@ var Transaction = class extends DBI {
2408
2521
  this.#txn.abort();
2409
2522
  } catch (err) {
2410
2523
  if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABANDONED") throw new TransactionAbandonedError(err, this);
2524
+ if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABORT_UNRESOLVED") throw new TransactionAbortUnresolvedError(err, this);
2411
2525
  throw err;
2412
2526
  }
2413
2527
  }
@@ -2476,6 +2590,28 @@ var Transaction = class extends DBI {
2476
2590
  return this.#txn.getTimestamp();
2477
2591
  }
2478
2592
  /**
2593
+ * Whether this transaction's batch is in the transaction log. A commit conflict after the batch
2594
+ * was written keeps it for the retry, which must not stage it again; an attempt refused before
2595
+ * writing it (`refusedBeforeLog`) leaves nothing.
2596
+ */
2597
+ logWritten() {
2598
+ return this.#txn.logWritten();
2599
+ }
2600
+ /**
2601
+ * Why the most recent attempt was refused or retried; one of {@link ConflictKind}.
2602
+ */
2603
+ lastConflictKind() {
2604
+ return this.#txn.lastConflictKind();
2605
+ }
2606
+ /**
2607
+ * Bounds coordinated-retry parks: past `deadline` (epoch milliseconds) a parked commit resolves
2608
+ * `RETRY_NOW` at once, so a caller enforcing its own deadline can abort instead of waiting out
2609
+ * the default park bound. 0 clears it.
2610
+ */
2611
+ setParkDeadline(deadline) {
2612
+ this.#txn.setParkDeadline(deadline);
2613
+ }
2614
+ /**
2479
2615
  * Get the transaction id.
2480
2616
  */
2481
2617
  get id() {
@@ -2496,8 +2632,8 @@ var Transaction = class extends DBI {
2496
2632
  }
2497
2633
  };
2498
2634
 
2499
- //#endregion
2500
- //#region src/database.ts
2635
+ // src/database.ts
2636
+ const backoffCell = new Int32Array(new SharedArrayBuffer(4));
2501
2637
  const BACKGROUND_ERROR_SEVERITY_NAMES = [
2502
2638
  "none",
2503
2639
  "soft",
@@ -2907,6 +3043,19 @@ var RocksDatabase = class RocksDatabase extends DBI {
2907
3043
  return this.store.db.flushSync(options);
2908
3044
  }
2909
3045
  /**
3046
+ * Syncs the write-ahead log to stable storage (`DB::SyncWAL`), so every WAL-backed write
3047
+ * committed before the call survives a power loss afterwards. Unlike {@link flush} it writes
3048
+ * no SST file and never stalls writers. Not covered: writes that skipped the WAL
3049
+ * (`disableWAL`), and transaction-log files, which keep their own durability. Runs on the
3050
+ * libuv threadpool; RocksDB serializes concurrent syncs of one WAL, so keep one in flight
3051
+ * per database rather than parking pool workers on a queue of them.
3052
+ *
3053
+ * On a read-only database the returned promise resolves without syncing.
3054
+ */
3055
+ syncWal() {
3056
+ return new Promise((resolve, reject) => this.store.db.syncWal(resolve, reject));
3057
+ }
3058
+ /**
2910
3059
  * Returns the most recent {@link BackgroundError} observed on this database, or
2911
3060
  * `null` when none has occurred. The `'error'` event is the push counterpart;
2912
3061
  * this is the pull equivalent for on-demand checks (e.g. a health probe) and
@@ -3375,7 +3524,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
3375
3524
  async transaction(callback, options) {
3376
3525
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
3377
3526
  const maxRetries = options?.maxRetries ?? 3;
3378
- const txn = new Transaction(this.store, options);
3527
+ const txn = new Transaction(this.store, options?.retryOnBusy === false && options.coordinatedRetry ? {
3528
+ ...options,
3529
+ coordinatedRetry: false
3530
+ } : options);
3379
3531
  let result;
3380
3532
  this.notify("begin-transaction");
3381
3533
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -3389,7 +3541,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3389
3541
  commitResult = await txn.commit();
3390
3542
  } catch (commitErr) {
3391
3543
  if (commitErr instanceof TransactionAlreadyAbortedError) return;
3392
- if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? commitErr.hasLog) && attempt < maxRetries) continue;
3544
+ if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? (commitErr.hasLog || commitErr.refusedBeforeLog)) && attempt < maxRetries) continue;
3393
3545
  this.#abandonTransaction(txn, commitErr);
3394
3546
  return;
3395
3547
  }
@@ -3421,10 +3573,13 @@ var RocksDatabase = class RocksDatabase extends DBI {
3421
3573
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
3422
3574
  const maxRetries = options?.maxRetries ?? 3;
3423
3575
  const isRetryable = (err, attempt) => {
3424
- return err instanceof TransactionRetryableError && (options?.retryOnBusy ?? err.hasLog) && attempt <= maxRetries;
3576
+ if (!(err instanceof TransactionRetryableError && (options?.retryOnBusy ?? (err.hasLog || err.refusedBeforeLog)) && attempt <= maxRetries)) return false;
3577
+ const kind = txn.lastConflictKind();
3578
+ if (kind === ConflictKind.intentBeforeLog || kind === ConflictKind.intentAfterLog) Atomics.wait(backoffCell, 0, 0, Math.min(2 ** attempt, 50));
3579
+ return true;
3425
3580
  };
3581
+ const txn = new Transaction(this.store, options);
3426
3582
  const runAttempt = (attempt) => {
3427
- const txn = new Transaction(this.store, options);
3428
3583
  let result;
3429
3584
  try {
3430
3585
  result = callback(txn, attempt);
@@ -3440,7 +3595,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3440
3595
  if (isRetryable(commitErr, attempt)) return runAttempt(attempt + 1);
3441
3596
  this.#abandonTransaction(txn, commitErr);
3442
3597
  }
3443
- });
3598
+ }, (callbackErr) => this.#abortTransaction(txn, callbackErr));
3444
3599
  try {
3445
3600
  txn.commitSync();
3446
3601
  return result;
@@ -3465,7 +3620,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3465
3620
  try {
3466
3621
  txn.abort();
3467
3622
  } catch (abortErr) {
3468
- if (abortErr instanceof TransactionAbandonedError) throw abortErr;
3623
+ if (abortErr instanceof TransactionAbandonedError || abortErr instanceof TransactionAbortUnresolvedError) throw abortErr;
3469
3624
  }
3470
3625
  throw commitErr;
3471
3626
  }
@@ -3580,10 +3735,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
3580
3735
  }
3581
3736
  };
3582
3737
 
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;
3738
+ // src/parse-transaction-log.ts
3739
+ 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
3740
  const MIN_VALID_TIMESTAMP = Date.UTC(2026, 0, 27);
3741
+ const VALID_FLAGS_MASK = TRANSACTION_LOG_ENTRY_LAST_FLAG$1 | TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1;
3587
3742
  /**
3588
3743
  * Loads an entire transaction log file into memory.
3589
3744
  * @param path - The path to the transaction log file.
@@ -3621,6 +3776,8 @@ function parseTransactionLog(path, options = {}) {
3621
3776
  if (!Number.isFinite(timestamp) || timestamp < MIN_VALID_TIMESTAMP) anomalies.push(`Header timestamp ${timestamp} predates 2026-01-27 (possible corruption)`);
3622
3777
  const entries = [];
3623
3778
  let entryAnomalyCount = 0;
3779
+ let previousEndedTransaction = true;
3780
+ let previousTimestamp = NaN;
3624
3781
  while (fileOffset < size) {
3625
3782
  const timestamp = read(8).readDoubleBE(0);
3626
3783
  if (timestamp === 0) {
@@ -3637,7 +3794,11 @@ function parseTransactionLog(path, options = {}) {
3637
3794
  const data = read(length);
3638
3795
  const entryAnomalies = [];
3639
3796
  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)`);
3797
+ if ((flags & ~VALID_FLAGS_MASK) !== 0) entryAnomalies.push(`flags 0x${flags.toString(16).padStart(2, "0")} contains undefined bits (expected 0x00-0x03)`);
3798
+ const startsTransaction = previousEndedTransaction || timestamp !== previousTimestamp;
3799
+ if ((flags & TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1) !== 0 && !startsTransaction) entryAnomalies.push("abort flag on an entry that does not start a transaction");
3800
+ previousEndedTransaction = (flags & TRANSACTION_LOG_ENTRY_LAST_FLAG$1) !== 0;
3801
+ previousTimestamp = timestamp;
3641
3802
  const entry = {
3642
3803
  timestamp,
3643
3804
  length,
@@ -3666,11 +3827,45 @@ function parseTransactionLog(path, options = {}) {
3666
3827
  }
3667
3828
  }
3668
3829
 
3669
- //#endregion
3670
- //#region src/transaction-log-reader.ts
3830
+ // src/transaction-log-reader.ts
3671
3831
  const FLOAT_TO_UINT32 = /* @__PURE__ */ new Float64Array(1);
3672
3832
  const UINT32_FROM_FLOAT = new Uint32Array(FLOAT_TO_UINT32.buffer);
3673
- const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE } = constants;
3833
+ const EXACT_START_SPAN = /* @__PURE__ */ new Float64Array(3);
3834
+ const ITERATOR_TOKEN = Symbol("transactionLogIterator");
3835
+ const logEquals = TransactionLog.prototype.equals;
3836
+ var LogIterator = class {
3837
+ #sourceLog;
3838
+ #control;
3839
+ next;
3840
+ constructor(token, sourceLog, next, control) {
3841
+ if (token !== ITERATOR_TOKEN) throw new TypeError("Transaction log iterators require query()");
3842
+ this.#sourceLog = sourceLog;
3843
+ this.next = next;
3844
+ this.#control = control;
3845
+ }
3846
+ static cursor(value) {
3847
+ if (typeof value === "object" && value !== null && #control in value) return value;
3848
+ }
3849
+ static isFrom(value, sourceLog) {
3850
+ return logEquals.call(sourceLog, value.#sourceLog);
3851
+ }
3852
+ [Symbol.iterator]() {
3853
+ return this;
3854
+ }
3855
+ peek() {
3856
+ return this.#control("peek");
3857
+ }
3858
+ close() {
3859
+ this.#control("close");
3860
+ }
3861
+ static reached(value, logId, position, atSegmentEnd) {
3862
+ return value.#control("reached", logId, position, atSegmentEnd);
3863
+ }
3864
+ static scanEnd(value, logId, size) {
3865
+ return value.#control("scanEnd", logId, size);
3866
+ }
3867
+ };
3868
+ const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE, TRANSACTION_LOG_ENTRY_LAST_FLAG, TRANSACTION_LOG_ENTRY_ABORTED_FLAG } = constants;
3674
3869
  /**
3675
3870
  * Number of consecutive well-formed frames that, on their own, signal that real log data has
3676
3871
  * resumed after a framing break. Kept in step with `RESYNC_MIN_FRAMES` in
@@ -3706,6 +3901,43 @@ var CorruptFrameError = class extends RangeError {
3706
3901
  }
3707
3902
  };
3708
3903
  /**
3904
+ * Thrown when an iterator advances past segments it never read and that can no longer be read —
3905
+ * typically ones retention purged while the reader lagged. The entries they held cannot be
3906
+ * recovered from this log, so a consumer replicating it must not treat the stream as contiguous.
3907
+ * Missing sequence numbers prove only that continuity is unknown, not that each held entries.
3908
+ *
3909
+ * The iterator is left positioned at the start of `resumeLogId`, so a caller that accepts the
3910
+ * discontinuity calls `next()` again to continue from there.
3911
+ *
3912
+ * Not a `RangeError`: this is not broken framing, and consumers classify those by class.
3913
+ */
3914
+ var TransactionLogGapError = class extends Error {
3915
+ /** Sequence number of the last segment this iterator read up to. */
3916
+ logId;
3917
+ /** Sequence number of the segment iteration resumes in. */
3918
+ resumeLogId;
3919
+ /** Segments between `logId` and `resumeLogId` that could not be read. Empty segments are not counted. */
3920
+ unavailableLogCount;
3921
+ constructor(logId, resumeLogId, unavailableLogCount) {
3922
+ super(`Transaction log iterator skipped ${unavailableLogCount} unavailable segment(s) between log ${logId} and log ${resumeLogId}; entries in them were not read`);
3923
+ this.name = "TransactionLogGapError";
3924
+ this.logId = logId;
3925
+ this.resumeLogId = resumeLogId;
3926
+ this.unavailableLogCount = unavailableLogCount;
3927
+ }
3928
+ };
3929
+ /** Readable input ended before the iterator's physical `until` bound. Retry after input is available. */
3930
+ var TransactionLogUntilUnreachedError = class extends Error {
3931
+ logId;
3932
+ position;
3933
+ constructor(logId, position) {
3934
+ super(`Transaction log reader stopped at position ${position.toString(16)} of log ${logId}, short of the iterator it reads until`);
3935
+ this.name = "TransactionLogUntilUnreachedError";
3936
+ this.logId = logId;
3937
+ this.position = position;
3938
+ }
3939
+ };
3940
+ /**
3709
3941
  * Whether a complete, in-bounds frame begins at `pos`. The only sane bound on an entry's length is
3710
3942
  * the written extent: a single entry can legitimately exceed the rotation threshold (the first
3711
3943
  * entry written to a fresh file is always written in full). A zero timestamp is the
@@ -3767,7 +3999,9 @@ function corruptFrame(message, transactionLog, dataView, logBuffer, position, li
3767
3999
  * exactly matches the start timestamp, and then return all subsequent transactions in the log
3768
4000
  * regardless of whether their timestamp is before or after the start
3769
4001
  */
3770
- Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, exactStart, startFromLastFlushed, readUncommitted, exclusiveStart } = {}) {
4002
+ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, exactStart, startFromLastFlushed, readUncommitted, exclusiveStart, until } = {}) {
4003
+ let untilCursor = LogIterator.cursor(until);
4004
+ if (until !== void 0 && (!untilCursor || !LogIterator.isFrom(untilCursor, this))) throw new TypeError("until must be an iterator from query() on the same transaction log");
3771
4005
  if (!this._lastCommittedPosition) {
3772
4006
  const lastCommittedPosition = this._getLastCommittedPosition();
3773
4007
  this._lastCommittedPosition = new Float64Array(lastCommittedPosition.buffer);
@@ -3781,6 +4015,14 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3781
4015
  let dataView;
3782
4016
  let logBuffer = this._currentLogBuffer?.deref();
3783
4017
  let foundExactStart = false;
4018
+ let stopLogId = 0;
4019
+ let stopPosition = 0;
4020
+ let tailLogId = 0;
4021
+ let tailPosition = 0;
4022
+ let segmentRemovals = 0;
4023
+ let previousEndedTransaction = true;
4024
+ let previousTimestamp = NaN;
4025
+ let skippingAborted = false;
3784
4026
  if (start === void 0 && !startFromLastFlushed) {
3785
4027
  position = size;
3786
4028
  if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
@@ -3790,127 +4032,304 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3790
4032
  FLOAT_TO_UINT32[0] = this._getLastFlushed();
3791
4033
  if (FLOAT_TO_UINT32[0] === 0) FLOAT_TO_UINT32[0] = this._findPosition(0);
3792
4034
  start ??= 0;
4035
+ } else if (exactStart) {
4036
+ const startPosition = this._findExactStart(start, EXACT_START_SPAN);
4037
+ FLOAT_TO_UINT32[0] = EXACT_START_SPAN[0];
4038
+ stopLogId = UINT32_FROM_FLOAT[1];
4039
+ stopPosition = UINT32_FROM_FLOAT[0];
4040
+ FLOAT_TO_UINT32[0] = EXACT_START_SPAN[1];
4041
+ tailLogId = UINT32_FROM_FLOAT[1];
4042
+ tailPosition = UINT32_FROM_FLOAT[0];
4043
+ segmentRemovals = EXACT_START_SPAN[2];
4044
+ FLOAT_TO_UINT32[0] = startPosition;
3793
4045
  } else FLOAT_TO_UINT32[0] = this._findPosition(start);
3794
4046
  logId = UINT32_FROM_FLOAT[1];
3795
4047
  position = UINT32_FROM_FLOAT[0];
3796
4048
  if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3797
4049
  }
4050
+ const positionedAt = position;
4051
+ if (stopLogId !== 0 && pastStop(logId, position, stopLogId, stopPosition) && mayJumpTo(untilCursor, tailLogId, tailPosition)) {
4052
+ stopLogId = 0;
4053
+ if (skipToTail(this, logId, position, tailLogId, tailPosition, segmentRemovals)) {
4054
+ logId = tailLogId;
4055
+ position = tailPosition;
4056
+ logBuffer = tailLogBuffer ?? logBuffer;
4057
+ }
4058
+ tailLogBuffer = void 0;
4059
+ }
4060
+ let gapFloor = logId;
4061
+ let startUnmapped = false;
4062
+ let startMissing = false;
3798
4063
  if (logBuffer === void 0 || logBuffer.logId !== logId) {
3799
4064
  logBuffer = getLogMemoryMap(this, logId);
3800
4065
  if (logBuffer && latestLogId === logId && !readUncommitted && this._currentLogBuffer?.deref() !== logBuffer) this._currentLogBuffer = new WeakRef(logBuffer);
3801
4066
  if (logBuffer === void 0) {
3802
4067
  logBuffer = Buffer.alloc(0);
3803
- logBuffer.logId = 0;
4068
+ logBuffer.logId = logId;
3804
4069
  logBuffer.size = 0;
3805
4070
  logBuffer.dataView = new DataView(logBuffer.buffer);
3806
4071
  size = 0;
4072
+ startUnmapped = logId > 0;
4073
+ if (positionedAt <= TRANSACTION_LOG_FILE_HEADER_SIZE || logId > 0 && this.getLogFileSize(logId) > 0) gapFloor = logId - 1;
3807
4074
  }
3808
4075
  }
3809
4076
  dataView = logBuffer.dataView;
3810
4077
  if (latestLogId < logId) size = 0;
3811
- else if (latestLogId > logId) {
4078
+ else if (latestLogId > logId && !startUnmapped) {
3812
4079
  const cachedSize = logBuffer.size;
3813
4080
  if (cachedSize === void 0) size = logBuffer.size = readableExtent(logBuffer);
3814
4081
  else size = cachedSize;
3815
4082
  }
3816
- return {
3817
- [Symbol.iterator]() {
3818
- return this;
3819
- },
3820
- next() {
3821
- let timestamp;
3822
- if (position >= size) {
3823
- const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
3824
- size = latestLogId < logBuffer.logId ? 0 : latestSize;
3825
- if (latestLogId > logBuffer.logId) {
3826
- size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3827
- if (position >= size) {
3828
- const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3829
- if (nextLogBuffer) {
3830
- dataView = nextLogBuffer.dataView;
3831
- logBuffer = nextLogBuffer;
3832
- if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3833
- else size = latestSize;
3834
- position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4083
+ let peeked;
4084
+ let peekedLogId = 0;
4085
+ let peekedPosition = 0;
4086
+ let closed = false;
4087
+ let needsCursorCheck = untilCursor !== void 0 || startUnmapped;
4088
+ const iterator = new LogIterator(ITERATOR_TOKEN, transactionLog, function next() {
4089
+ let scanEnd = size;
4090
+ if (needsCursorCheck) {
4091
+ if (peeked) {
4092
+ const result = peeked;
4093
+ if (!closed) {
4094
+ peeked = void 0;
4095
+ needsCursorCheck = untilCursor !== void 0;
4096
+ }
4097
+ return result;
4098
+ }
4099
+ if (untilCursor) {
4100
+ const limit = LogIterator.scanEnd(untilCursor, logBuffer.logId, size);
4101
+ if (limit === void 0) {
4102
+ untilCursor = void 0;
4103
+ needsCursorCheck = startUnmapped;
4104
+ } else {
4105
+ if (position >= limit && LogIterator.reached(untilCursor, logBuffer.logId, position, position >= size && logBuffer.length > 0 && position >= readableExtent(logBuffer))) return {
4106
+ done: true,
4107
+ value: void 0
4108
+ };
4109
+ scanEnd = limit;
4110
+ }
4111
+ }
4112
+ if (startUnmapped) {
4113
+ const mapped = getLogMemoryMap(transactionLog, logBuffer.logId);
4114
+ if (mapped) {
4115
+ logBuffer = mapped;
4116
+ dataView = mapped.dataView;
4117
+ } else {
4118
+ const latestLogId = loadLastPosition(transactionLog, !!readUncommitted).logId;
4119
+ const fileSize = transactionLog.getLogFileSize(logBuffer.logId);
4120
+ if (logBuffer.logId >= latestLogId || fileSize > position) return endOfReadable(untilCursor, logBuffer, position);
4121
+ if (fileSize === 0) startMissing = true;
4122
+ }
4123
+ startUnmapped = false;
4124
+ needsCursorCheck = untilCursor !== void 0;
4125
+ size = 0;
4126
+ }
4127
+ }
4128
+ let timestamp;
4129
+ if (position >= size) {
4130
+ const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
4131
+ size = latestLogId < logBuffer.logId ? 0 : latestSize;
4132
+ if (latestLogId > logBuffer.logId) {
4133
+ size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
4134
+ if (position >= size) {
4135
+ if (untilCursor && LogIterator.reached(untilCursor, logBuffer.logId, position, logBuffer.length > 0 && position >= readableExtent(logBuffer))) return {
4136
+ done: true,
4137
+ value: void 0
4138
+ };
4139
+ const fromLogId = logBuffer.logId - (startMissing ? 1 : 0);
4140
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor);
4141
+ if (nextLogBuffer) {
4142
+ startMissing = false;
4143
+ const unavailable = unavailableLogCount;
4144
+ dataView = nextLogBuffer.dataView;
4145
+ logBuffer = nextLogBuffer;
4146
+ if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
4147
+ else size = latestSize;
4148
+ position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4149
+ previousEndedTransaction = true;
4150
+ if (unavailable > 0) throw new TransactionLogGapError(Math.max(fromLogId, gapFloor), logBuffer.logId, unavailable);
4151
+ }
4152
+ }
4153
+ }
4154
+ scanEnd = untilCursor ? LogIterator.scanEnd(untilCursor, logBuffer.logId, size) ?? size : size;
4155
+ }
4156
+ if (position >= scanEnd) return endOfReadable(untilCursor, logBuffer, position);
4157
+ let jumpRefused = false;
4158
+ let limit = Math.min(size, logBuffer.length);
4159
+ do {
4160
+ try {
4161
+ timestamp = dataView.getFloat64(position);
4162
+ } catch (error) {
4163
+ error.message += ` at position ${position.toString(16)} of log ${logBuffer.logId} (size=${size}, log buffer length=${logBuffer.length})`;
4164
+ throw error;
4165
+ }
4166
+ if (!timestamp) return endOfReadable(untilCursor, logBuffer, position);
4167
+ if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE > limit) {
4168
+ 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);
4169
+ position = broken.nextPosition;
4170
+ previousEndedTransaction = true;
4171
+ throw broken.error;
4172
+ }
4173
+ const length = dataView.getUint32(position + 8);
4174
+ if (length === 0 || position + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length > limit) {
4175
+ 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);
4176
+ position = broken.nextPosition;
4177
+ previousEndedTransaction = true;
4178
+ throw broken.error;
4179
+ }
4180
+ position += TRANSACTION_LOG_ENTRY_HEADER_SIZE;
4181
+ const flags = logBuffer[position - 1];
4182
+ const aborted = (flags & TRANSACTION_LOG_ENTRY_ABORTED_FLAG) !== 0;
4183
+ if (previousEndedTransaction || timestamp !== previousTimestamp || aborted) skippingAborted = aborted;
4184
+ previousEndedTransaction = (flags & TRANSACTION_LOG_ENTRY_LAST_FLAG) !== 0;
4185
+ previousTimestamp = timestamp;
4186
+ let matchesRange;
4187
+ if (skippingAborted) matchesRange = false;
4188
+ else if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
4189
+ else if (exactStart) {
4190
+ matchesRange = false;
4191
+ if (timestamp === start) {
4192
+ matchesRange = !exclusiveStart;
4193
+ foundExactStart = true;
4194
+ stopLogId = 0;
4195
+ }
4196
+ } else matchesRange = (exclusiveStart ? timestamp > start : timestamp >= start) && timestamp < end;
4197
+ if (!matchesRange && stopLogId !== 0 && !jumpRefused && pastStop(logBuffer.logId, position + length, stopLogId, stopPosition)) {
4198
+ if (mayJumpTo(untilCursor, tailLogId, tailPosition)) {
4199
+ stopLogId = 0;
4200
+ position += length;
4201
+ if (skipToTail(transactionLog, logBuffer.logId, position, tailLogId, tailPosition, segmentRemovals)) {
4202
+ if (tailLogBuffer) {
4203
+ logBuffer = tailLogBuffer;
4204
+ dataView = tailLogBuffer.dataView;
3835
4205
  }
4206
+ position = tailPosition;
4207
+ previousEndedTransaction = true;
4208
+ size = 0;
3836
4209
  }
4210
+ tailLogBuffer = void 0;
4211
+ return iterator.next();
3837
4212
  }
4213
+ jumpRefused = true;
3838
4214
  }
3839
- while (position < size) {
3840
- try {
3841
- timestamp = dataView.getFloat64(position);
3842
- } catch (error) {
3843
- error.message += ` at position ${position.toString(16)} of log ${logBuffer.logId} (size=${size}, log buffer length=${logBuffer.length})`;
3844
- throw error;
4215
+ const entryStart = position;
4216
+ position += length;
4217
+ if (matchesRange) return {
4218
+ done: false,
4219
+ value: {
4220
+ timestamp,
4221
+ endTxn: previousEndedTransaction,
4222
+ data: logBuffer.subarray(entryStart, position)
3845
4223
  }
3846
- if (!timestamp) return {
4224
+ };
4225
+ if (position >= scanEnd) {
4226
+ if (untilCursor && LogIterator.reached(untilCursor, logBuffer.logId, position)) return {
3847
4227
  done: true,
3848
4228
  value: void 0
3849
4229
  };
3850
- const limit = readUncommitted ? logBuffer.length : Math.min(size, logBuffer.length);
3851
- if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE > limit) {
3852
- 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
- position = broken.nextPosition;
3854
- throw broken.error;
3855
- }
3856
- const length = dataView.getUint32(position + 8);
3857
- if (length === 0 || position + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length > limit) {
3858
- 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
- position = broken.nextPosition;
3860
- throw broken.error;
3861
- }
3862
- position += TRANSACTION_LOG_ENTRY_HEADER_SIZE;
3863
- let matchesRange;
3864
- if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
3865
- else if (exactStart) {
3866
- if (timestamp === start) {
3867
- matchesRange = !exclusiveStart;
3868
- foundExactStart = true;
3869
- } else matchesRange = false;
3870
- } else matchesRange = (exclusiveStart ? timestamp > start : timestamp >= start) && timestamp < end;
3871
- const entryStart = position;
3872
- position += length;
3873
- if (matchesRange) return {
3874
- done: false,
3875
- value: {
3876
- timestamp,
3877
- endTxn: Boolean(logBuffer[entryStart - 1] & 1),
3878
- data: logBuffer.subarray(entryStart, position)
4230
+ const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
4231
+ size = latestLogId < logBuffer.logId ? 0 : latestSize;
4232
+ if (latestLogId > logBuffer.logId) {
4233
+ size = logBuffer.size ?? readableExtent(logBuffer);
4234
+ if (!readUncommitted) logBuffer.size = size;
4235
+ if (position < size) {
4236
+ scanEnd = untilCursor ? LogIterator.scanEnd(untilCursor, logBuffer.logId, size) ?? size : size;
4237
+ limit = Math.min(size, logBuffer.length);
4238
+ continue;
3879
4239
  }
3880
- };
3881
- if (position >= size) {
3882
- const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
3883
- size = latestLogId < logBuffer.logId ? 0 : latestSize;
3884
- if (latestLogId > logBuffer.logId) {
3885
- size = logBuffer.size ?? readableExtent(logBuffer);
3886
- if (!readUncommitted) logBuffer.size = size;
3887
- if (position < size) continue;
3888
- const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3889
- if (!nextLogBuffer) return {
3890
- done: true,
3891
- value: void 0
3892
- };
3893
- logBuffer = nextLogBuffer;
3894
- dataView = logBuffer.dataView;
3895
- if (latestLogId === logBuffer.logId) size = latestSize;
3896
- else {
3897
- const cachedSize = logBuffer.size;
3898
- if (cachedSize === void 0) {
3899
- size = readableExtent(logBuffer);
3900
- if (!readUncommitted) logBuffer.size = size;
3901
- } else size = cachedSize;
3902
- }
3903
- position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4240
+ if (untilCursor && LogIterator.reached(untilCursor, logBuffer.logId, position, logBuffer.length > 0 && position >= readableExtent(logBuffer))) return {
4241
+ done: true,
4242
+ value: void 0
4243
+ };
4244
+ const fromLogId = logBuffer.logId;
4245
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor);
4246
+ if (!nextLogBuffer) return endOfReadable(untilCursor, logBuffer, position);
4247
+ const unavailable = unavailableLogCount;
4248
+ logBuffer = nextLogBuffer;
4249
+ dataView = logBuffer.dataView;
4250
+ if (latestLogId === logBuffer.logId) size = latestSize;
4251
+ else {
4252
+ const cachedSize = logBuffer.size;
4253
+ if (cachedSize === void 0) {
4254
+ size = readableExtent(logBuffer);
4255
+ if (!readUncommitted) logBuffer.size = size;
4256
+ } else size = cachedSize;
3904
4257
  }
4258
+ position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4259
+ previousEndedTransaction = true;
4260
+ if (unavailable > 0) throw new TransactionLogGapError(Math.max(fromLogId, gapFloor), logBuffer.logId, unavailable);
3905
4261
  }
4262
+ scanEnd = untilCursor ? LogIterator.scanEnd(untilCursor, logBuffer.logId, size) ?? size : size;
4263
+ limit = Math.min(size, logBuffer.length);
4264
+ }
4265
+ } while (position < scanEnd);
4266
+ return endOfReadable(untilCursor, logBuffer, position);
4267
+ }, function control(operation, fromLogId, fromPosition, atSegmentEnd) {
4268
+ if (operation === "peek") {
4269
+ if (peeked) return peeked;
4270
+ const result = iterator.next();
4271
+ if (!result.done) {
4272
+ peeked = result;
4273
+ needsCursorCheck = true;
4274
+ peekedLogId = logBuffer.logId;
4275
+ peekedPosition = position - result.value.data.length - TRANSACTION_LOG_ENTRY_HEADER_SIZE;
3906
4276
  }
3907
- return {
4277
+ return result;
4278
+ }
4279
+ if (operation === "close") {
4280
+ closed = true;
4281
+ needsCursorCheck = true;
4282
+ peeked = {
3908
4283
  done: true,
3909
4284
  value: void 0
3910
4285
  };
4286
+ logBuffer = void 0;
4287
+ dataView = void 0;
4288
+ untilCursor = void 0;
4289
+ return;
3911
4290
  }
3912
- };
4291
+ if (closed) return;
4292
+ const boundLogId = peeked ? peekedLogId : logBuffer.logId;
4293
+ const boundPosition = peeked ? peekedPosition : position;
4294
+ if (operation === "scanEnd") {
4295
+ if (boundLogId !== fromLogId) return boundLogId > fromLogId ? fromPosition : 0;
4296
+ return boundPosition < fromPosition ? boundPosition : fromPosition;
4297
+ }
4298
+ return pastStop(fromLogId, fromPosition, boundLogId, boundPosition) || !!atSegmentEnd && boundLogId === fromLogId + 1 && boundPosition === TRANSACTION_LOG_FILE_HEADER_SIZE;
4299
+ });
4300
+ return iterator;
3913
4301
  } });
4302
+ function endOfReadable(untilCursor, logBuffer, position) {
4303
+ if (untilCursor && LogIterator.reached(untilCursor, logBuffer.logId, position, logBuffer.length > 0 && position >= readableExtent(logBuffer)) === false) throw new TransactionLogUntilUnreachedError(logBuffer.logId, position);
4304
+ return {
4305
+ done: true,
4306
+ value: void 0
4307
+ };
4308
+ }
4309
+ function pastStop(logId, position, stopLogId, stopPosition) {
4310
+ return logId > stopLogId || logId === stopLogId && position >= stopPosition;
4311
+ }
4312
+ function mayJumpTo(untilCursor, logId, position) {
4313
+ return !untilCursor || LogIterator.reached(untilCursor, logId, position - 1) !== true;
4314
+ }
4315
+ let tailLogBuffer;
4316
+ /**
4317
+ * Whether an exact-start lookup at `position` of `fromLogId` may take its one-shot jump to the tail.
4318
+ * next() calls it on a frame boundary, so a mapping error leaves a resumable cursor; from query()
4319
+ * the error escapes before any iterator exists. The removal
4320
+ * check follows the mapping and holds the store's lock, so a purge either declines the jump (the
4321
+ * scan then reports the gap) or follows it like a purge of segments already read.
4322
+ */
4323
+ function skipToTail(transactionLog, fromLogId, position, tailLogId, tailPosition, segmentRemovals) {
4324
+ tailLogBuffer = void 0;
4325
+ if (tailLogId === fromLogId) return tailPosition > position;
4326
+ if (tailLogId < fromLogId) return false;
4327
+ const mapped = getLogMemoryMap(transactionLog, tailLogId);
4328
+ if (mapped === void 0 || transactionLog._segmentRemovals() !== segmentRemovals) return false;
4329
+ tailLogBuffer = mapped;
4330
+ return true;
4331
+ }
4332
+ let unavailableLogCount = 0;
3914
4333
  /**
3915
4334
  * Maps the next readable segment after `fromLogId`, skipping a run retention has deleted —
3916
4335
  * stopping at the hole wedges the iterator there for every later poll (invariant 30).
@@ -3919,15 +4338,22 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3919
4338
  * `_findPosition(0)`, which names the bottom of the contiguous run ending at the current segment
3920
4339
  * and so lands *past* a survivor sitting between two holes.
3921
4340
  */
3922
- function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId) {
4341
+ function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor) {
4342
+ unavailableLogCount = 0;
3923
4343
  let candidateLogId = fromLogId + 1;
3924
4344
  while (candidateLogId <= latestLogId) {
3925
4345
  const logBuffer = getLogMemoryMap(transactionLog, candidateLogId);
3926
4346
  if (logBuffer) {
3927
4347
  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;
4348
+ } else {
4349
+ const fileSize = transactionLog.getLogFileSize(candidateLogId);
4350
+ if (fileSize > TRANSACTION_LOG_FILE_HEADER_SIZE) return;
4351
+ if (fileSize === 0 && candidateLogId > gapFloor) unavailableLogCount++;
4352
+ }
4353
+ const nextLogId = transactionLog._nextLogId(candidateLogId);
4354
+ if (nextLogId === 0) return;
4355
+ unavailableLogCount += Math.max(0, nextLogId - 1 - Math.max(candidateLogId, gapFloor));
4356
+ candidateLogId = nextLogId;
3931
4357
  }
3932
4358
  }
3933
4359
  /**
@@ -3981,15 +4407,14 @@ function loadLastPosition(transactionLog, readUncommitted) {
3981
4407
  };
3982
4408
  }
3983
4409
 
3984
- //#endregion
3985
- //#region src/index.ts
4410
+ // src/index.ts
3986
4411
  const versions = {
3987
4412
  rocksdb: version,
3988
- "rocksdb-js": "2.12.0"
4413
+ "rocksdb-js": "2.14.0"
3989
4414
  };
3990
4415
 
3991
- //#endregion
3992
4416
  exports.BackgroundError = BackgroundError;
4417
+ exports.ConflictKind = ConflictKind;
3993
4418
  exports.CorruptFrameError = CorruptFrameError;
3994
4419
  exports.CountEstimator = CountEstimator;
3995
4420
  exports.DBI = DBI;
@@ -3997,7 +4422,10 @@ exports.DBIterator = DBIterator;
3997
4422
  exports.RocksDatabase = RocksDatabase;
3998
4423
  exports.Store = Store;
3999
4424
  exports.Transaction = Transaction;
4425
+ exports.TransactionAbortUnresolvedError = TransactionAbortUnresolvedError;
4000
4426
  exports.TransactionLog = TransactionLog;
4427
+ exports.TransactionLogGapError = TransactionLogGapError;
4428
+ exports.TransactionLogUntilUnreachedError = TransactionLogUntilUnreachedError;
4001
4429
  exports.backups = backups;
4002
4430
  exports.constants = constants;
4003
4431
  exports.coolTransactionLogs = coolTransactionLogs;
@@ -4006,6 +4434,7 @@ exports.fileLockRelease = fileLockRelease;
4006
4434
  exports.getRegistryStatus = getRegistryStatus;
4007
4435
  exports.getWriteBufferManagerStats = getWriteBufferManagerStats;
4008
4436
  exports.parseTransactionLog = parseTransactionLog;
4437
+ exports.raiseTimestampFloor = raiseTimestampFloor;
4009
4438
  exports.registryStatus = registryStatus;
4010
4439
  exports.shutdown = shutdown;
4011
4440
  exports.stats = stats;