@harperfast/rocksdb-js 2.9.1 → 2.11.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.
@@ -160,13 +160,32 @@ function createRL() {
160
160
  const iface = createInterface({ input: stdin, output: stdout, completer });
161
161
  iface.on('SIGINT', onSIGINT);
162
162
  iface.on('SIGBREAK', () => process.exit(0));
163
+ iface.on('close', () => {
164
+ rlClosed = true;
165
+ });
166
+ iface.on('error', (err) => {
167
+ console.error(bad(`stdin error: ${err.message ?? err}`));
168
+ process.exit(1);
169
+ });
163
170
  return iface;
164
171
  }
165
172
 
166
173
  let rl = createRL();
174
+ let lineIterator = rl[Symbol.asyncIterator]();
175
+ let rlClosed = false;
167
176
  let ctrlC = false;
168
177
  let currentAbortController = null;
169
178
 
179
+ function whenAborted(signal) {
180
+ return new Promise((_resolve, reject) => {
181
+ signal.addEventListener(
182
+ 'abort',
183
+ () => reject(Object.assign(new Error('The operation was aborted'), { code: 'ABORT_ERR' })),
184
+ { once: true }
185
+ );
186
+ });
187
+ }
188
+
170
189
  function str(text) {
171
190
  try {
172
191
  if (typeof text === 'symbol') {
@@ -240,17 +259,32 @@ async function run(fn) {
240
259
  return { time: end - start, result };
241
260
  }
242
261
 
262
+ // Returns the next line, or `null` once the iterator itself reports `done`. `close` can
263
+ // fire before lineIterator's already-buffered lines are drained, so `rlClosed` never
264
+ // gates reading — only resume()/setPrompt()/prompt()/pause(), which throw
265
+ // ERR_USE_AFTER_CLOSE once closed.
243
266
  async function ask(prompt) {
244
- rl.resume();
267
+ if (!rlClosed) {
268
+ rl.resume();
269
+ rl.setPrompt(prompt);
270
+ rl.prompt();
271
+ }
272
+ // A second next() would queue behind this one and steal the next real line out from
273
+ // under a Ctrl-C retry below, so retries re-await this same promise instead.
274
+ const pending = lineIterator.next();
245
275
  while (true) {
246
276
  try {
247
277
  currentAbortController = new AbortController();
248
- const answer = await rl.question(prompt, { signal: currentAbortController.signal });
249
- rl.pause();
278
+ const result = await Promise.race([pending, whenAborted(currentAbortController.signal)]);
250
279
  ctrlC = false;
251
- return answer;
280
+ if (result.done) return null;
281
+ if (!rlClosed) rl.pause();
282
+ return result.value;
252
283
  } catch (err) {
253
284
  if (err.code !== 'ABORT_ERR') throw err;
285
+ if (rlClosed) continue;
286
+ rl.clearLine();
287
+ rl.prompt();
254
288
  }
255
289
  }
256
290
  }
@@ -842,6 +876,12 @@ async function removeCommand(args) {
842
876
  }
843
877
 
844
878
  async function replCommand() {
879
+ // A non-TTY stdin can't feed the sub-REPL interactively, and input already read out
880
+ // of it has no defined recipient once this CLI's interface closes.
881
+ if (!stdin.isTTY) {
882
+ console.log(bad('The JS sub-REPL needs an interactive terminal; stdin is not one here.\n'));
883
+ process.exit(1);
884
+ }
845
885
  const history = [...rl.history];
846
886
  rl.close();
847
887
  r = repl.start({ prompt: styleText('magenta', 'js> '), useColors: true, useGlobal: false });
@@ -852,6 +892,8 @@ async function replCommand() {
852
892
  console.log();
853
893
  rl = createRL();
854
894
  rl.history = history;
895
+ lineIterator = rl[Symbol.asyncIterator]();
896
+ rlClosed = false;
855
897
  }
856
898
 
857
899
  function statsCommand() {
@@ -942,7 +984,9 @@ async function main() {
942
984
  console.log(`Type "help" for more information.`);
943
985
 
944
986
  while (true) {
945
- const line = (await ask('> ')).trim().split(/[ \t]+/);
987
+ const raw = await ask('> ');
988
+ if (raw === null) break;
989
+ const line = raw.trim().split(/[ \t]+/);
946
990
  const command = line[0];
947
991
  if (!command) continue;
948
992
  const commandFn = COMMANDS[command];
package/dist/index.cjs CHANGED
@@ -130,10 +130,60 @@ const NativeDatabase = binding.Database;
130
130
  const NativeIterator = binding.Iterator;
131
131
  const NativeTransaction = binding.Transaction;
132
132
  const TransactionLog = binding.TransactionLog;
133
- const registryStatus = binding.registryStatus;
133
+ /**
134
+ * Returns the status of every database currently held by the native registry.
135
+ * Walks every registered descriptor, so it samples live state rather than
136
+ * reading a stored property.
137
+ */
138
+ const getRegistryStatus = binding.registryStatus;
139
+ /**
140
+ * @deprecated Use {@link getRegistryStatus} instead. This alias will be removed
141
+ * in v3.0.0.
142
+ */
143
+ const registryStatus = getRegistryStatus;
144
+ /**
145
+ * Flushes every open database and waits for outstanding compactions, then
146
+ * releases the registry.
147
+ *
148
+ * **Throws** the first failure it encounters: a close whose native teardown
149
+ * failed (that descriptor stays quarantined — call `shutdown()` again to retry
150
+ * it), or a `lifecycleWaitSeconds` timeout waiting on a concurrent
151
+ * open/destroy/shutdown. A quarantining close also emits
152
+ * `database:closeFailed`; a timeout has no event, so the throw is its only
153
+ * signal.
154
+ *
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
+ * ```
168
+ */
134
169
  const shutdown = binding.shutdown;
135
170
  const currentThreadId = binding.currentThreadId;
136
171
  /**
172
+ * Reads the process-wide steady clock: `std::chrono::steady_clock` as
173
+ * fractional milliseconds from an unspecified origin that is fixed for the life
174
+ * 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
176
+ * — is in one domain, and the difference between two samples is real elapsed
177
+ * time unaffected by wall-clock steps. Non-decreasing but not unique (two
178
+ * samples may be equal; treat `==` as "not later"). Windows QPC samples from
179
+ * different threads within one native tick also have ambiguous ordering.
180
+ * Clock readings alone do not prove causality. Not comparable with
181
+ * `Date.now()`, `db.getMonotonicTimestamp()` or transaction timestamps, and not
182
+ * meaningful across processes or restarts. Time spent in host suspend is
183
+ * platform-defined. No open database is needed.
184
+ */
185
+ const steadyClockNow = binding.steadyClockNow;
186
+ /**
137
187
  * Advises the kernel that the file-backed pages of every mapped transaction log
138
188
  * are cold (Linux MADV_COLD), so they are reclaimed first under memory pressure
139
189
  * without being freed — useful during replication catch-up, where a full read of
@@ -154,11 +204,32 @@ const coolTransactionLogs = binding.coolTransactionLogs;
154
204
  */
155
205
  const transactionLogMapCount = binding.transactionLogMapCount;
156
206
  /**
207
+ * Number of coordinated-retry wake callbacks registered on verification-table
208
+ * locks across the process. Internal — used by tests to verify that a park
209
+ * ending by timeout, env teardown, or close leaves no registration behind.
210
+ */
211
+ const lockWakeCallbackCount = binding.lockWakeCallbackCount;
212
+ /**
157
213
  * Test-only: force the next `count` transaction commits to fail with TryAgain (rolled back, so
158
214
  * no data is committed), reproducing a stranded-snapshot conflict deterministically. Pass 0 to
159
215
  * disarm. Used by the ERR_TRY_AGAIN retry regression test.
160
216
  */
161
217
  const forceTryAgainForTesting = binding.forceTryAgainForTesting;
218
+ /**
219
+ * Test-only seam: makes the physical column-family drop report failure.
220
+ * `0` = inert, `1` = fail without dropping (the retry performs the drop),
221
+ * `2` = drop for real and then report failure (the retry resolves as
222
+ * "already dropped"). Process-global, like `forceTryAgainForTesting`.
223
+ */
224
+ const forceDropFailureForTesting = binding.forceDropFailureForTesting;
225
+ /** Parks one selected transaction write after its retirement precheck. */
226
+ const setTransactionStagingDelayForTesting = binding.setTransactionStagingDelayForTesting;
227
+ /** Reports whether the selected transaction write is parked. */
228
+ const isTransactionStagingDelayedForTesting = binding.isTransactionStagingDelayedForTesting;
229
+ /** Reports whether a transaction is parked at the native commit seam. */
230
+ const isTransactionCommitExecuteDelayedForTesting = binding.isTransactionCommitExecuteDelayedForTesting;
231
+ const setTransactionCommitAdmissionDelayForTesting = binding.setTransactionCommitAdmissionDelayForTesting;
232
+ const isTransactionCommitAdmissionDelayedForTesting = binding.isTransactionCommitAdmissionDelayedForTesting;
162
233
  /** Delays one selected watchdog join in the concurrent-shutdown regression test. */
163
234
  const setWriteBufferManagerJoinDelayForTesting = binding.setWriteBufferManagerJoinDelayForTesting;
164
235
  /**
@@ -1676,6 +1747,7 @@ var Store = class {
1676
1747
  /**
1677
1748
  * The target maximum size of a transaction log before rotation. Transactions
1678
1749
  * are never split across files, so one transaction may exceed this target.
1750
+ * Defaults to 16 MiB. `0` (formerly unlimited) is rejected at open.
1679
1751
  */
1680
1752
  transactionLogMaxSize;
1681
1753
  /**
@@ -1690,6 +1762,11 @@ var Store = class {
1690
1762
  */
1691
1763
  transactionLogsPath;
1692
1764
  /**
1765
+ * The transaction log whose batch keys this process originates; seeds the
1766
+ * monotonic timestamp floor at open. See `StoreOptions.timestampFloorLog`.
1767
+ */
1768
+ timestampFloorLog;
1769
+ /**
1693
1770
  * Whether this store's column family participates in the VerificationTable.
1694
1771
  */
1695
1772
  verificationTable;
@@ -1748,6 +1825,7 @@ var Store = class {
1748
1825
  this.transactionLogMaxSize = options?.transactionLogMaxSize;
1749
1826
  this.transactionLogRetention = options?.transactionLogRetention;
1750
1827
  this.transactionLogsPath = options?.transactionLogsPath;
1828
+ this.timestampFloorLog = options?.timestampFloorLog;
1751
1829
  this.verificationTable = options?.verificationTable;
1752
1830
  this.writeBufferSize = options?.writeBufferSize;
1753
1831
  this.writeKey = writeKey;
@@ -2112,6 +2190,7 @@ var Store = class {
2112
2190
  transactionLogMaxSize: this.transactionLogMaxSize,
2113
2191
  transactionLogRetentionMs: this.transactionLogRetention ? parseDuration(this.transactionLogRetention) : void 0,
2114
2192
  transactionLogsPath: this.transactionLogsPath,
2193
+ timestampFloorLog: this.timestampFloorLog,
2115
2194
  verificationTable: this.verificationTable,
2116
2195
  writeBufferSize: this.writeBufferSize
2117
2196
  });
@@ -2119,12 +2198,12 @@ var Store = class {
2119
2198
  return false;
2120
2199
  }
2121
2200
  putSync(context, key, value, options) {
2122
- if (!this.db.opened) throw new Error("Database not open");
2201
+ if (!this.db.opened) throw new Error(this.db.closing ? "Database is closing" : "Database not open");
2123
2202
  const valueBuffer = this.encodeValue(value);
2124
2203
  context.putSync(this.encodeKey(key), valueBuffer, this.getTxnId(options));
2125
2204
  }
2126
2205
  removeSync(context, key, options) {
2127
- if (!this.db.opened) throw new Error("Database not open");
2206
+ if (!this.db.opened) throw new Error(this.db.closing ? "Database is closing" : "Database not open");
2128
2207
  context.removeSync(this.encodeKey(key), this.getTxnId(options));
2129
2208
  }
2130
2209
  /**
@@ -2593,6 +2672,66 @@ var RocksDatabase = class RocksDatabase extends DBI {
2593
2672
  return this.store.db.getCompression();
2594
2673
  }
2595
2674
  /**
2675
+ * Dynamically changes the compression algorithm (and optional level) for
2676
+ * this database's column family on an already-open database — no close, no
2677
+ * reopen, and it does not conflict with another handle that already has
2678
+ * this column family open (unlike an explicit open-time `compression`
2679
+ * mismatch). That other handle's live options change too, silently,
2680
+ * without notice; a later explicit reopen of it is compared against the
2681
+ * new value, so a stale request it still holds can then conflict. Backed
2682
+ * by RocksDB's `DB::SetOptions()`, which documents both `compression` and
2683
+ * `blob_compression_type` as mutable column family options.
2684
+ *
2685
+ * This governs only *newly written* files (the next flush and any future
2686
+ * compaction output) going forward; SST and blob files already on disk
2687
+ * keep their existing compression until they are rewritten by a later
2688
+ * compaction. `compact()`'s default options can skip already-bottommost
2689
+ * files; use `compact({ bottommost: true })` to rewrite them under the new
2690
+ * codec. This is the live-mutation counterpart to the open-time
2691
+ * `compression` option — see the README's Compression section for when to
2692
+ * use each.
2693
+ *
2694
+ * @example
2695
+ * ```typescript
2696
+ * const db = RocksDatabase.open('/path/to/db', { compression: 'none' });
2697
+ * db.setCompression({ algorithm: 'zstd', level: 19 });
2698
+ * // new writes are now compressed with zstd; existing SSTs are untouched
2699
+ * // until compacted.
2700
+ * ```
2701
+ */
2702
+ setCompression(compression) {
2703
+ const { compression: algorithm, compressionLevel } = normalizeCompression(compression);
2704
+ if (algorithm === void 0) throw new TypeError("setCompression requires a compression algorithm");
2705
+ try {
2706
+ this.store.db.setCompression(algorithm, compressionLevel);
2707
+ } catch (error) {
2708
+ if (error?.appliedInMemoryOnly) this.syncStoreCompressionFromLive();
2709
+ throw error;
2710
+ }
2711
+ this.syncStoreCompressionFromLive();
2712
+ }
2713
+ /**
2714
+ * Resyncs the store's cached open-time compression option from the live
2715
+ * database state after `setCompression()`. Also clears
2716
+ * `compressionForAllColumnFamilies`: that flag means "apply this option to
2717
+ * every column family opened for this database", which no longer describes
2718
+ * a live change scoped to a single column family — leaving it set would
2719
+ * restamp every other family with this algorithm on the next
2720
+ * same-instance `close()` + `open()`.
2721
+ *
2722
+ * The flag clear is unconditional (a plain assignment, so it cannot itself
2723
+ * fail) and comes first: a concurrent close racing the live-state read
2724
+ * below throws "Database not open" from the getter, and that read failing
2725
+ * must not also skip the flag clear -- otherwise the same restamp this
2726
+ * method exists to prevent survives on the next open().
2727
+ */
2728
+ syncStoreCompressionFromLive() {
2729
+ this.store.compressionForAllColumnFamilies = false;
2730
+ try {
2731
+ this.store.compression = this.compression;
2732
+ } catch {}
2733
+ }
2734
+ /**
2596
2735
  * The informational-log settings currently in effect for this database,
2597
2736
  * read live from RocksDB, as `{ maxLogFileSize, infoLogLevel }`. These are
2598
2737
  * database-wide settings (not per-column-family): `maxLogFileSize` bounds
@@ -2628,7 +2767,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
2628
2767
  * native binding use namespaced keys (e.g. `'transactionLog:warning'`).
2629
2768
  *
2630
2769
  * Listeners are not tied to any specific database — they fire for every
2631
- * matching event emitted in this process.
2770
+ * matching event emitted in this process. Native lifecycle failures use
2771
+ * `'database:closeFailed'` with `(path, error)` string arguments. The event
2772
+ * reports both completed and incomplete teardowns; an incomplete close can
2773
+ * be retried by `shutdown()` or explicitly deleted by `destroy()`.
2632
2774
  *
2633
2775
  * @example
2634
2776
  * ```typescript
@@ -2712,7 +2854,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
2712
2854
  this.store.db.catchUpWithPrimarySync();
2713
2855
  }
2714
2856
  destroy() {
2715
- this.store.db.destroy();
2857
+ this.store.db.destroy(this.store.readOnly);
2716
2858
  }
2717
2859
  async drop() {
2718
2860
  return new Promise((resolve, reject) => {
@@ -3578,8 +3720,8 @@ function frameFits(dataView, pos, dataEnd) {
3578
3720
  * terminator, let a chain "end" anywhere in megabytes of padding. Against the written extent the
3579
3721
  * end is a single offset, so a chain landing on it is ~1/2^32 to be coincidence.
3580
3722
  *
3581
- * The JS counterpart of `validFramingResumes()` in `transaction_log_recovery.cpp`, which answers
3582
- * the same question at open time but keeps only the yes/no, discarding the offset.
3723
+ * The JS counterpart of `findFramingResumeOffset()` in `transaction_log_recovery.cpp`, which the
3724
+ * open-time scan and the timestamp index use to keep walking past the same break.
3583
3725
  */
3584
3726
  function findResyncPosition(dataView, from, dataEnd) {
3585
3727
  const lastStart = dataEnd - TRANSACTION_LOG_ENTRY_HEADER_SIZE;
@@ -3593,18 +3735,11 @@ function findResyncPosition(dataView, from, dataEnd) {
3593
3735
  }
3594
3736
  }
3595
3737
  /**
3596
- * Builds the report for a broken frame, with the offset iteration should continue from. Resolves
3597
- * the written extent here rather than per frame: `getLogFileSize` crosses into native and takes
3598
- * the store mutex, which a healthy read must never pay.
3738
+ * Builds the report for a broken frame, with the offset iteration should continue from.
3599
3739
  */
3600
3740
  function corruptFrame(message, transactionLog, dataView, logBuffer, position, limit, size, readUncommitted) {
3601
3741
  let dataEnd = limit;
3602
- if (readUncommitted) try {
3603
- const writtenExtent = transactionLog.getLogFileSize(logBuffer.logId);
3604
- dataEnd = writtenExtent > 0 ? Math.min(logBuffer.length, writtenExtent) : 0;
3605
- } catch {
3606
- dataEnd = 0;
3607
- }
3742
+ if (readUncommitted) dataEnd = Math.min(logBuffer.length, logBuffer.size ?? readableExtent(logBuffer));
3608
3743
  const resyncPosition = findResyncPosition(dataView, position + 1, dataEnd);
3609
3744
  return {
3610
3745
  error: new CorruptFrameError(message, logBuffer.logId, position, resyncPosition),
@@ -3634,7 +3769,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3634
3769
  let logId = latestLogId;
3635
3770
  let position = 0;
3636
3771
  let dataView;
3637
- let logBuffer = this._currentLogBuffer;
3772
+ let logBuffer = this._currentLogBuffer?.deref();
3638
3773
  let foundExactStart = false;
3639
3774
  if (start === void 0 && !startFromLastFlushed) {
3640
3775
  position = size;
@@ -3652,7 +3787,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3652
3787
  }
3653
3788
  if (logBuffer === void 0 || logBuffer.logId !== logId) {
3654
3789
  logBuffer = getLogMemoryMap(this, logId);
3655
- if (logBuffer && latestLogId === logId && !readUncommitted) this._currentLogBuffer = logBuffer;
3790
+ if (logBuffer && latestLogId === logId && !readUncommitted && this._currentLogBuffer?.deref() !== logBuffer) this._currentLogBuffer = new WeakRef(logBuffer);
3656
3791
  if (logBuffer === void 0) {
3657
3792
  logBuffer = Buffer.alloc(0);
3658
3793
  logBuffer.logId = 0;
@@ -3662,9 +3797,11 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3662
3797
  }
3663
3798
  }
3664
3799
  dataView = logBuffer.dataView;
3665
- if (latestLogId !== logId) {
3666
- size = logBuffer.size;
3667
- if (size === void 0) size = logBuffer.size = this.getLogFileSize(logId);
3800
+ if (latestLogId < logId) size = 0;
3801
+ else if (latestLogId > logId) {
3802
+ const cachedSize = logBuffer.size;
3803
+ if (cachedSize === void 0) size = logBuffer.size = readableExtent(logBuffer);
3804
+ else size = cachedSize;
3668
3805
  }
3669
3806
  return {
3670
3807
  [Symbol.iterator]() {
@@ -3674,15 +3811,15 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3674
3811
  let timestamp;
3675
3812
  if (position >= size) {
3676
3813
  const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
3677
- size = latestSize;
3814
+ size = latestLogId < logBuffer.logId ? 0 : latestSize;
3678
3815
  if (latestLogId > logBuffer.logId) {
3679
- size = logBuffer.size ?? (logBuffer.size = transactionLog.getLogFileSize(logBuffer.logId));
3816
+ size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3680
3817
  if (position >= size) {
3681
- const nextLogBuffer = getLogMemoryMap(transactionLog, logBuffer.logId + 1);
3818
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3682
3819
  if (nextLogBuffer) {
3683
3820
  dataView = nextLogBuffer.dataView;
3684
3821
  logBuffer = nextLogBuffer;
3685
- if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = transactionLog.getLogFileSize(logBuffer.logId));
3822
+ if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3686
3823
  else size = latestSize;
3687
3824
  position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3688
3825
  }
@@ -3733,19 +3870,25 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3733
3870
  };
3734
3871
  if (position >= size) {
3735
3872
  const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
3736
- size = latestSize;
3873
+ size = latestLogId < logBuffer.logId ? 0 : latestSize;
3737
3874
  if (latestLogId > logBuffer.logId) {
3738
- const nextLogBuffer = getLogMemoryMap(transactionLog, logBuffer.logId + 1);
3875
+ size = logBuffer.size ?? readableExtent(logBuffer);
3876
+ if (!readUncommitted) logBuffer.size = size;
3877
+ if (position < size) continue;
3878
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3739
3879
  if (!nextLogBuffer) return {
3740
3880
  done: true,
3741
3881
  value: void 0
3742
3882
  };
3743
3883
  logBuffer = nextLogBuffer;
3744
3884
  dataView = logBuffer.dataView;
3745
- size = logBuffer.size;
3746
- if (size == void 0) {
3747
- size = transactionLog.getLogFileSize(logBuffer.logId);
3748
- if (!readUncommitted) logBuffer.size = size;
3885
+ if (latestLogId === logBuffer.logId) size = latestSize;
3886
+ else {
3887
+ const cachedSize = logBuffer.size;
3888
+ if (cachedSize === void 0) {
3889
+ size = readableExtent(logBuffer);
3890
+ if (!readUncommitted) logBuffer.size = size;
3891
+ } else size = cachedSize;
3749
3892
  }
3750
3893
  position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3751
3894
  }
@@ -3758,6 +3901,35 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3758
3901
  }
3759
3902
  };
3760
3903
  } });
3904
+ /**
3905
+ * Maps the next readable segment after `fromLogId`, skipping a run retention has deleted —
3906
+ * stopping at the hole wedges the iterator there for every later poll (invariant 30).
3907
+ *
3908
+ * The jump target is `_nextLogId()`, the successor over the store's registered segments — *not*
3909
+ * `_findPosition(0)`, which names the bottom of the contiguous run ending at the current segment
3910
+ * and so lands *past* a survivor sitting between two holes.
3911
+ */
3912
+ function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId) {
3913
+ let candidateLogId = fromLogId + 1;
3914
+ while (candidateLogId <= latestLogId) {
3915
+ const logBuffer = getLogMemoryMap(transactionLog, candidateLogId);
3916
+ if (logBuffer) {
3917
+ if (candidateLogId === latestLogId || readableExtent(logBuffer) > TRANSACTION_LOG_FILE_HEADER_SIZE) return logBuffer;
3918
+ } else if (transactionLog.getLogFileSize(candidateLogId) > TRANSACTION_LOG_FILE_HEADER_SIZE) return;
3919
+ candidateLogId = transactionLog._nextLogId(candidateLogId);
3920
+ if (candidateLogId === 0) return;
3921
+ }
3922
+ }
3923
+ /**
3924
+ * The bound for reading `logBuffer`. Native advances this append-owned extent only after a
3925
+ * successful write. It remains attached to the mapping after purge forgets the file object, so
3926
+ * readers retain committed history without interpreting a partial failed append as another frame.
3927
+ */
3928
+ function readableExtent(logBuffer) {
3929
+ const extent = logBuffer.readableExtent;
3930
+ if (extent === void 0) throw new Error(`Transaction log buffer ${logBuffer.logId} has no readableExtent`);
3931
+ return Math.min(logBuffer.length, extent);
3932
+ }
3761
3933
  function getLogMemoryMap(transactionLog, logId) {
3762
3934
  if (logId <= 0) return;
3763
3935
  let logBuffer = transactionLog._logBuffers.get(logId)?.deref();
@@ -3803,7 +3975,7 @@ function loadLastPosition(transactionLog, readUncommitted) {
3803
3975
  //#region src/index.ts
3804
3976
  const versions = {
3805
3977
  rocksdb: version,
3806
- "rocksdb-js": "2.9.1"
3978
+ "rocksdb-js": "2.11.0"
3807
3979
  };
3808
3980
 
3809
3981
  //#endregion
@@ -3821,11 +3993,13 @@ exports.constants = constants;
3821
3993
  exports.coolTransactionLogs = coolTransactionLogs;
3822
3994
  exports.currentThreadId = currentThreadId;
3823
3995
  exports.fileLockRelease = fileLockRelease;
3996
+ exports.getRegistryStatus = getRegistryStatus;
3824
3997
  exports.getWriteBufferManagerStats = getWriteBufferManagerStats;
3825
3998
  exports.parseTransactionLog = parseTransactionLog;
3826
3999
  exports.registryStatus = registryStatus;
3827
4000
  exports.shutdown = shutdown;
3828
4001
  exports.stats = stats;
4002
+ exports.steadyClockNow = steadyClockNow;
3829
4003
  exports.supportedCompression = supportedCompression;
3830
4004
  exports.tryFileLock = tryFileLock;
3831
4005
  exports.validateTransactionLogStore = validateTransactionLogStore;