@harperfast/rocksdb-js 2.9.0 → 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
@@ -95,10 +95,20 @@ Creates a new database instance.
95
95
  - `maxWriteBufferSizeToMaintain: number` The number of bytes of recent memtable history to keep in
96
96
  memory for transaction conflict checking. `-1` (the default) derives the value from
97
97
  `maxWriteBufferNumber * writeBufferSize` (the RocksDB-recommended default for optimistic
98
- transactions) — except when a stalling [`writeBufferManager`](#dbconfigoptions) is configured
99
- (`writeBufferManagerSize > 0` with `writeBufferManagerAllowStall`), in which case it resolves to
100
- `0` to avoid retaining memtable history the manager will never release. An explicit non-negative
101
- value is always honored as-is.
98
+ transactions) — except when the database has a [`writeBufferManager`](#dbconfigoptions)
99
+ attached, in which case it resolves to `1` so the manager is not filled with history it will
100
+ never release. What decides this is the manager the database itself holds, not the current
101
+ `writeBufferManagerSize`: a column family created later on an already-open database is clamped
102
+ too, because its history is still charged to that manager. It also applies to any attached
103
+ manager, not only a stalling one — `writeBufferManagerAllowStall` can be changed at runtime
104
+ while this value is fixed when a column family is created, and history the budget cannot reclaim
105
+ is a problem either way. `1` is the smallest target a transactional database can be given: RocksDB's transaction wrappers rewrite a `0` target to
106
+ the derived value, so `0` requests the _largest_ history rather than none, and an explicitly
107
+ requested `0` is normalized to `1` for the same reason. A positive target still retains the most
108
+ recent flushed memtable per column family until that family's next write. An explicit positive
109
+ value is honored as-is; sizing it against the budget and the column-family count is then yours,
110
+ and a configuration whose known families already reach the budget is reported on the
111
+ [`'log.warn'`](#event-api) channel.
102
112
  - `name: string` The column family name. Defaults to `"default"`.
103
113
  - `noBlockCache: boolean` When `true`, disables the block cache. Block caching is enabled by
104
114
  default and the cache is shared across all database instances.
@@ -175,7 +185,13 @@ Creates a new database instance.
175
185
  ### `db.close()`
176
186
 
177
187
  Closes a database. This function can be called multiple times and will only close an opened
178
- 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.
179
195
 
180
196
  ```typescript
181
197
  const db = RocksDatabase.open('foo');
@@ -217,6 +233,12 @@ Sets global database settings.
217
233
  Defaults to 32MB. Set to `0` (zero) disables block cache for future opened databases. Existing
218
234
  block cache for any opened databases is resized immediately. Negative values throw an error.
219
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.
220
242
  - `verificationTableEntries: number` The number of slots in the process-global
221
243
  [Verification Table](#verification-table). Each slot is 8 bytes, so the default of `131072`
222
244
  (128K) slots is 1 MB. Set to `0` to disable the verification table. This must be configured
@@ -236,8 +258,10 @@ Sets global database settings.
236
258
  back into the reclaimed space. Defaults to `false`.
237
259
  - `writeBufferManagerSize: number` Total memtable memory limit (bytes) shared across every
238
260
  database opened in this process. When set, RocksDB uses a single `WriteBufferManager` so write
239
- buffers are bounded process-wide rather than per database. A value of `0` disables the manager.
240
- Defaults to `0`.
261
+ buffers are bounded process-wide rather than per database. Defaults to `0`, which means no
262
+ manager. Setting `0` later stops _new_ opens from attaching one, but does not detach or resize
263
+ the manager a database already holds — `write_buffer_manager` is fixed for the life of an open
264
+ database, so its memtables stay charged against that budget.
241
265
 
242
266
  ```typescript
243
267
  RocksDatabase.config({
@@ -562,7 +586,16 @@ db.compactSync({ bottommost: true });
562
586
  ### `db.destroy(): void`
563
587
 
564
588
  Completely removes a database based on the `db` instance's path including all data, column families,
565
- 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.
566
599
 
567
600
  ```typescript
568
601
  db.destroy();
@@ -571,8 +604,8 @@ console.log(fs.existsSync(db.path)); // false
571
604
 
572
605
  ### `db.drop(): Promise<void>`
573
606
 
574
- Removes all entries in the database. If the database was opened with a `name`, the database will be
575
- 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.
576
609
 
577
610
  ```typescript
578
611
  const db = RocksDatabase.open('path/to/db', { name: 'users' });
@@ -580,9 +613,56 @@ await db.drop();
580
613
  db.close();
581
614
  ```
582
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
+
583
663
  ### `db.dropSync(): void`
584
664
 
585
- Synchronous version of `db.drop()`.
665
+ Synchronous version of `db.drop()`, with the same deferral contract.
586
666
 
587
667
  ```typescript
588
668
  const db = RocksDatabase.open('path/to/db');
@@ -791,6 +871,12 @@ const ts = db.getMonotonicTimestamp();
791
871
  console.log(ts); // 1764307857213.739
792
872
  ```
793
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
+
794
880
  ### `db.getOldestSnapshotTimestamp(): number`
795
881
 
796
882
  Returns a number representing a unix timestamp of the oldest unreleased snapshot.
@@ -916,8 +1002,13 @@ Returns a new `ArrayBuffer` with two additional methods:
916
1002
  - `cancel()` - Removes the callback; future `notify()` calls do nothing
917
1003
 
918
1004
  Note: If a shared buffer already exists for the given `key`, the returned `ArrayBuffer` will
919
- reference this existing shared buffer. Once all `ArrayBuffer` instances have gone out of scope and
920
- 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()`.
921
1012
 
922
1013
  ```typescript
923
1014
  const buffer = new Uint8Array(db.getUserSharedBuffer('isDone', new ArrayBuffer(1)));
@@ -1148,8 +1239,8 @@ uses this timestamp as the batch key; producers may also encode it into their ow
1148
1239
 
1149
1240
  Type: `number`
1150
1241
 
1151
- The transaction ID represented as a 32-bit unsigned integer. Transaction IDs are unique to the
1152
- RocksDB database path, regardless the database name/column family.
1242
+ The transaction ID, a positive integer no greater than `Number.MAX_SAFE_INTEGER`. Transaction IDs
1243
+ are unique to the RocksDB database path, regardless the database name/column family.
1153
1244
 
1154
1245
  #### `txn.setTimestamp(ts?: number): void`
1155
1246
 
@@ -2200,6 +2291,51 @@ Options:
2200
2291
  Validating a store that is being actively appended to can spuriously report a torn tail for the
2201
2292
  current log file — the tail of an in-flight append is indistinguishable from a crash artifact.
2202
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
+
2203
2339
  ### `currentThreadId(): number`
2204
2340
 
2205
2341
  Returns the current thread ID.
@@ -2214,9 +2350,13 @@ console.log(currentThreadId());
2214
2350
  Returns an array containing that status of all active RocksDB instances.
2215
2351
 
2216
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.
2217
2356
  - `refCount: number` The number of JavaScript database instances plus the registry's reference.
2218
2357
  - `columnFamiles: object` A map of column family names and their their info.
2219
- - `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.
2220
2360
  - `transactions: number` The count of active transactions.
2221
2361
  - `closables: number` The count of active database, transactions, and iterators.
2222
2362
  - `locks: number` The count of active locks.
@@ -2231,11 +2371,23 @@ console.log(registryStatus());
2231
2371
 
2232
2372
  The `shutdown()` will flush all in-memory data to disk and wait for any outstanding compactions to
2233
2373
  finish, for all open databases. It is highly recommended to call this in a `process` `exit` event
2234
- 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.
2235
2381
 
2236
2382
  ```typescript
2237
2383
  import { shutdown } from '@harperfast/rocksdb-js';
2238
- 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
+ });
2239
2391
  ```
2240
2392
 
2241
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
  /**
@@ -1293,7 +1348,15 @@ var DBIterator = class {
1293
1348
  this.iterator.return?.();
1294
1349
  return DONE_RESULT;
1295
1350
  }
1296
- const result = this.iterator.next();
1351
+ let result;
1352
+ try {
1353
+ result = this.iterator.next();
1354
+ } catch (err) {
1355
+ try {
1356
+ this.iterator.return?.();
1357
+ } catch {}
1358
+ throw err;
1359
+ }
1297
1360
  if (result === ITERATOR_RESULT_DONE) return DONE_RESULT;
1298
1361
  this.#yielded++;
1299
1362
  const includeValues = this.#includeValues;
@@ -1571,10 +1634,16 @@ var Store = class {
1571
1634
  /**
1572
1635
  * The bytes of recent memtable history to retain in memory for transaction
1573
1636
  * conflict checking. `-1` derives the value from
1574
- * `maxWriteBufferNumber * writeBufferSize`, except under a stalling
1575
- * `writeBufferManager`, where it derives `0`: retained history is charged to
1576
- * that manager and is only trimmable down to this target, so a target the
1577
- * budget cannot hold stalls writes permanently. Set a number to size it
1637
+ * `maxWriteBufferNumber * writeBufferSize`, except when a
1638
+ * `writeBufferManager` is configured, where it derives `1`: retained history
1639
+ * is charged to that manager and is only trimmable down to this target, so a
1640
+ * target the budget cannot hold is memory the manager never reclaims, and
1641
+ * stalls writes permanently when `writeBufferManagerAllowStall` is on. Any
1642
+ * configured manager, not only a stalling one — that flag is mutable at
1643
+ * runtime while this value is fixed when the family is created. `1`, not `0`, because
1644
+ * RocksDB's transaction wrappers rewrite a `0` target to the derived value —
1645
+ * so `0` asks for the largest history rather than none, and an explicit `0`
1646
+ * is normalized to `1` for that reason. Set a positive number to size it
1578
1647
  * yourself against the budget and the column-family count.
1579
1648
  */
1580
1649
  maxWriteBufferSizeToMaintain;
@@ -2105,12 +2174,12 @@ var Store = class {
2105
2174
  return false;
2106
2175
  }
2107
2176
  putSync(context, key, value, options) {
2108
- 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");
2109
2178
  const valueBuffer = this.encodeValue(value);
2110
2179
  context.putSync(this.encodeKey(key), valueBuffer, this.getTxnId(options));
2111
2180
  }
2112
2181
  removeSync(context, key, options) {
2113
- 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");
2114
2183
  context.removeSync(this.encodeKey(key), this.getTxnId(options));
2115
2184
  }
2116
2185
  /**
@@ -2614,7 +2683,10 @@ var RocksDatabase = class RocksDatabase extends DBI {
2614
2683
  * native binding use namespaced keys (e.g. `'transactionLog:warning'`).
2615
2684
  *
2616
2685
  * Listeners are not tied to any specific database — they fire for every
2617
- * 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()`.
2618
2690
  *
2619
2691
  * @example
2620
2692
  * ```typescript
@@ -2698,7 +2770,7 @@ var RocksDatabase = class RocksDatabase extends DBI {
2698
2770
  this.store.db.catchUpWithPrimarySync();
2699
2771
  }
2700
2772
  destroy() {
2701
- this.store.db.destroy();
2773
+ this.store.db.destroy(this.store.readOnly);
2702
2774
  }
2703
2775
  async drop() {
2704
2776
  return new Promise((resolve, reject) => {
@@ -3564,8 +3636,8 @@ function frameFits(dataView, pos, dataEnd) {
3564
3636
  * terminator, let a chain "end" anywhere in megabytes of padding. Against the written extent the
3565
3637
  * end is a single offset, so a chain landing on it is ~1/2^32 to be coincidence.
3566
3638
  *
3567
- * The JS counterpart of `validFramingResumes()` in `transaction_log_recovery.cpp`, which answers
3568
- * 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.
3569
3641
  */
3570
3642
  function findResyncPosition(dataView, from, dataEnd) {
3571
3643
  const lastStart = dataEnd - TRANSACTION_LOG_ENTRY_HEADER_SIZE;
@@ -3789,7 +3861,7 @@ function loadLastPosition(transactionLog, readUncommitted) {
3789
3861
  //#region src/index.ts
3790
3862
  const versions = {
3791
3863
  rocksdb: version,
3792
- "rocksdb-js": "2.9.0"
3864
+ "rocksdb-js": "2.10.0"
3793
3865
  };
3794
3866
 
3795
3867
  //#endregion
@@ -3812,6 +3884,7 @@ exports.parseTransactionLog = parseTransactionLog;
3812
3884
  exports.registryStatus = registryStatus;
3813
3885
  exports.shutdown = shutdown;
3814
3886
  exports.stats = stats;
3887
+ exports.steadyClockNow = steadyClockNow;
3815
3888
  exports.supportedCompression = supportedCompression;
3816
3889
  exports.tryFileLock = tryFileLock;
3817
3890
  exports.validateTransactionLogStore = validateTransactionLogStore;