@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.
package/dist/index.mjs CHANGED
@@ -105,10 +105,60 @@ const NativeDatabase = binding.Database;
105
105
  const NativeIterator = binding.Iterator;
106
106
  const NativeTransaction = binding.Transaction;
107
107
  const TransactionLog = binding.TransactionLog;
108
- const registryStatus = binding.registryStatus;
108
+ /**
109
+ * Returns the status of every database currently held by the native registry.
110
+ * Walks every registered descriptor, so it samples live state rather than
111
+ * reading a stored property.
112
+ */
113
+ const getRegistryStatus = binding.registryStatus;
114
+ /**
115
+ * @deprecated Use {@link getRegistryStatus} instead. This alias will be removed
116
+ * in v3.0.0.
117
+ */
118
+ const registryStatus = getRegistryStatus;
119
+ /**
120
+ * Flushes every open database and waits for outstanding compactions, then
121
+ * releases the registry.
122
+ *
123
+ * **Throws** the first failure it encounters: a close whose native teardown
124
+ * failed (that descriptor stays quarantined — call `shutdown()` again to retry
125
+ * it), or a `lifecycleWaitSeconds` timeout waiting on a concurrent
126
+ * open/destroy/shutdown. A quarantining close also emits
127
+ * `database:closeFailed`; a timeout has no event, so the throw is its only
128
+ * signal.
129
+ *
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
+ * ```
143
+ */
109
144
  const shutdown = binding.shutdown;
110
145
  const currentThreadId = binding.currentThreadId;
111
146
  /**
147
+ * Reads the process-wide steady clock: `std::chrono::steady_clock` as
148
+ * fractional milliseconds from an unspecified origin that is fixed for the life
149
+ * 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
151
+ * — is in one domain, and the difference between two samples is real elapsed
152
+ * time unaffected by wall-clock steps. Non-decreasing but not unique (two
153
+ * samples may be equal; treat `==` as "not later"). Windows QPC samples from
154
+ * different threads within one native tick also have ambiguous ordering.
155
+ * Clock readings alone do not prove causality. Not comparable with
156
+ * `Date.now()`, `db.getMonotonicTimestamp()` or transaction timestamps, and not
157
+ * meaningful across processes or restarts. Time spent in host suspend is
158
+ * platform-defined. No open database is needed.
159
+ */
160
+ const steadyClockNow = binding.steadyClockNow;
161
+ /**
112
162
  * Advises the kernel that the file-backed pages of every mapped transaction log
113
163
  * are cold (Linux MADV_COLD), so they are reclaimed first under memory pressure
114
164
  * without being freed — useful during replication catch-up, where a full read of
@@ -129,11 +179,32 @@ const coolTransactionLogs = binding.coolTransactionLogs;
129
179
  */
130
180
  const transactionLogMapCount = binding.transactionLogMapCount;
131
181
  /**
182
+ * Number of coordinated-retry wake callbacks registered on verification-table
183
+ * locks across the process. Internal — used by tests to verify that a park
184
+ * ending by timeout, env teardown, or close leaves no registration behind.
185
+ */
186
+ const lockWakeCallbackCount = binding.lockWakeCallbackCount;
187
+ /**
132
188
  * Test-only: force the next `count` transaction commits to fail with TryAgain (rolled back, so
133
189
  * no data is committed), reproducing a stranded-snapshot conflict deterministically. Pass 0 to
134
190
  * disarm. Used by the ERR_TRY_AGAIN retry regression test.
135
191
  */
136
192
  const forceTryAgainForTesting = binding.forceTryAgainForTesting;
193
+ /**
194
+ * Test-only seam: makes the physical column-family drop report failure.
195
+ * `0` = inert, `1` = fail without dropping (the retry performs the drop),
196
+ * `2` = drop for real and then report failure (the retry resolves as
197
+ * "already dropped"). Process-global, like `forceTryAgainForTesting`.
198
+ */
199
+ const forceDropFailureForTesting = binding.forceDropFailureForTesting;
200
+ /** Parks one selected transaction write after its retirement precheck. */
201
+ const setTransactionStagingDelayForTesting = binding.setTransactionStagingDelayForTesting;
202
+ /** Reports whether the selected transaction write is parked. */
203
+ const isTransactionStagingDelayedForTesting = binding.isTransactionStagingDelayedForTesting;
204
+ /** Reports whether a transaction is parked at the native commit seam. */
205
+ const isTransactionCommitExecuteDelayedForTesting = binding.isTransactionCommitExecuteDelayedForTesting;
206
+ const setTransactionCommitAdmissionDelayForTesting = binding.setTransactionCommitAdmissionDelayForTesting;
207
+ const isTransactionCommitAdmissionDelayedForTesting = binding.isTransactionCommitAdmissionDelayedForTesting;
137
208
  /** Delays one selected watchdog join in the concurrent-shutdown regression test. */
138
209
  const setWriteBufferManagerJoinDelayForTesting = binding.setWriteBufferManagerJoinDelayForTesting;
139
210
  /**
@@ -1651,6 +1722,7 @@ var Store = class {
1651
1722
  /**
1652
1723
  * The target maximum size of a transaction log before rotation. Transactions
1653
1724
  * are never split across files, so one transaction may exceed this target.
1725
+ * Defaults to 16 MiB. `0` (formerly unlimited) is rejected at open.
1654
1726
  */
1655
1727
  transactionLogMaxSize;
1656
1728
  /**
@@ -1665,6 +1737,11 @@ var Store = class {
1665
1737
  */
1666
1738
  transactionLogsPath;
1667
1739
  /**
1740
+ * The transaction log whose batch keys this process originates; seeds the
1741
+ * monotonic timestamp floor at open. See `StoreOptions.timestampFloorLog`.
1742
+ */
1743
+ timestampFloorLog;
1744
+ /**
1668
1745
  * Whether this store's column family participates in the VerificationTable.
1669
1746
  */
1670
1747
  verificationTable;
@@ -1723,6 +1800,7 @@ var Store = class {
1723
1800
  this.transactionLogMaxSize = options?.transactionLogMaxSize;
1724
1801
  this.transactionLogRetention = options?.transactionLogRetention;
1725
1802
  this.transactionLogsPath = options?.transactionLogsPath;
1803
+ this.timestampFloorLog = options?.timestampFloorLog;
1726
1804
  this.verificationTable = options?.verificationTable;
1727
1805
  this.writeBufferSize = options?.writeBufferSize;
1728
1806
  this.writeKey = writeKey;
@@ -2087,6 +2165,7 @@ var Store = class {
2087
2165
  transactionLogMaxSize: this.transactionLogMaxSize,
2088
2166
  transactionLogRetentionMs: this.transactionLogRetention ? parseDuration(this.transactionLogRetention) : void 0,
2089
2167
  transactionLogsPath: this.transactionLogsPath,
2168
+ timestampFloorLog: this.timestampFloorLog,
2090
2169
  verificationTable: this.verificationTable,
2091
2170
  writeBufferSize: this.writeBufferSize
2092
2171
  });
@@ -2094,12 +2173,12 @@ var Store = class {
2094
2173
  return false;
2095
2174
  }
2096
2175
  putSync(context, key, value, options) {
2097
- if (!this.db.opened) throw new Error("Database not open");
2176
+ if (!this.db.opened) throw new Error(this.db.closing ? "Database is closing" : "Database not open");
2098
2177
  const valueBuffer = this.encodeValue(value);
2099
2178
  context.putSync(this.encodeKey(key), valueBuffer, this.getTxnId(options));
2100
2179
  }
2101
2180
  removeSync(context, key, options) {
2102
- if (!this.db.opened) throw new Error("Database not open");
2181
+ if (!this.db.opened) throw new Error(this.db.closing ? "Database is closing" : "Database not open");
2103
2182
  context.removeSync(this.encodeKey(key), this.getTxnId(options));
2104
2183
  }
2105
2184
  /**
@@ -2568,6 +2647,66 @@ var RocksDatabase = class RocksDatabase extends DBI {
2568
2647
  return this.store.db.getCompression();
2569
2648
  }
2570
2649
  /**
2650
+ * Dynamically changes the compression algorithm (and optional level) for
2651
+ * this database's column family on an already-open database — no close, no
2652
+ * reopen, and it does not conflict with another handle that already has
2653
+ * this column family open (unlike an explicit open-time `compression`
2654
+ * mismatch). That other handle's live options change too, silently,
2655
+ * without notice; a later explicit reopen of it is compared against the
2656
+ * new value, so a stale request it still holds can then conflict. Backed
2657
+ * by RocksDB's `DB::SetOptions()`, which documents both `compression` and
2658
+ * `blob_compression_type` as mutable column family options.
2659
+ *
2660
+ * This governs only *newly written* files (the next flush and any future
2661
+ * compaction output) going forward; SST and blob files already on disk
2662
+ * keep their existing compression until they are rewritten by a later
2663
+ * compaction. `compact()`'s default options can skip already-bottommost
2664
+ * files; use `compact({ bottommost: true })` to rewrite them under the new
2665
+ * codec. This is the live-mutation counterpart to the open-time
2666
+ * `compression` option — see the README's Compression section for when to
2667
+ * use each.
2668
+ *
2669
+ * @example
2670
+ * ```typescript
2671
+ * const db = RocksDatabase.open('/path/to/db', { compression: 'none' });
2672
+ * db.setCompression({ algorithm: 'zstd', level: 19 });
2673
+ * // new writes are now compressed with zstd; existing SSTs are untouched
2674
+ * // until compacted.
2675
+ * ```
2676
+ */
2677
+ setCompression(compression) {
2678
+ const { compression: algorithm, compressionLevel } = normalizeCompression(compression);
2679
+ if (algorithm === void 0) throw new TypeError("setCompression requires a compression algorithm");
2680
+ try {
2681
+ this.store.db.setCompression(algorithm, compressionLevel);
2682
+ } catch (error) {
2683
+ if (error?.appliedInMemoryOnly) this.syncStoreCompressionFromLive();
2684
+ throw error;
2685
+ }
2686
+ this.syncStoreCompressionFromLive();
2687
+ }
2688
+ /**
2689
+ * Resyncs the store's cached open-time compression option from the live
2690
+ * database state after `setCompression()`. Also clears
2691
+ * `compressionForAllColumnFamilies`: that flag means "apply this option to
2692
+ * every column family opened for this database", which no longer describes
2693
+ * a live change scoped to a single column family — leaving it set would
2694
+ * restamp every other family with this algorithm on the next
2695
+ * same-instance `close()` + `open()`.
2696
+ *
2697
+ * The flag clear is unconditional (a plain assignment, so it cannot itself
2698
+ * fail) and comes first: a concurrent close racing the live-state read
2699
+ * below throws "Database not open" from the getter, and that read failing
2700
+ * must not also skip the flag clear -- otherwise the same restamp this
2701
+ * method exists to prevent survives on the next open().
2702
+ */
2703
+ syncStoreCompressionFromLive() {
2704
+ this.store.compressionForAllColumnFamilies = false;
2705
+ try {
2706
+ this.store.compression = this.compression;
2707
+ } catch {}
2708
+ }
2709
+ /**
2571
2710
  * The informational-log settings currently in effect for this database,
2572
2711
  * read live from RocksDB, as `{ maxLogFileSize, infoLogLevel }`. These are
2573
2712
  * database-wide settings (not per-column-family): `maxLogFileSize` bounds
@@ -2603,7 +2742,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
2603
2742
  * native binding use namespaced keys (e.g. `'transactionLog:warning'`).
2604
2743
  *
2605
2744
  * Listeners are not tied to any specific database — they fire for every
2606
- * matching event emitted in this process.
2745
+ * matching event emitted in this process. Native lifecycle failures use
2746
+ * `'database:closeFailed'` with `(path, error)` string arguments. The event
2747
+ * reports both completed and incomplete teardowns; an incomplete close can
2748
+ * be retried by `shutdown()` or explicitly deleted by `destroy()`.
2607
2749
  *
2608
2750
  * @example
2609
2751
  * ```typescript
@@ -2687,7 +2829,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
2687
2829
  this.store.db.catchUpWithPrimarySync();
2688
2830
  }
2689
2831
  destroy() {
2690
- this.store.db.destroy();
2832
+ this.store.db.destroy(this.store.readOnly);
2691
2833
  }
2692
2834
  async drop() {
2693
2835
  return new Promise((resolve, reject) => {
@@ -3553,8 +3695,8 @@ function frameFits(dataView, pos, dataEnd) {
3553
3695
  * terminator, let a chain "end" anywhere in megabytes of padding. Against the written extent the
3554
3696
  * end is a single offset, so a chain landing on it is ~1/2^32 to be coincidence.
3555
3697
  *
3556
- * The JS counterpart of `validFramingResumes()` in `transaction_log_recovery.cpp`, which answers
3557
- * the same question at open time but keeps only the yes/no, discarding the offset.
3698
+ * The JS counterpart of `findFramingResumeOffset()` in `transaction_log_recovery.cpp`, which the
3699
+ * open-time scan and the timestamp index use to keep walking past the same break.
3558
3700
  */
3559
3701
  function findResyncPosition(dataView, from, dataEnd) {
3560
3702
  const lastStart = dataEnd - TRANSACTION_LOG_ENTRY_HEADER_SIZE;
@@ -3568,18 +3710,11 @@ function findResyncPosition(dataView, from, dataEnd) {
3568
3710
  }
3569
3711
  }
3570
3712
  /**
3571
- * Builds the report for a broken frame, with the offset iteration should continue from. Resolves
3572
- * the written extent here rather than per frame: `getLogFileSize` crosses into native and takes
3573
- * the store mutex, which a healthy read must never pay.
3713
+ * Builds the report for a broken frame, with the offset iteration should continue from.
3574
3714
  */
3575
3715
  function corruptFrame(message, transactionLog, dataView, logBuffer, position, limit, size, readUncommitted) {
3576
3716
  let dataEnd = limit;
3577
- if (readUncommitted) try {
3578
- const writtenExtent = transactionLog.getLogFileSize(logBuffer.logId);
3579
- dataEnd = writtenExtent > 0 ? Math.min(logBuffer.length, writtenExtent) : 0;
3580
- } catch {
3581
- dataEnd = 0;
3582
- }
3717
+ if (readUncommitted) dataEnd = Math.min(logBuffer.length, logBuffer.size ?? readableExtent(logBuffer));
3583
3718
  const resyncPosition = findResyncPosition(dataView, position + 1, dataEnd);
3584
3719
  return {
3585
3720
  error: new CorruptFrameError(message, logBuffer.logId, position, resyncPosition),
@@ -3609,7 +3744,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3609
3744
  let logId = latestLogId;
3610
3745
  let position = 0;
3611
3746
  let dataView;
3612
- let logBuffer = this._currentLogBuffer;
3747
+ let logBuffer = this._currentLogBuffer?.deref();
3613
3748
  let foundExactStart = false;
3614
3749
  if (start === void 0 && !startFromLastFlushed) {
3615
3750
  position = size;
@@ -3627,7 +3762,7 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3627
3762
  }
3628
3763
  if (logBuffer === void 0 || logBuffer.logId !== logId) {
3629
3764
  logBuffer = getLogMemoryMap(this, logId);
3630
- if (logBuffer && latestLogId === logId && !readUncommitted) this._currentLogBuffer = logBuffer;
3765
+ if (logBuffer && latestLogId === logId && !readUncommitted && this._currentLogBuffer?.deref() !== logBuffer) this._currentLogBuffer = new WeakRef(logBuffer);
3631
3766
  if (logBuffer === void 0) {
3632
3767
  logBuffer = Buffer.alloc(0);
3633
3768
  logBuffer.logId = 0;
@@ -3637,9 +3772,11 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3637
3772
  }
3638
3773
  }
3639
3774
  dataView = logBuffer.dataView;
3640
- if (latestLogId !== logId) {
3641
- size = logBuffer.size;
3642
- if (size === void 0) size = logBuffer.size = this.getLogFileSize(logId);
3775
+ if (latestLogId < logId) size = 0;
3776
+ else if (latestLogId > logId) {
3777
+ const cachedSize = logBuffer.size;
3778
+ if (cachedSize === void 0) size = logBuffer.size = readableExtent(logBuffer);
3779
+ else size = cachedSize;
3643
3780
  }
3644
3781
  return {
3645
3782
  [Symbol.iterator]() {
@@ -3649,15 +3786,15 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3649
3786
  let timestamp;
3650
3787
  if (position >= size) {
3651
3788
  const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
3652
- size = latestSize;
3789
+ size = latestLogId < logBuffer.logId ? 0 : latestSize;
3653
3790
  if (latestLogId > logBuffer.logId) {
3654
- size = logBuffer.size ?? (logBuffer.size = transactionLog.getLogFileSize(logBuffer.logId));
3791
+ size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3655
3792
  if (position >= size) {
3656
- const nextLogBuffer = getLogMemoryMap(transactionLog, logBuffer.logId + 1);
3793
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3657
3794
  if (nextLogBuffer) {
3658
3795
  dataView = nextLogBuffer.dataView;
3659
3796
  logBuffer = nextLogBuffer;
3660
- if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = transactionLog.getLogFileSize(logBuffer.logId));
3797
+ if (latestLogId > logBuffer.logId) size = logBuffer.size ?? (logBuffer.size = readableExtent(logBuffer));
3661
3798
  else size = latestSize;
3662
3799
  position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3663
3800
  }
@@ -3708,19 +3845,25 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3708
3845
  };
3709
3846
  if (position >= size) {
3710
3847
  const { logId: latestLogId, size: latestSize } = loadLastPosition(transactionLog, !!readUncommitted);
3711
- size = latestSize;
3848
+ size = latestLogId < logBuffer.logId ? 0 : latestSize;
3712
3849
  if (latestLogId > logBuffer.logId) {
3713
- const nextLogBuffer = getLogMemoryMap(transactionLog, logBuffer.logId + 1);
3850
+ size = logBuffer.size ?? readableExtent(logBuffer);
3851
+ if (!readUncommitted) logBuffer.size = size;
3852
+ if (position < size) continue;
3853
+ const nextLogBuffer = nextReadableLogBuffer(transactionLog, logBuffer.logId, latestLogId);
3714
3854
  if (!nextLogBuffer) return {
3715
3855
  done: true,
3716
3856
  value: void 0
3717
3857
  };
3718
3858
  logBuffer = nextLogBuffer;
3719
3859
  dataView = logBuffer.dataView;
3720
- size = logBuffer.size;
3721
- if (size == void 0) {
3722
- size = transactionLog.getLogFileSize(logBuffer.logId);
3723
- if (!readUncommitted) logBuffer.size = size;
3860
+ if (latestLogId === logBuffer.logId) size = latestSize;
3861
+ else {
3862
+ const cachedSize = logBuffer.size;
3863
+ if (cachedSize === void 0) {
3864
+ size = readableExtent(logBuffer);
3865
+ if (!readUncommitted) logBuffer.size = size;
3866
+ } else size = cachedSize;
3724
3867
  }
3725
3868
  position = TRANSACTION_LOG_FILE_HEADER_SIZE;
3726
3869
  }
@@ -3733,6 +3876,35 @@ Object.defineProperty(TransactionLog.prototype, "query", { value({ start, end, e
3733
3876
  }
3734
3877
  };
3735
3878
  } });
3879
+ /**
3880
+ * Maps the next readable segment after `fromLogId`, skipping a run retention has deleted —
3881
+ * stopping at the hole wedges the iterator there for every later poll (invariant 30).
3882
+ *
3883
+ * The jump target is `_nextLogId()`, the successor over the store's registered segments — *not*
3884
+ * `_findPosition(0)`, which names the bottom of the contiguous run ending at the current segment
3885
+ * and so lands *past* a survivor sitting between two holes.
3886
+ */
3887
+ function nextReadableLogBuffer(transactionLog, fromLogId, latestLogId) {
3888
+ let candidateLogId = fromLogId + 1;
3889
+ while (candidateLogId <= latestLogId) {
3890
+ const logBuffer = getLogMemoryMap(transactionLog, candidateLogId);
3891
+ if (logBuffer) {
3892
+ if (candidateLogId === latestLogId || readableExtent(logBuffer) > TRANSACTION_LOG_FILE_HEADER_SIZE) return logBuffer;
3893
+ } else if (transactionLog.getLogFileSize(candidateLogId) > TRANSACTION_LOG_FILE_HEADER_SIZE) return;
3894
+ candidateLogId = transactionLog._nextLogId(candidateLogId);
3895
+ if (candidateLogId === 0) return;
3896
+ }
3897
+ }
3898
+ /**
3899
+ * The bound for reading `logBuffer`. Native advances this append-owned extent only after a
3900
+ * successful write. It remains attached to the mapping after purge forgets the file object, so
3901
+ * readers retain committed history without interpreting a partial failed append as another frame.
3902
+ */
3903
+ function readableExtent(logBuffer) {
3904
+ const extent = logBuffer.readableExtent;
3905
+ if (extent === void 0) throw new Error(`Transaction log buffer ${logBuffer.logId} has no readableExtent`);
3906
+ return Math.min(logBuffer.length, extent);
3907
+ }
3736
3908
  function getLogMemoryMap(transactionLog, logId) {
3737
3909
  if (logId <= 0) return;
3738
3910
  let logBuffer = transactionLog._logBuffers.get(logId)?.deref();
@@ -3778,9 +3950,9 @@ function loadLastPosition(transactionLog, readUncommitted) {
3778
3950
  //#region src/index.ts
3779
3951
  const versions = {
3780
3952
  rocksdb: version,
3781
- "rocksdb-js": "2.9.1"
3953
+ "rocksdb-js": "2.11.0"
3782
3954
  };
3783
3955
 
3784
3956
  //#endregion
3785
- export { BackgroundError, CorruptFrameError, CountEstimator, DBI, DBIterator, RocksDatabase, Store, Transaction, TransactionLog, backups, constants, coolTransactionLogs, currentThreadId, fileLockRelease, getWriteBufferManagerStats, parseTransactionLog, registryStatus, shutdown, stats, supportedCompression, tryFileLock, validateTransactionLogStore, versions };
3957
+ 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 };
3786
3958
  //# sourceMappingURL=index.mjs.map