@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.mjs CHANGED
@@ -3,16 +3,16 @@ import { execSync } from "node:child_process";
3
3
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { isMainThread } from "node:worker_threads";
6
7
  import { access, cp, mkdir, readdir, rm } from "node:fs/promises";
7
8
  import * as orderedBinary from "ordered-binary";
8
9
  import { ExtendedIterable } from "@harperfast/extended-iterable";
9
10
  import { Encoder } from "msgpackr";
10
11
 
11
- //#region \0rolldown/runtime.js
12
+ // \0rolldown/runtime.js
12
13
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
13
14
 
14
- //#endregion
15
- //#region src/load-binding.ts
15
+ // src/load-binding.ts
16
16
  const nativeExtRE = /\.node$/;
17
17
  const req = createRequire(import.meta.url);
18
18
  /**
@@ -55,6 +55,18 @@ function locateBinding() {
55
55
  throw new Error("Unable to locate rocksdb-js native binding");
56
56
  }
57
57
  const binding = req(locateBinding());
58
+ if (isMainThread) {
59
+ let exitShutdownRan = false;
60
+ const shutdownOnExit = () => {
61
+ if (exitShutdownRan) return;
62
+ exitShutdownRan = true;
63
+ if (!binding.shutdownOnExit() && !process.exitCode) process.exitCode = 1;
64
+ };
65
+ process.on("exit", shutdownOnExit);
66
+ process.on("uncaughtExceptionMonitor", () => {
67
+ if (process._exiting) shutdownOnExit();
68
+ });
69
+ }
58
70
  /**
59
71
  * The native `BackgroundError` constructor (a real `Error` subclass). Exported
60
72
  * as both a value — for `err instanceof BackgroundError` — and, via declaration
@@ -117,7 +129,7 @@ const getRegistryStatus = binding.registryStatus;
117
129
  */
118
130
  const registryStatus = getRegistryStatus;
119
131
  /**
120
- * Flushes every open database and waits for outstanding compactions, then
132
+ * Flushes every open database and cancels its outstanding compactions, then
121
133
  * releases the registry.
122
134
  *
123
135
  * **Throws** the first failure it encounters: a close whose native teardown
@@ -127,19 +139,10 @@ const registryStatus = getRegistryStatus;
127
139
  * `database:closeFailed`; a timeout has no event, so the throw is its only
128
140
  * signal.
129
141
  *
130
- * A `process.on('exit')` listener must therefore wrap it: an exception thrown
131
- * from an `exit` listener skips every `exit` listener registered after it (and
132
- * sets exit code 1 unless an `uncaughtException` handler is installed).
133
- *
134
- * ```typescript
135
- * process.on('exit', () => {
136
- * try {
137
- * shutdown();
138
- * } catch (error) {
139
- * console.error('rocksdb-js shutdown failed', error);
140
- * }
141
- * });
142
- * ```
142
+ * You do not need to call it at exit: when rocksdb-js loads on the main
143
+ * thread it registers a `process.on('exit')` listener that does the same and
144
+ * reports failures to stderr and the exit code instead of throwing (see the
145
+ * `shutdownOnExit` config option). Calling it yourself as well is safe.
143
146
  */
144
147
  const shutdown = binding.shutdown;
145
148
  const currentThreadId = binding.currentThreadId;
@@ -147,7 +150,7 @@ const currentThreadId = binding.currentThreadId;
147
150
  * Reads the process-wide steady clock: `std::chrono::steady_clock` as
148
151
  * fractional milliseconds from an unspecified origin that is fixed for the life
149
152
  * 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
153
+ * `worker_threads` worker, whenever started or restarted, on Node and Bun
151
154
  * — is in one domain, and the difference between two samples is real elapsed
152
155
  * time unaffected by wall-clock steps. Non-decreasing but not unique (two
153
156
  * samples may be equal; treat `==` as "not later"). Windows QPC samples from
@@ -159,6 +162,30 @@ const currentThreadId = binding.currentThreadId;
159
162
  */
160
163
  const steadyClockNow = binding.steadyClockNow;
161
164
  /**
165
+ * Raises the process-wide monotonic timestamp clock — the one behind
166
+ * `db.getMonotonicTimestamp()`, every transaction's initial timestamp and so
167
+ * every transaction-log batch key — to at least `timestamp` (Unix epoch
168
+ * milliseconds). Every timestamp the process issues after the call is strictly
169
+ * greater than `timestamp`, across every database and `worker_threads` worker.
170
+ * Timestamps already issued and timestamps adopted with `txn.setTimestamp()`
171
+ * are unaffected. No open database is needed.
172
+ *
173
+ * Use it to carry a floor the caller persisted across a restart, which a
174
+ * backward wall-clock step would otherwise let the new process reissue. The
175
+ * floor is in memory only and is not a fence: restore it before the process
176
+ * starts any transaction it originates, and pass only floors this node
177
+ * persisted itself, since the clock stays that far ahead until wall time
178
+ * catches up.
179
+ *
180
+ * @returns `true` if the clock was raised: `timestamp` was ahead of both the
181
+ * wall clock and every timestamp already issued. `false` if the clock was
182
+ * already at or above it.
183
+ * @throws {TypeError} If `timestamp` is not a number.
184
+ * @throws {RangeError} If `timestamp` is not finite, is not greater than 0, is
185
+ * not less than 8.64e15, or is more than ten years ahead of the wall clock.
186
+ */
187
+ const raiseTimestampFloor = binding.raiseTimestampFloor;
188
+ /**
162
189
  * Advises the kernel that the file-backed pages of every mapped transaction log
163
190
  * are cold (Linux MADV_COLD), so they are reclaimed first under memory pressure
164
191
  * without being freed — useful during replication catch-up, where a full read of
@@ -185,12 +212,30 @@ const transactionLogMapCount = binding.transactionLogMapCount;
185
212
  */
186
213
  const lockWakeCallbackCount = binding.lockWakeCallbackCount;
187
214
  /**
215
+ * The calling thread's counters for the per-transaction iterator pool: pooled
216
+ * iterators created, and idle ones reused by a later range. Internal — used by
217
+ * tests to prove a range was served by a reused iterator.
218
+ */
219
+ const iteratorPoolStats = binding.iteratorPoolStats;
220
+ /**
221
+ * Whether transaction `getRange()` reuses pooled native iterators (off with
222
+ * `ROCKSDB_JS_ITERATOR_POOL=0`). Internal.
223
+ */
224
+ const iteratorPoolEnabled = binding.iteratorPoolEnabled;
225
+ /**
188
226
  * Test-only: force the next `count` transaction commits to fail with TryAgain (rolled back, so
189
227
  * no data is committed), reproducing a stranded-snapshot conflict deterministically. Pass 0 to
190
228
  * disarm. Used by the ERR_TRY_AGAIN retry regression test.
191
229
  */
192
230
  const forceTryAgainForTesting = binding.forceTryAgainForTesting;
193
231
  /**
232
+ * Test-only: turn the RocksDB conflict check before the transaction-log write off (`true`) or back
233
+ * on, so a test can make a logged batch conflict at commit from a stale snapshot.
234
+ */
235
+ const disablePrelogConflictCheckForTesting = binding.disablePrelogConflictCheckForTesting;
236
+ /** Test-only: run the pre-log conflict check on retries alone (`true`, the default) or on every attempt. */
237
+ const setPrelogConflictCheckRetriesOnlyForTesting = binding.setPrelogConflictCheckRetriesOnlyForTesting;
238
+ /**
194
239
  * Test-only seam: makes the physical column-family drop report failure.
195
240
  * `0` = inert, `1` = fail without dropping (the retry performs the drop),
196
241
  * `2` = drop for real and then report failure (the retry resolves as
@@ -233,8 +278,7 @@ const nativeValidateTransactionLog = binding.validateTransactionLog;
233
278
  const stats = binding.stats;
234
279
  const version = binding.version;
235
280
 
236
- //#endregion
237
- //#region src/validate-transaction-log.ts
281
+ // src/validate-transaction-log.ts
238
282
  /**
239
283
  * Validates a transaction log store directory: every `<sequence>.txnlog` file
240
284
  * is checked for a valid header (token, version) and intact v1 entry framing
@@ -274,8 +318,7 @@ function validateTransactionLogStore(path, options) {
274
318
  return new Promise((resolve, reject) => nativeValidateTransactionLog(resolve, reject, path, options?.strict ?? false));
275
319
  }
276
320
 
277
- //#endregion
278
- //#region src/backup.ts
321
+ // src/backup.ts
279
322
  /** Subdirectory (under a backup directory) holding per-backup transaction log snapshots. */
280
323
  const TRANSACTION_LOGS_DIRNAME = "transaction_logs";
281
324
  /** Non-blocking existence check (`fs.existsSync` would block the event loop). */
@@ -559,8 +602,7 @@ const backups = {
559
602
  }
560
603
  };
561
604
 
562
- //#endregion
563
- //#region src/count-estimator.ts
605
+ // src/count-estimator.ts
564
606
  /**
565
607
  * Below this many traversed entries the observed density is too noisy to
566
608
  * calibrate with, so `estimate()` returns the raw statistical estimate.
@@ -703,8 +745,7 @@ var CountEstimator = class {
703
745
  }
704
746
  };
705
747
 
706
- //#endregion
707
- //#region src/util.ts
748
+ // src/util.ts
708
749
  /**
709
750
  * Parses a duration string into milliseconds.
710
751
  *
@@ -775,8 +816,7 @@ function when(subject, callback, errback) {
775
816
  }
776
817
  }
777
818
 
778
- //#endregion
779
- //#region src/dbi.ts
819
+ // src/dbi.ts
780
820
  /**
781
821
  * The base class for all database operations. This base class is shared by
782
822
  * `RocksDatabase` and `Transaction`.
@@ -1061,8 +1101,7 @@ var DBI = class DBI {
1061
1101
  }
1062
1102
  };
1063
1103
 
1064
- //#endregion
1065
- //#region src/tar.ts
1104
+ // src/tar.ts
1066
1105
  /**
1067
1106
  * A minimal, dependency-free streaming USTAR (POSIX tar) encoder.
1068
1107
  *
@@ -1268,8 +1307,7 @@ var TarEncoder = class {
1268
1307
  }
1269
1308
  };
1270
1309
 
1271
- //#endregion
1272
- //#region src/backup-stream.ts
1310
+ // src/backup-stream.ts
1273
1311
  /** Native event discriminator: a new file header (vs. a payload chunk). */
1274
1312
  const EVENT_FILE = 0;
1275
1313
  /**
@@ -1313,9 +1351,22 @@ async function backupToStream(db, stream, options) {
1313
1351
  }
1314
1352
  }
1315
1353
 
1316
- //#endregion
1317
- //#region src/dbi-iterator.ts
1318
- const { ITERATOR_RESULT_DONE, ITERATOR_RESULT_FAST } = constants;
1354
+ // src/dbi-iterator.ts
1355
+ const { ITERATOR_RESULT_DONE, ITERATOR_RESULT_FAST, ITERATOR_RESULT_RELEASED } = constants;
1356
+ const transactionsToTrim = /* @__PURE__ */ new Set();
1357
+ function trimIdleIterators() {
1358
+ try {
1359
+ for (const transaction of transactionsToTrim) transaction.trimIterators();
1360
+ } finally {
1361
+ transactionsToTrim.clear();
1362
+ }
1363
+ }
1364
+ function scheduleTrim(transaction) {
1365
+ if (transaction !== void 0) {
1366
+ if (transactionsToTrim.size === 0) setImmediate(trimIdleIterators);
1367
+ transactionsToTrim.add(transaction);
1368
+ }
1369
+ }
1319
1370
  const DONE_RESULT = Object.freeze({
1320
1371
  done: true,
1321
1372
  value: void 0
@@ -1335,10 +1386,12 @@ var DBIterator = class {
1335
1386
  #includeValues;
1336
1387
  #limit;
1337
1388
  #yielded = 0;
1338
- constructor(iterator, store, includeValues, limit) {
1389
+ #transaction;
1390
+ constructor(iterator, store, includeValues, limit, transaction) {
1339
1391
  this.iterator = iterator;
1340
1392
  this.store = store;
1341
1393
  this.#includeValues = includeValues;
1394
+ this.#transaction = transaction;
1342
1395
  this.#limit = limit ?? void 0;
1343
1396
  }
1344
1397
  [Symbol.iterator]() {
@@ -1346,7 +1399,7 @@ var DBIterator = class {
1346
1399
  }
1347
1400
  next() {
1348
1401
  if (this.#limit !== void 0 && this.#yielded >= this.#limit) {
1349
- this.iterator.return?.();
1402
+ this.#release(this.iterator.return());
1350
1403
  return DONE_RESULT;
1351
1404
  }
1352
1405
  let result;
@@ -1354,11 +1407,15 @@ var DBIterator = class {
1354
1407
  result = this.iterator.next();
1355
1408
  } catch (err) {
1356
1409
  try {
1357
- this.iterator.return?.();
1410
+ this.#release(this.iterator.return());
1358
1411
  } catch {}
1359
1412
  throw err;
1360
1413
  }
1361
1414
  if (result === ITERATOR_RESULT_DONE) return DONE_RESULT;
1415
+ if (result === ITERATOR_RESULT_RELEASED) {
1416
+ scheduleTrim(this.#transaction);
1417
+ return DONE_RESULT;
1418
+ }
1362
1419
  this.#yielded++;
1363
1420
  const includeValues = this.#includeValues;
1364
1421
  const value = {};
@@ -1379,20 +1436,23 @@ var DBIterator = class {
1379
1436
  };
1380
1437
  }
1381
1438
  return(value) {
1382
- this.iterator.return?.();
1439
+ this.#release(this.iterator.return());
1383
1440
  return {
1384
1441
  done: true,
1385
1442
  value
1386
1443
  };
1387
1444
  }
1388
1445
  throw(err) {
1389
- this.iterator.throw?.(err);
1446
+ this.#release(this.iterator.throw(err));
1390
1447
  throw err;
1391
1448
  }
1449
+ #release(pooled) {
1450
+ if (pooled) scheduleTrim(this.#transaction);
1451
+ this.#transaction = void 0;
1452
+ }
1392
1453
  };
1393
1454
 
1394
- //#endregion
1395
- //#region src/encoding.ts
1455
+ // src/encoding.ts
1396
1456
  /**
1397
1457
  * Initializes the key encoder functions.
1398
1458
  *
@@ -1459,8 +1519,7 @@ function createFixedBuffer(size) {
1459
1519
  return buffer;
1460
1520
  }
1461
1521
 
1462
- //#endregion
1463
- //#region src/store.ts
1522
+ // src/store.ts
1464
1523
  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
1524
  const KEY_BUFFER_SIZE = 4096;
1466
1525
  const KEY_BUFFER = createFixedBuffer(KEY_BUFFER_SIZE);
@@ -1761,6 +1820,14 @@ var Store = class {
1761
1820
  */
1762
1821
  writeBufferSize;
1763
1822
  /**
1823
+ * The memtable arena block size in bytes; `0` or unset is automatic.
1824
+ */
1825
+ arenaBlockSize;
1826
+ /**
1827
+ * Bits per key of the whole-key filter in each SST; `0` disables filters.
1828
+ */
1829
+ bloomFilterBitsPerKey;
1830
+ /**
1764
1831
  * The function used to encode keys using the shared `keyBuffer`.
1765
1832
  */
1766
1833
  writeKey;
@@ -1813,6 +1880,8 @@ var Store = class {
1813
1880
  this.timestampFloorLog = options?.timestampFloorLog;
1814
1881
  this.verificationTable = options?.verificationTable;
1815
1882
  this.writeBufferSize = options?.writeBufferSize;
1883
+ this.arenaBlockSize = options?.arenaBlockSize;
1884
+ this.bloomFilterBitsPerKey = options?.bloomFilterBitsPerKey;
1816
1885
  this.writeKey = writeKey;
1817
1886
  this.#columnFamilyOptions = options ? { ...options } : void 0;
1818
1887
  }
@@ -1972,7 +2041,7 @@ var Store = class {
1972
2041
  const result = context.getSync(keyParam, flags | ONLY_IF_IN_MEMORY_CACHE_FLAG, txnId, expectedVersion);
1973
2042
  if (typeof result === "number") {
1974
2043
  if (result === NOT_IN_MEMORY_CACHE_FLAG) return new Promise((resolve, reject) => {
1975
- context.get(keyParam, resolve, reject, txnId, expectedVersion);
2044
+ context.get(keyParam, resolve, reject, txnId, expectedVersion, flags);
1976
2045
  });
1977
2046
  if (result === FRESH_VERSION_FLAG) return result;
1978
2047
  VALUE_BUFFER.end = result;
@@ -2063,7 +2132,9 @@ var Store = class {
2063
2132
  if (context !== this.db) flags |= ITERATOR_CONTEXT_IS_TRANSACTION_FLAG;
2064
2133
  if (txnId !== void 0) flags |= ITERATOR_HAS_TRANSACTION_ID_FLAG;
2065
2134
  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));
2135
+ const nativeIterator = new NativeIterator(context, flags, startKeyEnd, endKeyStart, endKeyEnd, advancedOptions, txnId);
2136
+ const transaction = context !== this.db ? context : options?.transaction?._context;
2137
+ return new ExtendedIterable(new DBIterator(nativeIterator, this, includeValues, options?.limit, transaction));
2067
2138
  }
2068
2139
  getSync(context, key, alwaysCreateNewBuffer = false, options) {
2069
2140
  const keyParam = getKeyParam(this.encodeKey(key));
@@ -2177,7 +2248,9 @@ var Store = class {
2177
2248
  transactionLogsPath: this.transactionLogsPath,
2178
2249
  timestampFloorLog: this.timestampFloorLog,
2179
2250
  verificationTable: this.verificationTable,
2180
- writeBufferSize: this.writeBufferSize
2251
+ writeBufferSize: this.writeBufferSize,
2252
+ arenaBlockSize: this.arenaBlockSize,
2253
+ bloomFilterBitsPerKey: this.bloomFilterBitsPerKey
2181
2254
  });
2182
2255
  this.identityPath = this.db.identityPath;
2183
2256
  return false;
@@ -2283,8 +2356,7 @@ function getKeyParam(keyBuffer) {
2283
2356
  return keyBuffer.length;
2284
2357
  }
2285
2358
 
2286
- //#endregion
2287
- //#region src/transaction.ts
2359
+ // src/transaction.ts
2288
2360
  /**
2289
2361
  * Sentinel value returned by `commit()` when `coordinatedRetry: true` and the
2290
2362
  * transaction encountered an IsBusy conflict. The native layer parks on VT
@@ -2308,10 +2380,18 @@ var TransactionAlreadyAbortedError = class extends Error {
2308
2380
  */
2309
2381
  var TransactionRetryableError = class extends Error {
2310
2382
  hasLog;
2383
+ /**
2384
+ * The attempt was refused before writing its transaction-log batch (`transactionLogAbortMarkers`):
2385
+ * an older transaction held a write intent on one of its keys (`transactionLogWriteIntents`), or, on a retry, RocksDB's conflict
2386
+ * check found a write that already committed (see `lastConflictKind()`). Nothing of it is in the
2387
+ * log, so a retry may stage its batch afresh.
2388
+ */
2389
+ refusedBeforeLog;
2311
2390
  txn;
2312
2391
  constructor(error, txn) {
2313
2392
  super(error.message);
2314
2393
  this.hasLog = error.hasLog ?? false;
2394
+ this.refusedBeforeLog = error.refusedBeforeLog ?? false;
2315
2395
  this.txn = txn;
2316
2396
  }
2317
2397
  };
@@ -2341,6 +2421,38 @@ var TransactionAbandonedError = class extends Error {
2341
2421
  }
2342
2422
  };
2343
2423
  /**
2424
+ * `abort()` of a transaction whose transaction-log batch could not be marked aborted
2425
+ * (`transactionLogAbortMarkers`). The batch stays unresolved, so committed readers do not pass it and
2426
+ * the log store refuses appends, until a retry on the next append, database flush or store close
2427
+ * marks it.
2428
+ */
2429
+ var TransactionAbortUnresolvedError = class extends Error {
2430
+ code = "ERR_TRANSACTION_ABORT_UNRESOLVED";
2431
+ txn;
2432
+ constructor(error, txn) {
2433
+ super(error.message);
2434
+ this.txn = txn;
2435
+ }
2436
+ };
2437
+ /**
2438
+ * Why a transaction's most recent attempt was refused or retried, from {@link Transaction.lastConflictKind}.
2439
+ */
2440
+ const ConflictKind = {
2441
+ none: 0,
2442
+ /** Refused on an older transaction's write intent before writing its log batch. */
2443
+ intentBeforeLog: 1,
2444
+ /** Refused on an older transaction's write intent after its log batch was written. */
2445
+ intentAfterLog: 2,
2446
+ /** RocksDB found a conflict at commit. */
2447
+ atCommit: 3,
2448
+ /**
2449
+ * RocksDB's conflict check, run before the log write, found a write that already committed, or,
2450
+ * as at commit, could not validate because the memtable history it needs was flushed (needs a
2451
+ * RocksDB build that exports the check).
2452
+ */
2453
+ checkBeforeLog: 4
2454
+ };
2455
+ /**
2344
2456
  * Provides transaction level operations to a transaction callback.
2345
2457
  */
2346
2458
  var Transaction = class extends DBI {
@@ -2355,9 +2467,10 @@ var Transaction = class extends DBI {
2355
2467
  if (store.readOnly) {
2356
2468
  super(store);
2357
2469
  this.#txn = { id: 0 };
2358
- this.abandonWrites = this.abort = this.commitSync = this.setTimestamp = () => {};
2470
+ this.abandonWrites = this.abort = this.commitSync = this.setTimestamp = this.setParkDeadline = () => {};
2359
2471
  this.commit = async () => {};
2360
- this.getTimestamp = () => 0;
2472
+ this.getTimestamp = this.lastConflictKind = () => 0;
2473
+ this.logWritten = () => false;
2361
2474
  } else {
2362
2475
  const txn = new NativeTransaction(store.db, options);
2363
2476
  super(store, txn);
@@ -2383,6 +2496,7 @@ var Transaction = class extends DBI {
2383
2496
  this.#txn.abort();
2384
2497
  } catch (err) {
2385
2498
  if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABANDONED") throw new TransactionAbandonedError(err, this);
2499
+ if (err instanceof Error && "code" in err && err.code === "ERR_TRANSACTION_ABORT_UNRESOLVED") throw new TransactionAbortUnresolvedError(err, this);
2386
2500
  throw err;
2387
2501
  }
2388
2502
  }
@@ -2451,6 +2565,28 @@ var Transaction = class extends DBI {
2451
2565
  return this.#txn.getTimestamp();
2452
2566
  }
2453
2567
  /**
2568
+ * Whether this transaction's batch is in the transaction log. A commit conflict after the batch
2569
+ * was written keeps it for the retry, which must not stage it again; an attempt refused before
2570
+ * writing it (`refusedBeforeLog`) leaves nothing.
2571
+ */
2572
+ logWritten() {
2573
+ return this.#txn.logWritten();
2574
+ }
2575
+ /**
2576
+ * Why the most recent attempt was refused or retried; one of {@link ConflictKind}.
2577
+ */
2578
+ lastConflictKind() {
2579
+ return this.#txn.lastConflictKind();
2580
+ }
2581
+ /**
2582
+ * Bounds coordinated-retry parks: past `deadline` (epoch milliseconds) a parked commit resolves
2583
+ * `RETRY_NOW` at once, so a caller enforcing its own deadline can abort instead of waiting out
2584
+ * the default park bound. 0 clears it.
2585
+ */
2586
+ setParkDeadline(deadline) {
2587
+ this.#txn.setParkDeadline(deadline);
2588
+ }
2589
+ /**
2454
2590
  * Get the transaction id.
2455
2591
  */
2456
2592
  get id() {
@@ -2471,8 +2607,8 @@ var Transaction = class extends DBI {
2471
2607
  }
2472
2608
  };
2473
2609
 
2474
- //#endregion
2475
- //#region src/database.ts
2610
+ // src/database.ts
2611
+ const backoffCell = new Int32Array(new SharedArrayBuffer(4));
2476
2612
  const BACKGROUND_ERROR_SEVERITY_NAMES = [
2477
2613
  "none",
2478
2614
  "soft",
@@ -2882,6 +3018,19 @@ var RocksDatabase = class RocksDatabase extends DBI {
2882
3018
  return this.store.db.flushSync(options);
2883
3019
  }
2884
3020
  /**
3021
+ * Syncs the write-ahead log to stable storage (`DB::SyncWAL`), so every WAL-backed write
3022
+ * committed before the call survives a power loss afterwards. Unlike {@link flush} it writes
3023
+ * no SST file and never stalls writers. Not covered: writes that skipped the WAL
3024
+ * (`disableWAL`), and transaction-log files, which keep their own durability. Runs on the
3025
+ * libuv threadpool; RocksDB serializes concurrent syncs of one WAL, so keep one in flight
3026
+ * per database rather than parking pool workers on a queue of them.
3027
+ *
3028
+ * On a read-only database the returned promise resolves without syncing.
3029
+ */
3030
+ syncWal() {
3031
+ return new Promise((resolve, reject) => this.store.db.syncWal(resolve, reject));
3032
+ }
3033
+ /**
2885
3034
  * Returns the most recent {@link BackgroundError} observed on this database, or
2886
3035
  * `null` when none has occurred. The `'error'` event is the push counterpart;
2887
3036
  * this is the pull equivalent for on-demand checks (e.g. a health probe) and
@@ -3350,7 +3499,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
3350
3499
  async transaction(callback, options) {
3351
3500
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
3352
3501
  const maxRetries = options?.maxRetries ?? 3;
3353
- const txn = new Transaction(this.store, options);
3502
+ const txn = new Transaction(this.store, options?.retryOnBusy === false && options.coordinatedRetry ? {
3503
+ ...options,
3504
+ coordinatedRetry: false
3505
+ } : options);
3354
3506
  let result;
3355
3507
  this.notify("begin-transaction");
3356
3508
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -3364,7 +3516,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3364
3516
  commitResult = await txn.commit();
3365
3517
  } catch (commitErr) {
3366
3518
  if (commitErr instanceof TransactionAlreadyAbortedError) return;
3367
- if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? commitErr.hasLog) && attempt < maxRetries) continue;
3519
+ if (commitErr instanceof TransactionRetryableError && (options?.retryOnBusy ?? (commitErr.hasLog || commitErr.refusedBeforeLog)) && attempt < maxRetries) continue;
3368
3520
  this.#abandonTransaction(txn, commitErr);
3369
3521
  return;
3370
3522
  }
@@ -3396,10 +3548,13 @@ var RocksDatabase = class RocksDatabase extends DBI {
3396
3548
  if (typeof callback !== "function") throw new TypeError("Callback must be a function");
3397
3549
  const maxRetries = options?.maxRetries ?? 3;
3398
3550
  const isRetryable = (err, attempt) => {
3399
- return err instanceof TransactionRetryableError && (options?.retryOnBusy ?? err.hasLog) && attempt <= maxRetries;
3551
+ if (!(err instanceof TransactionRetryableError && (options?.retryOnBusy ?? (err.hasLog || err.refusedBeforeLog)) && attempt <= maxRetries)) return false;
3552
+ const kind = txn.lastConflictKind();
3553
+ if (kind === ConflictKind.intentBeforeLog || kind === ConflictKind.intentAfterLog) Atomics.wait(backoffCell, 0, 0, Math.min(2 ** attempt, 50));
3554
+ return true;
3400
3555
  };
3556
+ const txn = new Transaction(this.store, options);
3401
3557
  const runAttempt = (attempt) => {
3402
- const txn = new Transaction(this.store, options);
3403
3558
  let result;
3404
3559
  try {
3405
3560
  result = callback(txn, attempt);
@@ -3415,7 +3570,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3415
3570
  if (isRetryable(commitErr, attempt)) return runAttempt(attempt + 1);
3416
3571
  this.#abandonTransaction(txn, commitErr);
3417
3572
  }
3418
- });
3573
+ }, (callbackErr) => this.#abortTransaction(txn, callbackErr));
3419
3574
  try {
3420
3575
  txn.commitSync();
3421
3576
  return result;
@@ -3440,7 +3595,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
3440
3595
  try {
3441
3596
  txn.abort();
3442
3597
  } catch (abortErr) {
3443
- if (abortErr instanceof TransactionAbandonedError) throw abortErr;
3598
+ if (abortErr instanceof TransactionAbandonedError || abortErr instanceof TransactionAbortUnresolvedError) throw abortErr;
3444
3599
  }
3445
3600
  throw commitErr;
3446
3601
  }
@@ -3555,10 +3710,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
3555
3710
  }
3556
3711
  };
3557
3712
 
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;
3713
+ // src/parse-transaction-log.ts
3714
+ 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
3715
  const MIN_VALID_TIMESTAMP = Date.UTC(2026, 0, 27);
3716
+ const VALID_FLAGS_MASK = TRANSACTION_LOG_ENTRY_LAST_FLAG$1 | TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1;
3562
3717
  /**
3563
3718
  * Loads an entire transaction log file into memory.
3564
3719
  * @param path - The path to the transaction log file.
@@ -3596,6 +3751,8 @@ function parseTransactionLog(path, options = {}) {
3596
3751
  if (!Number.isFinite(timestamp) || timestamp < MIN_VALID_TIMESTAMP) anomalies.push(`Header timestamp ${timestamp} predates 2026-01-27 (possible corruption)`);
3597
3752
  const entries = [];
3598
3753
  let entryAnomalyCount = 0;
3754
+ let previousEndedTransaction = true;
3755
+ let previousTimestamp = NaN;
3599
3756
  while (fileOffset < size) {
3600
3757
  const timestamp = read(8).readDoubleBE(0);
3601
3758
  if (timestamp === 0) {
@@ -3612,7 +3769,11 @@ function parseTransactionLog(path, options = {}) {
3612
3769
  const data = read(length);
3613
3770
  const entryAnomalies = [];
3614
3771
  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)`);
3772
+ if ((flags & ~VALID_FLAGS_MASK) !== 0) entryAnomalies.push(`flags 0x${flags.toString(16).padStart(2, "0")} contains undefined bits (expected 0x00-0x03)`);
3773
+ const startsTransaction = previousEndedTransaction || timestamp !== previousTimestamp;
3774
+ if ((flags & TRANSACTION_LOG_ENTRY_ABORTED_FLAG$1) !== 0 && !startsTransaction) entryAnomalies.push("abort flag on an entry that does not start a transaction");
3775
+ previousEndedTransaction = (flags & TRANSACTION_LOG_ENTRY_LAST_FLAG$1) !== 0;
3776
+ previousTimestamp = timestamp;
3616
3777
  const entry = {
3617
3778
  timestamp,
3618
3779
  length,
@@ -3641,11 +3802,45 @@ function parseTransactionLog(path, options = {}) {
3641
3802
  }
3642
3803
  }
3643
3804
 
3644
- //#endregion
3645
- //#region src/transaction-log-reader.ts
3805
+ // src/transaction-log-reader.ts
3646
3806
  const FLOAT_TO_UINT32 = /* @__PURE__ */ new Float64Array(1);
3647
3807
  const UINT32_FROM_FLOAT = new Uint32Array(FLOAT_TO_UINT32.buffer);
3648
- const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE } = constants;
3808
+ const EXACT_START_SPAN = /* @__PURE__ */ new Float64Array(3);
3809
+ const ITERATOR_TOKEN = Symbol("transactionLogIterator");
3810
+ const logEquals = TransactionLog.prototype.equals;
3811
+ var LogIterator = class {
3812
+ #sourceLog;
3813
+ #control;
3814
+ next;
3815
+ constructor(token, sourceLog, next, control) {
3816
+ if (token !== ITERATOR_TOKEN) throw new TypeError("Transaction log iterators require query()");
3817
+ this.#sourceLog = sourceLog;
3818
+ this.next = next;
3819
+ this.#control = control;
3820
+ }
3821
+ static cursor(value) {
3822
+ if (typeof value === "object" && value !== null && #control in value) return value;
3823
+ }
3824
+ static isFrom(value, sourceLog) {
3825
+ return logEquals.call(sourceLog, value.#sourceLog);
3826
+ }
3827
+ [Symbol.iterator]() {
3828
+ return this;
3829
+ }
3830
+ peek() {
3831
+ return this.#control("peek");
3832
+ }
3833
+ close() {
3834
+ this.#control("close");
3835
+ }
3836
+ static reached(value, logId, position, atSegmentEnd) {
3837
+ return value.#control("reached", logId, position, atSegmentEnd);
3838
+ }
3839
+ static scanEnd(value, logId, size) {
3840
+ return value.#control("scanEnd", logId, size);
3841
+ }
3842
+ };
3843
+ const { TRANSACTION_LOG_FILE_HEADER_SIZE, TRANSACTION_LOG_ENTRY_HEADER_SIZE, TRANSACTION_LOG_ENTRY_LAST_FLAG, TRANSACTION_LOG_ENTRY_ABORTED_FLAG } = constants;
3649
3844
  /**
3650
3845
  * Number of consecutive well-formed frames that, on their own, signal that real log data has
3651
3846
  * resumed after a framing break. Kept in step with `RESYNC_MIN_FRAMES` in
@@ -3681,6 +3876,43 @@ var CorruptFrameError = class extends RangeError {
3681
3876
  }
3682
3877
  };
3683
3878
  /**
3879
+ * Thrown when an iterator advances past segments it never read and that can no longer be read —
3880
+ * typically ones retention purged while the reader lagged. The entries they held cannot be
3881
+ * recovered from this log, so a consumer replicating it must not treat the stream as contiguous.
3882
+ * Missing sequence numbers prove only that continuity is unknown, not that each held entries.
3883
+ *
3884
+ * The iterator is left positioned at the start of `resumeLogId`, so a caller that accepts the
3885
+ * discontinuity calls `next()` again to continue from there.
3886
+ *
3887
+ * Not a `RangeError`: this is not broken framing, and consumers classify those by class.
3888
+ */
3889
+ var TransactionLogGapError = class extends Error {
3890
+ /** Sequence number of the last segment this iterator read up to. */
3891
+ logId;
3892
+ /** Sequence number of the segment iteration resumes in. */
3893
+ resumeLogId;
3894
+ /** Segments between `logId` and `resumeLogId` that could not be read. Empty segments are not counted. */
3895
+ unavailableLogCount;
3896
+ constructor(logId, resumeLogId, unavailableLogCount) {
3897
+ super(`Transaction log iterator skipped ${unavailableLogCount} unavailable segment(s) between log ${logId} and log ${resumeLogId}; entries in them were not read`);
3898
+ this.name = "TransactionLogGapError";
3899
+ this.logId = logId;
3900
+ this.resumeLogId = resumeLogId;
3901
+ this.unavailableLogCount = unavailableLogCount;
3902
+ }
3903
+ };
3904
+ /** Readable input ended before the iterator's physical `until` bound. Retry after input is available. */
3905
+ var TransactionLogUntilUnreachedError = class extends Error {
3906
+ logId;
3907
+ position;
3908
+ constructor(logId, position) {
3909
+ super(`Transaction log reader stopped at position ${position.toString(16)} of log ${logId}, short of the iterator it reads until`);
3910
+ this.name = "TransactionLogUntilUnreachedError";
3911
+ this.logId = logId;
3912
+ this.position = position;
3913
+ }
3914
+ };
3915
+ /**
3684
3916
  * Whether a complete, in-bounds frame begins at `pos`. The only sane bound on an entry's length is
3685
3917
  * the written extent: a single entry can legitimately exceed the rotation threshold (the first
3686
3918
  * entry written to a fresh file is always written in full). A zero timestamp is the
@@ -3742,7 +3974,9 @@ function corruptFrame(message, transactionLog, dataView, logBuffer, position, li
3742
3974
  * exactly matches the start timestamp, and then return all subsequent transactions in the log
3743
3975
  * regardless of whether their timestamp is before or after the start
3744
3976
  */
3745
- Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, exactStart, startFromLastFlushed, readUncommitted, exclusiveStart } = {}) {
3977
+ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, exactStart, startFromLastFlushed, readUncommitted, exclusiveStart, until } = {}) {
3978
+ let untilCursor = LogIterator.cursor(until);
3979
+ if (until !== void 0 && (!untilCursor || !LogIterator.isFrom(untilCursor, this))) throw new TypeError("until must be an iterator from query() on the same transaction log");
3746
3980
  if (!this._lastCommittedPosition) {
3747
3981
  const lastCommittedPosition = this._getLastCommittedPosition();
3748
3982
  this._lastCommittedPosition = new Float64Array(lastCommittedPosition.buffer);
@@ -3756,6 +3990,14 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3756
3990
  let dataView;
3757
3991
  let logBuffer = this._currentLogBuffer?.deref();
3758
3992
  let foundExactStart = false;
3993
+ let stopLogId = 0;
3994
+ let stopPosition = 0;
3995
+ let tailLogId = 0;
3996
+ let tailPosition = 0;
3997
+ let segmentRemovals = 0;
3998
+ let previousEndedTransaction = true;
3999
+ let previousTimestamp = NaN;
4000
+ let skippingAborted = false;
3759
4001
  if (start === void 0 && !startFromLastFlushed) {
3760
4002
  position = size;
3761
4003
  if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
@@ -3765,127 +4007,304 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3765
4007
  FLOAT_TO_UINT32[0] = this._getLastFlushed();
3766
4008
  if (FLOAT_TO_UINT32[0] === 0) FLOAT_TO_UINT32[0] = this._findPosition(0);
3767
4009
  start ??= 0;
4010
+ } else if (exactStart) {
4011
+ const startPosition = this._findExactStart(start, EXACT_START_SPAN);
4012
+ FLOAT_TO_UINT32[0] = EXACT_START_SPAN[0];
4013
+ stopLogId = UINT32_FROM_FLOAT[1];
4014
+ stopPosition = UINT32_FROM_FLOAT[0];
4015
+ FLOAT_TO_UINT32[0] = EXACT_START_SPAN[1];
4016
+ tailLogId = UINT32_FROM_FLOAT[1];
4017
+ tailPosition = UINT32_FROM_FLOAT[0];
4018
+ segmentRemovals = EXACT_START_SPAN[2];
4019
+ FLOAT_TO_UINT32[0] = startPosition;
3768
4020
  } else FLOAT_TO_UINT32[0] = this._findPosition(start);
3769
4021
  logId = UINT32_FROM_FLOAT[1];
3770
4022
  position = UINT32_FROM_FLOAT[0];
3771
4023
  if (position === 0) position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3772
4024
  }
4025
+ const positionedAt = position;
4026
+ if (stopLogId !== 0 && pastStop(logId, position, stopLogId, stopPosition) && mayJumpTo(untilCursor, tailLogId, tailPosition)) {
4027
+ stopLogId = 0;
4028
+ if (skipToTail(this, logId, position, tailLogId, tailPosition, segmentRemovals)) {
4029
+ logId = tailLogId;
4030
+ position = tailPosition;
4031
+ logBuffer = tailLogBuffer ?? logBuffer;
4032
+ }
4033
+ tailLogBuffer = void 0;
4034
+ }
4035
+ let gapFloor = logId;
4036
+ let startUnmapped = false;
4037
+ let startMissing = false;
3773
4038
  if (logBuffer === void 0 || logBuffer.logId !== logId) {
3774
4039
  logBuffer = getLogMemoryMap(this, logId);
3775
4040
  if (logBuffer && latestLogId === logId && !readUncommitted && this._currentLogBuffer?.deref() !== logBuffer) this._currentLogBuffer = new WeakRef(logBuffer);
3776
4041
  if (logBuffer === void 0) {
3777
4042
  logBuffer = Buffer.alloc(0);
3778
- logBuffer.logId = 0;
4043
+ logBuffer.logId = logId;
3779
4044
  logBuffer.size = 0;
3780
4045
  logBuffer.dataView = new DataView(logBuffer.buffer);
3781
4046
  size = 0;
4047
+ startUnmapped = logId > 0;
4048
+ if (positionedAt <= TRANSACTION_LOG_FILE_HEADER_SIZE || logId > 0 && this.getLogFileSize(logId) > 0) gapFloor = logId - 1;
3782
4049
  }
3783
4050
  }
3784
4051
  dataView = logBuffer.dataView;
3785
4052
  if (latestLogId < logId) size = 0;
3786
- else if (latestLogId > logId) {
4053
+ else if (latestLogId > logId && !startUnmapped) {
3787
4054
  const cachedSize = logBuffer.size;
3788
4055
  if (cachedSize === void 0) size = logBuffer.size = readableExtent(logBuffer);
3789
4056
  else size = cachedSize;
3790
4057
  }
3791
- return {
3792
- [Symbol.iterator]() {
3793
- return this;
3794
- },
3795
- next() {
3796
- let timestamp;
3797
- if (position >= size) {
3798
- const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
3799
- size = latestLogId < logBuffer.logId ? 0 : latestSize;
3800
- if (latestLogId > logBuffer.logId) {
3801
- size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3802
- if (position >= size) {
3803
- const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3804
- if (nextLogBuffer) {
3805
- dataView = nextLogBuffer.dataView;
3806
- logBuffer = nextLogBuffer;
3807
- if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3808
- else size = latestSize;
3809
- position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4058
+ let peeked;
4059
+ let peekedLogId = 0;
4060
+ let peekedPosition = 0;
4061
+ let closed = false;
4062
+ let needsCursorCheck = untilCursor !== void 0 || startUnmapped;
4063
+ const iterator = new LogIterator(ITERATOR_TOKEN, transactionLog, function next() {
4064
+ let scanEnd = size;
4065
+ if (needsCursorCheck) {
4066
+ if (peeked) {
4067
+ const result = peeked;
4068
+ if (!closed) {
4069
+ peeked = void 0;
4070
+ needsCursorCheck = untilCursor !== void 0;
4071
+ }
4072
+ return result;
4073
+ }
4074
+ if (untilCursor) {
4075
+ const limit = LogIterator.scanEnd(untilCursor, logBuffer.logId, size);
4076
+ if (limit === void 0) {
4077
+ untilCursor = void 0;
4078
+ needsCursorCheck = startUnmapped;
4079
+ } else {
4080
+ if (position >= limit && LogIterator.reached(untilCursor, logBuffer.logId, position, position >= size && logBuffer.length > 0 && position >= readableExtent(logBuffer))) return {
4081
+ done: true,
4082
+ value: void 0
4083
+ };
4084
+ scanEnd = limit;
4085
+ }
4086
+ }
4087
+ if (startUnmapped) {
4088
+ const mapped = getLogMemoryMap(transactionLog, logBuffer.logId);
4089
+ if (mapped) {
4090
+ logBuffer = mapped;
4091
+ dataView = mapped.dataView;
4092
+ } else {
4093
+ const latestLogId = loadLastPosition(transactionLog, !!readUncommitted).logId;
4094
+ const fileSize = transactionLog.getLogFileSize(logBuffer.logId);
4095
+ if (logBuffer.logId >= latestLogId || fileSize > position) return endOfReadable(untilCursor, logBuffer, position);
4096
+ if (fileSize === 0) startMissing = true;
4097
+ }
4098
+ startUnmapped = false;
4099
+ needsCursorCheck = untilCursor !== void 0;
4100
+ size = 0;
4101
+ }
4102
+ }
4103
+ let timestamp;
4104
+ if (position >= size) {
4105
+ const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
4106
+ size = latestLogId < logBuffer.logId ? 0 : latestSize;
4107
+ if (latestLogId > logBuffer.logId) {
4108
+ size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
4109
+ if (position >= size) {
4110
+ if (untilCursor && LogIterator.reached(untilCursor, logBuffer.logId, position, logBuffer.length > 0 && position >= readableExtent(logBuffer))) return {
4111
+ done: true,
4112
+ value: void 0
4113
+ };
4114
+ const fromLogId = logBuffer.logId - (startMissing ? 1 : 0);
4115
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor);
4116
+ if (nextLogBuffer) {
4117
+ startMissing = false;
4118
+ const unavailable = unavailableLogCount;
4119
+ dataView = nextLogBuffer.dataView;
4120
+ logBuffer = nextLogBuffer;
4121
+ if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
4122
+ else size = latestSize;
4123
+ position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4124
+ previousEndedTransaction = true;
4125
+ if (unavailable > 0) throw new TransactionLogGapError(Math.max(fromLogId, gapFloor), logBuffer.logId, unavailable);
4126
+ }
4127
+ }
4128
+ }
4129
+ scanEnd = untilCursor ? LogIterator.scanEnd(untilCursor, logBuffer.logId, size) ?? size : size;
4130
+ }
4131
+ if (position >= scanEnd) return endOfReadable(untilCursor, logBuffer, position);
4132
+ let jumpRefused = false;
4133
+ let limit = Math.min(size, logBuffer.length);
4134
+ do {
4135
+ try {
4136
+ timestamp = dataView.getFloat64(position);
4137
+ } catch (error) {
4138
+ error.message += ` at position ${position.toString(16)} of log ${logBuffer.logId} (size=${size}, log buffer length=${logBuffer.length})`;
4139
+ throw error;
4140
+ }
4141
+ if (!timestamp) return endOfReadable(untilCursor, logBuffer, position);
4142
+ if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE > limit) {
4143
+ 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);
4144
+ position = broken.nextPosition;
4145
+ previousEndedTransaction = true;
4146
+ throw broken.error;
4147
+ }
4148
+ const length = dataView.getUint32(position + 8);
4149
+ if (length === 0 || position + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length > limit) {
4150
+ 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);
4151
+ position = broken.nextPosition;
4152
+ previousEndedTransaction = true;
4153
+ throw broken.error;
4154
+ }
4155
+ position += TRANSACTION_LOG_ENTRY_HEADER_SIZE;
4156
+ const flags = logBuffer[position - 1];
4157
+ const aborted = (flags & TRANSACTION_LOG_ENTRY_ABORTED_FLAG) !== 0;
4158
+ if (previousEndedTransaction || timestamp !== previousTimestamp || aborted) skippingAborted = aborted;
4159
+ previousEndedTransaction = (flags & TRANSACTION_LOG_ENTRY_LAST_FLAG) !== 0;
4160
+ previousTimestamp = timestamp;
4161
+ let matchesRange;
4162
+ if (skippingAborted) matchesRange = false;
4163
+ else if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
4164
+ else if (exactStart) {
4165
+ matchesRange = false;
4166
+ if (timestamp === start) {
4167
+ matchesRange = !exclusiveStart;
4168
+ foundExactStart = true;
4169
+ stopLogId = 0;
4170
+ }
4171
+ } else matchesRange = (exclusiveStart ? timestamp > start : timestamp >= start) && timestamp < end;
4172
+ if (!matchesRange && stopLogId !== 0 && !jumpRefused && pastStop(logBuffer.logId, position + length, stopLogId, stopPosition)) {
4173
+ if (mayJumpTo(untilCursor, tailLogId, tailPosition)) {
4174
+ stopLogId = 0;
4175
+ position += length;
4176
+ if (skipToTail(transactionLog, logBuffer.logId, position, tailLogId, tailPosition, segmentRemovals)) {
4177
+ if (tailLogBuffer) {
4178
+ logBuffer = tailLogBuffer;
4179
+ dataView = tailLogBuffer.dataView;
3810
4180
  }
4181
+ position = tailPosition;
4182
+ previousEndedTransaction = true;
4183
+ size = 0;
3811
4184
  }
4185
+ tailLogBuffer = void 0;
4186
+ return iterator.next();
3812
4187
  }
4188
+ jumpRefused = true;
3813
4189
  }
3814
- while (position < size) {
3815
- try {
3816
- timestamp = dataView.getFloat64(position);
3817
- } catch (error) {
3818
- error.message += ` at position ${position.toString(16)} of log ${logBuffer.logId} (size=${size}, log buffer length=${logBuffer.length})`;
3819
- throw error;
4190
+ const entryStart = position;
4191
+ position += length;
4192
+ if (matchesRange) return {
4193
+ done: false,
4194
+ value: {
4195
+ timestamp,
4196
+ endTxn: previousEndedTransaction,
4197
+ data: logBuffer.subarray(entryStart, position)
3820
4198
  }
3821
- if (!timestamp) return {
4199
+ };
4200
+ if (position >= scanEnd) {
4201
+ if (untilCursor && LogIterator.reached(untilCursor, logBuffer.logId, position)) return {
3822
4202
  done: true,
3823
4203
  value: void 0
3824
4204
  };
3825
- const limit = readUncommitted ? logBuffer.length : Math.min(size, logBuffer.length);
3826
- if (position + TRANSACTION_LOG_ENTRY_HEADER_SIZE > limit) {
3827
- 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
- position = broken.nextPosition;
3829
- throw broken.error;
3830
- }
3831
- const length = dataView.getUint32(position + 8);
3832
- if (length === 0 || position + TRANSACTION_LOG_ENTRY_HEADER_SIZE + length > limit) {
3833
- 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
- position = broken.nextPosition;
3835
- throw broken.error;
3836
- }
3837
- position += TRANSACTION_LOG_ENTRY_HEADER_SIZE;
3838
- let matchesRange;
3839
- if (foundExactStart) matchesRange = (!exclusiveStart || timestamp !== start) && timestamp < end;
3840
- else if (exactStart) {
3841
- if (timestamp === start) {
3842
- matchesRange = !exclusiveStart;
3843
- foundExactStart = true;
3844
- } else matchesRange = false;
3845
- } else matchesRange = (exclusiveStart ? timestamp > start : timestamp >= start) && timestamp < end;
3846
- const entryStart = position;
3847
- position += length;
3848
- if (matchesRange) return {
3849
- done: false,
3850
- value: {
3851
- timestamp,
3852
- endTxn: Boolean(logBuffer[entryStart - 1] & 1),
3853
- data: logBuffer.subarray(entryStart, position)
4205
+ const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
4206
+ size = latestLogId < logBuffer.logId ? 0 : latestSize;
4207
+ if (latestLogId > logBuffer.logId) {
4208
+ size = logBuffer.size ?? readableExtent(logBuffer);
4209
+ if (!readUncommitted) logBuffer.size = size;
4210
+ if (position < size) {
4211
+ scanEnd = untilCursor ? LogIterator.scanEnd(untilCursor, logBuffer.logId, size) ?? size : size;
4212
+ limit = Math.min(size, logBuffer.length);
4213
+ continue;
3854
4214
  }
3855
- };
3856
- if (position >= size) {
3857
- const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
3858
- size = latestLogId < logBuffer.logId ? 0 : latestSize;
3859
- if (latestLogId > logBuffer.logId) {
3860
- size = logBuffer.size ?? readableExtent(logBuffer);
3861
- if (!readUncommitted) logBuffer.size = size;
3862
- if (position < size) continue;
3863
- const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3864
- if (!nextLogBuffer) return {
3865
- done: true,
3866
- value: void 0
3867
- };
3868
- logBuffer = nextLogBuffer;
3869
- dataView = logBuffer.dataView;
3870
- if (latestLogId === logBuffer.logId) size = latestSize;
3871
- else {
3872
- const cachedSize = logBuffer.size;
3873
- if (cachedSize === void 0) {
3874
- size = readableExtent(logBuffer);
3875
- if (!readUncommitted) logBuffer.size = size;
3876
- } else size = cachedSize;
3877
- }
3878
- position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4215
+ if (untilCursor && LogIterator.reached(untilCursor, logBuffer.logId, position, logBuffer.length > 0 && position >= readableExtent(logBuffer))) return {
4216
+ done: true,
4217
+ value: void 0
4218
+ };
4219
+ const fromLogId = logBuffer.logId;
4220
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor);
4221
+ if (!nextLogBuffer) return endOfReadable(untilCursor, logBuffer, position);
4222
+ const unavailable = unavailableLogCount;
4223
+ logBuffer = nextLogBuffer;
4224
+ dataView = logBuffer.dataView;
4225
+ if (latestLogId === logBuffer.logId) size = latestSize;
4226
+ else {
4227
+ const cachedSize = logBuffer.size;
4228
+ if (cachedSize === void 0) {
4229
+ size = readableExtent(logBuffer);
4230
+ if (!readUncommitted) logBuffer.size = size;
4231
+ } else size = cachedSize;
3879
4232
  }
4233
+ position = TRANSACTION_LOG_FILE_HEADER_SIZE;
4234
+ previousEndedTransaction = true;
4235
+ if (unavailable > 0) throw new TransactionLogGapError(Math.max(fromLogId, gapFloor), logBuffer.logId, unavailable);
3880
4236
  }
4237
+ scanEnd = untilCursor ? LogIterator.scanEnd(untilCursor, logBuffer.logId, size) ?? size : size;
4238
+ limit = Math.min(size, logBuffer.length);
4239
+ }
4240
+ } while (position < scanEnd);
4241
+ return endOfReadable(untilCursor, logBuffer, position);
4242
+ }, function control(operation, fromLogId, fromPosition, atSegmentEnd) {
4243
+ if (operation === "peek") {
4244
+ if (peeked) return peeked;
4245
+ const result = iterator.next();
4246
+ if (!result.done) {
4247
+ peeked = result;
4248
+ needsCursorCheck = true;
4249
+ peekedLogId = logBuffer.logId;
4250
+ peekedPosition = position - result.value.data.length - TRANSACTION_LOG_ENTRY_HEADER_SIZE;
3881
4251
  }
3882
- return {
4252
+ return result;
4253
+ }
4254
+ if (operation === "close") {
4255
+ closed = true;
4256
+ needsCursorCheck = true;
4257
+ peeked = {
3883
4258
  done: true,
3884
4259
  value: void 0
3885
4260
  };
4261
+ logBuffer = void 0;
4262
+ dataView = void 0;
4263
+ untilCursor = void 0;
4264
+ return;
3886
4265
  }
3887
- };
4266
+ if (closed) return;
4267
+ const boundLogId = peeked ? peekedLogId : logBuffer.logId;
4268
+ const boundPosition = peeked ? peekedPosition : position;
4269
+ if (operation === "scanEnd") {
4270
+ if (boundLogId !== fromLogId) return boundLogId > fromLogId ? fromPosition : 0;
4271
+ return boundPosition < fromPosition ? boundPosition : fromPosition;
4272
+ }
4273
+ return pastStop(fromLogId, fromPosition, boundLogId, boundPosition) || !!atSegmentEnd && boundLogId === fromLogId + 1 && boundPosition === TRANSACTION_LOG_FILE_HEADER_SIZE;
4274
+ });
4275
+ return iterator;
3888
4276
  } });
4277
+ function endOfReadable(untilCursor, logBuffer, position) {
4278
+ if (untilCursor && LogIterator.reached(untilCursor, logBuffer.logId, position, logBuffer.length > 0 && position >= readableExtent(logBuffer)) === false) throw new TransactionLogUntilUnreachedError(logBuffer.logId, position);
4279
+ return {
4280
+ done: true,
4281
+ value: void 0
4282
+ };
4283
+ }
4284
+ function pastStop(logId, position, stopLogId, stopPosition) {
4285
+ return logId > stopLogId || logId === stopLogId && position >= stopPosition;
4286
+ }
4287
+ function mayJumpTo(untilCursor, logId, position) {
4288
+ return !untilCursor || LogIterator.reached(untilCursor, logId, position - 1) !== true;
4289
+ }
4290
+ let tailLogBuffer;
4291
+ /**
4292
+ * Whether an exact-start lookup at `position` of `fromLogId` may take its one-shot jump to the tail.
4293
+ * next() calls it on a frame boundary, so a mapping error leaves a resumable cursor; from query()
4294
+ * the error escapes before any iterator exists. The removal
4295
+ * check follows the mapping and holds the store's lock, so a purge either declines the jump (the
4296
+ * scan then reports the gap) or follows it like a purge of segments already read.
4297
+ */
4298
+ function skipToTail(transactionLog, fromLogId, position, tailLogId, tailPosition, segmentRemovals) {
4299
+ tailLogBuffer = void 0;
4300
+ if (tailLogId === fromLogId) return tailPosition > position;
4301
+ if (tailLogId < fromLogId) return false;
4302
+ const mapped = getLogMemoryMap(transactionLog, tailLogId);
4303
+ if (mapped === void 0 || transactionLog._segmentRemovals() !== segmentRemovals) return false;
4304
+ tailLogBuffer = mapped;
4305
+ return true;
4306
+ }
4307
+ let unavailableLogCount = 0;
3889
4308
  /**
3890
4309
  * Maps the next readable segment after `fromLogId`, skipping a run retention has deleted —
3891
4310
  * stopping at the hole wedges the iterator there for every later poll (invariant 30).
@@ -3894,15 +4313,22 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3894
4313
  * `_findPosition(0)`, which names the bottom of the contiguous run ending at the current segment
3895
4314
  * and so lands *past* a survivor sitting between two holes.
3896
4315
  */
3897
- function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId) {
4316
+ function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId, gapFloor) {
4317
+ unavailableLogCount = 0;
3898
4318
  let candidateLogId = fromLogId + 1;
3899
4319
  while (candidateLogId <= latestLogId) {
3900
4320
  const logBuffer = getLogMemoryMap(transactionLog, candidateLogId);
3901
4321
  if (logBuffer) {
3902
4322
  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;
4323
+ } else {
4324
+ const fileSize = transactionLog.getLogFileSize(candidateLogId);
4325
+ if (fileSize > TRANSACTION_LOG_FILE_HEADER_SIZE) return;
4326
+ if (fileSize === 0 && candidateLogId > gapFloor) unavailableLogCount++;
4327
+ }
4328
+ const nextLogId = transactionLog._nextLogId(candidateLogId);
4329
+ if (nextLogId === 0) return;
4330
+ unavailableLogCount += Math.max(0, nextLogId - 1 - Math.max(candidateLogId, gapFloor));
4331
+ candidateLogId = nextLogId;
3906
4332
  }
3907
4333
  }
3908
4334
  /**
@@ -3956,13 +4382,11 @@ function loadLastPosition(transactionLog, readUncommitted) {
3956
4382
  };
3957
4383
  }
3958
4384
 
3959
- //#endregion
3960
- //#region src/index.ts
4385
+ // src/index.ts
3961
4386
  const versions = {
3962
4387
  rocksdb: version,
3963
- "rocksdb-js": "2.12.0"
4388
+ "rocksdb-js": "2.14.0"
3964
4389
  };
3965
4390
 
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 };
4391
+ export { BackgroundError, ConflictKind, CorruptFrameError, CountEstimator, DBI, DBIterator, RocksDatabase, Store, Transaction, TransactionAbortUnresolvedError, TransactionLog, TransactionLogGapError, TransactionLogUntilUnreachedError, backups, constants, coolTransactionLogs, currentThreadId, fileLockRelease, getRegistryStatus, getWriteBufferManagerStats, parseTransactionLog, raiseTimestampFloor, registryStatus, shutdown, stats, steadyClockNow, supportedCompression, tryFileLock, validateTransactionLogStore, versions };
3968
4392
  //# sourceMappingURL=index.mjs.map