@harperfast/rocksdb-js 2.9.1 → 2.10.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/README.md CHANGED
@@ -185,7 +185,13 @@ Creates a new database instance.
185
185
  ### `db.close()`
186
186
 
187
187
  Closes a database. This function can be called multiple times and will only close an opened
188
- database. A database instance can be reopened once its closed.
188
+ database. A database instance can be reopened once it is closed. A flush failure leaves the native
189
+ database quarantined so `shutdown()` can retry without losing unflushed data; an explicit
190
+ `destroy()` can instead delete it. A failure while waiting for compaction to settle is reported
191
+ after native teardown completes; the optional `compactOnClose` pass itself is best-effort and its
192
+ errors do not fail the close, since a skipped compaction loses no data. All native close errors
193
+ emit `database:closeFailed`. The quarantine applies to both writable and read-only opens because
194
+ both modes share the physical path lifecycle.
189
195
 
190
196
  ```typescript
191
197
  const db = RocksDatabase.open('foo');
@@ -227,6 +233,12 @@ Sets global database settings.
227
233
  Defaults to 32MB. Set to `0` (zero) disables block cache for future opened databases. Existing
228
234
  block cache for any opened databases is resized immediately. Negative values throw an error.
229
235
  - `compactOnClose: boolean` When `true`, compacts the database on close. Defaults to `false`.
236
+ - `lifecycleWaitSeconds: number` How long a synchronous open, destroy, or shutdown waits for a
237
+ _conflicting_ lifecycle operation already in progress on the same path (e.g. another open or
238
+ close) before throwing a retryable timeout error. It does not bound the separate, intentionally
239
+ unbounded wait that `destroy()`/`shutdown()` make for in-flight backups, checkpoints, or other
240
+ async work still using the database — see [`db.destroy()`](#dbdestroy-void). Defaults to `30`
241
+ seconds and must be a positive integer.
230
242
  - `verificationTableEntries: number` The number of slots in the process-global
231
243
  [Verification Table](#verification-table). Each slot is 8 bytes, so the default of `131072`
232
244
  (128K) slots is 1 MB. Set to `0` to disable the verification table. This must be configured
@@ -574,7 +586,16 @@ db.compactSync({ bottommost: true });
574
586
  ### `db.destroy(): void`
575
587
 
576
588
  Completely removes a database based on the `db` instance's path including all data, column families,
577
- and files on disk.
589
+ and files on disk. Destruction owns the physical path for the process: it closes every writable and
590
+ read-only handle for that path, waits for registered backups and checkpoints to stop using the
591
+ native database, and prevents another handle from reopening the path until removal finishes. Those
592
+ waits are synchronous and can outlive `lifecycleWaitSeconds` once destruction has claimed the path,
593
+ because releasing the native database beneath an active copy would be unsafe.
594
+
595
+ A previously opened instance does not need to remain open, which allows an explicit `destroy()`
596
+ retry after failed physical cleanup. A never-opened or read-only instance cannot destroy the
597
+ database. `shutdown()` reports a pending cleanup tombstone but never retries deletion; only an
598
+ explicit `destroy()` can remove the path.
578
599
 
579
600
  ```typescript
580
601
  db.destroy();
@@ -583,8 +604,8 @@ console.log(fs.existsSync(db.path)); // false
583
604
 
584
605
  ### `db.drop(): Promise<void>`
585
606
 
586
- Removes all entries in the database. If the database was opened with a `name`, the database will be
587
- deleted on close.
607
+ Drops the column family the database was opened with (`name`). For the default column family this
608
+ clears all entries instead.
588
609
 
589
610
  ```typescript
590
611
  const db = RocksDatabase.open('path/to/db', { name: 'users' });
@@ -592,9 +613,56 @@ await db.drop();
592
613
  db.close();
593
614
  ```
594
615
 
616
+ #### Dropping column families
617
+
618
+ A drop retires the column family **logically** before it returns: the name is gone from
619
+ `db.columns`, a later `open()` with the same name creates a fresh, empty column family, and any
620
+ transaction that then stages a write to a handle of the dropped family, or commits one it staged
621
+ earlier, is refused whole with `ERR_COLUMN_FAMILY_DROPPED` (`Column family "users" was dropped`).
622
+ That terminal refusal releases the transaction's verification-table intents and bars further
623
+ writes or commit attempts; retained reads continue until the caller aborts the transaction, and
624
+ they still serve that transaction's own staged writes — values no commit will ever produce. A
625
+ caller that catches the refusal instead of letting it propagate must not read a value back through
626
+ the transaction and carry it forward.
627
+ If a staging call fails while the generation is being retired, retirement takes precedence and the
628
+ transaction is refused whole even when the immediate RocksDB failure was a pessimistic lock timeout.
629
+ Handles other threads still hold keep **reading** the dropped data until they close; a
630
+ non-transactional `putSync`/`removeSync` through such a handle is discarded.
631
+
632
+ The **physical** RocksDB drop is deferred behind commits already admitted when the drop lands: a
633
+ commit claims every column family its batch names before it writes its transaction-log batch and
634
+ releases them after RocksDB has applied it, and the physical drop runs from whichever
635
+ side releases last (or, for a commit a mid-flight `close()` tore out of its pipeline, from the next
636
+ drop, open of that name, or close on the database). With no such commit (the common case) `drop()`/`dropSync()` perform the
637
+ physical drop before returning, exactly as before. This is what keeps a drop racing another
638
+ thread's commit from latching RocksDB's fatal `Invalid column family specified in write batch`
639
+ error on the whole database.
640
+
641
+ Consequences to know about:
642
+
643
+ - A commit admitted before the drop completes successfully into the retiring generation, then the
644
+ physical drop removes that generation. Its caller sees a successful commit, but those writes are
645
+ intentionally discarded with the rest of the dropped column family; a same-name reopen creates
646
+ a fresh, empty generation. If the transaction writes to a transaction log, its entries are still
647
+ published; consumers must order the schema drop after those entries.
648
+ - `open()` of a name whose previous generation is still held by an admitted commit waits for the
649
+ full admission-to-reclamation interval (bounded by `ROCKSDB_JS_CF_RECLAIM_WAIT_MS`, default
650
+ `30000`) before creating the fresh column family; if the previous generation's physical drop
651
+ failed, the open retries it once and throws with that error if it fails again.
652
+ - A physical drop that fails (an I/O error writing the MANIFEST) keeps the name retired, is
653
+ retried on the next drop on the database, the next `open()` of that name, or close, and is
654
+ reported through the global `log.warn` event and the `columnFamily.pendingReclaims` stat. When
655
+ the failing drop was the one `drop()` itself ran, the call rejects with that error as well.
656
+
657
+ What is **not** guaranteed: a process that exits while a physical drop is still pending, or
658
+ after one failed, leaves the column family on disk under its name, and the next open of the
659
+ database opens it as a live column family. A backup or checkpoint taken inside that window copies
660
+ it. Callers that need a drop to survive a crash record their own durable tombstone before
661
+ acknowledging it.
662
+
595
663
  ### `db.dropSync(): void`
596
664
 
597
- Synchronous version of `db.drop()`.
665
+ Synchronous version of `db.drop()`, with the same deferral contract.
598
666
 
599
667
  ```typescript
600
668
  const db = RocksDatabase.open('path/to/db');
@@ -803,6 +871,12 @@ const ts = db.getMonotonicTimestamp();
803
871
  console.log(ts); // 1764307857213.739
804
872
  ```
805
873
 
874
+ It is a wall-clock (Unix epoch) value made strictly increasing: on a tie or a backward step of the
875
+ host clock it advances by one floating-point ulp per call until the wall clock catches up. That
876
+ keeps transaction timestamps ordered and durable, but it does not measure elapsed time — after a
877
+ backward step, differences between two calls understate real time. Use
878
+ [`steadyClockNow()`](#steadyclocknow-number) for elapsed durations and deadlines.
879
+
806
880
  ### `db.getOldestSnapshotTimestamp(): number`
807
881
 
808
882
  Returns a number representing a unix timestamp of the oldest unreleased snapshot.
@@ -928,8 +1002,13 @@ Returns a new `ArrayBuffer` with two additional methods:
928
1002
  - `cancel()` - Removes the callback; future `notify()` calls do nothing
929
1003
 
930
1004
  Note: If a shared buffer already exists for the given `key`, the returned `ArrayBuffer` will
931
- reference this existing shared buffer. Once all `ArrayBuffer` instances have gone out of scope and
932
- garbage collected, the underlying memory and notify callback will be freed.
1005
+ reference this existing shared buffer. The buffer lives as long as its column family is open (a
1006
+ `drop()` discards it with the column family — except the default column family, which `drop()`
1007
+ only clears, so its buffers survive): it is process-wide state, so a thread dropping or garbage
1008
+ collecting its own view (or exiting) never resets it for the others. Because nothing is ever
1009
+ evicted, key the buffer on a small fixed set of names rather than on unbounded data such as
1010
+ record ids. The notify callback is removed when the `ArrayBuffer` it was registered with is
1011
+ garbage collected, or by `cancel()`.
933
1012
 
934
1013
  ```typescript
935
1014
  const buffer = new Uint8Array(db.getUserSharedBuffer('isDone', new ArrayBuffer(1)));
@@ -2212,6 +2291,51 @@ Options:
2212
2291
  Validating a store that is being actively appended to can spuriously report a torn tail for the
2213
2292
  current log file — the tail of an in-flight append is indistinguishable from a crash artifact.
2214
2293
 
2294
+ ### `steadyClockNow(): number`
2295
+
2296
+ Reads the process-wide steady clock. No database handle is needed.
2297
+
2298
+ ```typescript
2299
+ import { steadyClockNow } from '@harperfast/rocksdb-js';
2300
+
2301
+ const start = steadyClockNow();
2302
+ // ... work, possibly on other worker threads ...
2303
+ const elapsedMs = steadyClockNow() - start;
2304
+ ```
2305
+
2306
+ Contract:
2307
+
2308
+ - **Units / type**: milliseconds with a fractional part, as a `number`. Read from
2309
+ `std::chrono::steady_clock` (`CLOCK_MONOTONIC` on Linux, `CLOCK_MONOTONIC_RAW` on macOS,
2310
+ `QueryPerformanceCounter` on Windows).
2311
+ - **Origin and lifetime**: the origin is unspecified and fixed for the life of the process. Every
2312
+ sample taken in the process is in one domain — the main thread and every `worker_threads` worker,
2313
+ including workers started or restarted at any later time, under Node, Bun and Deno alike. Samples
2314
+ are not meaningful across processes or restarts, and are unrelated to the Unix epoch: never compare
2315
+ them with `Date.now()`, `db.getMonotonicTimestamp()` or transaction timestamps.
2316
+ - **Monotonic, not unique**: a sample taken after another (in real time, on any thread) is `>=` it.
2317
+ Two samples can be equal — in the same clock tick, or when two distinct readings round to the same
2318
+ double — so treat `==` as "not later" and pair the sample with a sequence number if strict ordering
2319
+ is required. On Windows, QPC samples from different threads within ±1 native counter tick also
2320
+ have ambiguous ordering; do not use near-equal clock samples alone to prove causality. No
2321
+ wall-clock ratchet is applied.
2322
+ - **Precision**: the double's spacing grows with distance from the origin: about 2 ns at 100 days,
2323
+ 61 ns at 10 years, 0.49 µs at 100 years. Conversion error is below 0.51 µs per sample through
2324
+ 100 years from the native origin. This is representation precision, not clock accuracy. Rounding is
2325
+ monotone, so distinct readings can only collapse to equality, never invert. The clock's own
2326
+ resolution is platform-defined (the C++ duration period is 1 ns on these platforms; actual clock
2327
+ resolution can be coarser). The native signed 64-bit nanosecond range is about 292 years in either
2328
+ direction from its origin; conversion to `number` does not narrow it.
2329
+ - **Independent of the wall clock**: `settimeofday`/NTP steps in either direction do not move it. NTP
2330
+ frequency slew is platform-defined (`CLOCK_MONOTONIC` is slewed; `CLOCK_MONOTONIC_RAW` and
2331
+ `QueryPerformanceCounter` are not).
2332
+ - **Suspend**: whether time the host spends suspended counts is platform-defined — `CLOCK_MONOTONIC`
2333
+ excludes it on Linux; current macOS `CLOCK_MONOTONIC_RAW` and Windows QPC include it. Do not
2334
+ assume runtime timers (`setTimeout`) use the same suspend policy. Portable suspend-inclusive
2335
+ deadlines are not provided by this API.
2336
+ - **Cost**: one clock read and a double conversion; no lock, no native allocation, no wall-clock read,
2337
+ no database or log I/O, no per-worker calibration. Safe from any thread. The runtime may allocate a boxed JS number.
2338
+
2215
2339
  ### `currentThreadId(): number`
2216
2340
 
2217
2341
  Returns the current thread ID.
@@ -2226,9 +2350,13 @@ console.log(currentThreadId());
2226
2350
  Returns an array containing that status of all active RocksDB instances.
2227
2351
 
2228
2352
  - `path: string` The database path.
2353
+ - `closeError?: string` The native lifecycle error retaining this registry entry.
2354
+ - `destroyCleanupPending?: boolean` The native database is closed, but physical path cleanup must
2355
+ finish before the next open. Call `destroy()` to retry cleanup.
2229
2356
  - `refCount: number` The number of JavaScript database instances plus the registry's reference.
2230
2357
  - `columnFamiles: object` A map of column family names and their their info.
2231
- - `userSharedBuffers: number` The count of active user shared buffers.
2358
+ - `userSharedBuffers: number` The count of user shared buffer keys created since the column family
2359
+ was opened; entries are never evicted, so this never decreases.
2232
2360
  - `transactions: number` The count of active transactions.
2233
2361
  - `closables: number` The count of active database, transactions, and iterators.
2234
2362
  - `locks: number` The count of active locks.
@@ -2243,11 +2371,23 @@ console.log(registryStatus());
2243
2371
 
2244
2372
  The `shutdown()` will flush all in-memory data to disk and wait for any outstanding compactions to
2245
2373
  finish, for all open databases. It is highly recommended to call this in a `process` `exit` event
2246
- listener (on the main thread), to ensure that all data is flushed to disk before the process exits:
2374
+ listener (on the main thread), to ensure that all data is flushed to disk before the process exits.
2375
+ It throws the first close failure after attempting every claimed database; call it again to retry
2376
+ any descriptor whose native teardown did not complete. It reports pending destroy-cleanup
2377
+ tombstones without deleting their paths; retry those with an explicit `destroy()`.
2378
+
2379
+ Wrap the call: an exception thrown from an `exit` listener skips every `exit` listener registered
2380
+ after it, and sets the exit code to 1 unless an `uncaughtException` handler is installed.
2247
2381
 
2248
2382
  ```typescript
2249
2383
  import { shutdown } from '@harperfast/rocksdb-js';
2250
- process.on('exit', shutdown);
2384
+ process.on('exit', () => {
2385
+ try {
2386
+ shutdown();
2387
+ } catch (error) {
2388
+ console.error('rocksdb-js shutdown failed', error);
2389
+ }
2390
+ });
2251
2391
  ```
2252
2392
 
2253
2393
  ### `versions: { 'rocksdb': string; 'rocksdb-js': string }`
package/dist/index.cjs CHANGED
@@ -131,9 +131,49 @@ const NativeIterator = binding.Iterator;
131
131
  const NativeTransaction = binding.Transaction;
132
132
  const TransactionLog = binding.TransactionLog;
133
133
  const registryStatus = binding.registryStatus;
134
+ /**
135
+ * Flushes every open database and waits for outstanding compactions, then
136
+ * releases the registry.
137
+ *
138
+ * **Throws** the first failure it encounters: a close whose native teardown
139
+ * failed (that descriptor stays quarantined — call `shutdown()` again to retry
140
+ * it), or a `lifecycleWaitSeconds` timeout waiting on a concurrent
141
+ * open/destroy/shutdown. A quarantining close also emits
142
+ * `database:closeFailed`; a timeout has no event, so the throw is its only
143
+ * signal.
144
+ *
145
+ * A `process.on('exit')` listener must therefore wrap it: an exception thrown
146
+ * from an `exit` listener skips every `exit` listener registered after it (and
147
+ * sets exit code 1 unless an `uncaughtException` handler is installed).
148
+ *
149
+ * ```typescript
150
+ * process.on('exit', () => {
151
+ * try {
152
+ * shutdown();
153
+ * } catch (error) {
154
+ * console.error('rocksdb-js shutdown failed', error);
155
+ * }
156
+ * });
157
+ * ```
158
+ */
134
159
  const shutdown = binding.shutdown;
135
160
  const currentThreadId = binding.currentThreadId;
136
161
  /**
162
+ * Reads the process-wide steady clock: `std::chrono::steady_clock` as
163
+ * fractional milliseconds from an unspecified origin that is fixed for the life
164
+ * of the process. Every sample in the process — main thread and every
165
+ * `worker_threads` worker, whenever started or restarted, on Node, Bun and Deno
166
+ * — is in one domain, and the difference between two samples is real elapsed
167
+ * time unaffected by wall-clock steps. Non-decreasing but not unique (two
168
+ * samples may be equal; treat `==` as "not later"). Windows QPC samples from
169
+ * different threads within one native tick also have ambiguous ordering.
170
+ * Clock readings alone do not prove causality. Not comparable with
171
+ * `Date.now()`, `db.getMonotonicTimestamp()` or transaction timestamps, and not
172
+ * meaningful across processes or restarts. Time spent in host suspend is
173
+ * platform-defined. No open database is needed.
174
+ */
175
+ const steadyClockNow = binding.steadyClockNow;
176
+ /**
137
177
  * Advises the kernel that the file-backed pages of every mapped transaction log
138
178
  * are cold (Linux MADV_COLD), so they are reclaimed first under memory pressure
139
179
  * without being freed — useful during replication catch-up, where a full read of
@@ -159,6 +199,21 @@ const transactionLogMapCount = binding.transactionLogMapCount;
159
199
  * disarm. Used by the ERR_TRY_AGAIN retry regression test.
160
200
  */
161
201
  const forceTryAgainForTesting = binding.forceTryAgainForTesting;
202
+ /**
203
+ * Test-only seam: makes the physical column-family drop report failure.
204
+ * `0` = inert, `1` = fail without dropping (the retry performs the drop),
205
+ * `2` = drop for real and then report failure (the retry resolves as
206
+ * "already dropped"). Process-global, like `forceTryAgainForTesting`.
207
+ */
208
+ const forceDropFailureForTesting = binding.forceDropFailureForTesting;
209
+ /** Parks one selected transaction write after its retirement precheck. */
210
+ const setTransactionStagingDelayForTesting = binding.setTransactionStagingDelayForTesting;
211
+ /** Reports whether the selected transaction write is parked. */
212
+ const isTransactionStagingDelayedForTesting = binding.isTransactionStagingDelayedForTesting;
213
+ /** Reports whether a transaction is parked at the native commit seam. */
214
+ const isTransactionCommitExecuteDelayedForTesting = binding.isTransactionCommitExecuteDelayedForTesting;
215
+ const setTransactionCommitAdmissionDelayForTesting = binding.setTransactionCommitAdmissionDelayForTesting;
216
+ const isTransactionCommitAdmissionDelayedForTesting = binding.isTransactionCommitAdmissionDelayedForTesting;
162
217
  /** Delays one selected watchdog join in the concurrent-shutdown regression test. */
163
218
  const setWriteBufferManagerJoinDelayForTesting = binding.setWriteBufferManagerJoinDelayForTesting;
164
219
  /**
@@ -2119,12 +2174,12 @@ var Store = class {
2119
2174
  return false;
2120
2175
  }
2121
2176
  putSync(context, key, value, options) {
2122
- if (!this.db.opened) throw new Error("Database not open");
2177
+ if (!this.db.opened) throw new Error(this.db.closing ? "Database is closing" : "Database not open");
2123
2178
  const valueBuffer = this.encodeValue(value);
2124
2179
  context.putSync(this.encodeKey(key), valueBuffer, this.getTxnId(options));
2125
2180
  }
2126
2181
  removeSync(context, key, options) {
2127
- if (!this.db.opened) throw new Error("Database not open");
2182
+ if (!this.db.opened) throw new Error(this.db.closing ? "Database is closing" : "Database not open");
2128
2183
  context.removeSync(this.encodeKey(key), this.getTxnId(options));
2129
2184
  }
2130
2185
  /**
@@ -2628,7 +2683,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
2628
2683
  * native binding use namespaced keys (e.g. `'transactionLog:warning'`).
2629
2684
  *
2630
2685
  * Listeners are not tied to any specific database — they fire for every
2631
- * matching event emitted in this process.
2686
+ * matching event emitted in this process. Native lifecycle failures use
2687
+ * `'database:closeFailed'` with `(path, error)` string arguments. The event
2688
+ * reports both completed and incomplete teardowns; an incomplete close can
2689
+ * be retried by `shutdown()` or explicitly deleted by `destroy()`.
2632
2690
  *
2633
2691
  * @example
2634
2692
  * ```typescript
@@ -2712,7 +2770,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
2712
2770
  this.store.db.catchUpWithPrimarySync();
2713
2771
  }
2714
2772
  destroy() {
2715
- this.store.db.destroy();
2773
+ this.store.db.destroy(this.store.readOnly);
2716
2774
  }
2717
2775
  async drop() {
2718
2776
  return new Promise((resolve, reject) => {
@@ -3578,8 +3636,8 @@ function frameFits(dataView, pos, dataEnd) {
3578
3636
  * terminator, let a chain "end" anywhere in megabytes of padding. Against the written extent the
3579
3637
  * end is a single offset, so a chain landing on it is ~1/2^32 to be coincidence.
3580
3638
  *
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.
3639
+ * The JS counterpart of `findFramingResumeOffset()` in `transaction_log_recovery.cpp`, which the
3640
+ * open-time scan and the timestamp index use to keep walking past the same break.
3583
3641
  */
3584
3642
  function findResyncPosition(dataView, from, dataEnd) {
3585
3643
  const lastStart = dataEnd - TRANSACTION_LOG_ENTRY_HEADER_SIZE;
@@ -3803,7 +3861,7 @@ function loadLastPosition(transactionLog, readUncommitted) {
3803
3861
  //#region src/index.ts
3804
3862
  const versions = {
3805
3863
  rocksdb: version,
3806
- "rocksdb-js": "2.9.1"
3864
+ "rocksdb-js": "2.10.0"
3807
3865
  };
3808
3866
 
3809
3867
  //#endregion
@@ -3826,6 +3884,7 @@ exports.parseTransactionLog = parseTransactionLog;
3826
3884
  exports.registryStatus = registryStatus;
3827
3885
  exports.shutdown = shutdown;
3828
3886
  exports.stats = stats;
3887
+ exports.steadyClockNow = steadyClockNow;
3829
3888
  exports.supportedCompression = supportedCompression;
3830
3889
  exports.tryFileLock = tryFileLock;
3831
3890
  exports.validateTransactionLogStore = validateTransactionLogStore;