@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/README.md +377 -56
- package/bin/rocksdb-js.mjs +49 -5
- package/dist/index.cjs +206 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +180 -7
- package/dist/index.d.mts +180 -7
- package/dist/index.mjs +205 -33
- package/dist/index.mjs.map +1 -1
- package/docs/backups.md +3 -2
- package/docs/performance/mutex-contention-2026-09-16.json +684 -0
- package/docs/performance/mutex-contention-admission-2026-09-17.json +730 -0
- package/docs/performance/mutex-contention.md +204 -0
- package/docs/stats.md +48 -46
- package/docs/steady-clock-design.md +121 -0
- package/docs/transaction-log.md +120 -1
- package/docs/transaction-timestamp-integrity-design.md +70 -8
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -169,10 +169,25 @@ Creates a new database instance.
|
|
|
169
169
|
- `transactionLogMaxSize: number` The target maximum size of a transaction log file. Transactions
|
|
170
170
|
are never split across files: if the complete transaction does not fit, the log rotates before
|
|
171
171
|
writing it. A transaction written to an empty file may exceed the target. Defaults to 16 MB.
|
|
172
|
+
`0` is rejected: it formerly meant unlimited segment size, which a reader's fixed-size mapping
|
|
173
|
+
cannot cover. A store configured with `0` must omit the option or set a size before opening;
|
|
174
|
+
once it rotates by size, rotated segments older than `transactionLogRetention` are purged, so
|
|
175
|
+
raise the retention to keep more history.
|
|
172
176
|
- `transactionLogRetention: string | number` The number of minutes to retain transaction logs
|
|
173
177
|
before purging. Defaults to `'3d'` (3 days).
|
|
174
178
|
- `transactionLogsPath: string` The path to store transaction logs. Defaults to
|
|
175
179
|
`"${db.path}/transaction_logs"`.
|
|
180
|
+
- `timestampFloorLog: string` The name of the transaction log whose batch keys this process
|
|
181
|
+
originates. At open, the process-wide monotonic clock is raised above every batch key still
|
|
182
|
+
durable in that log, so a backward wall-clock step between runs cannot reissue a transaction
|
|
183
|
+
timestamp that is already a key in it. See
|
|
184
|
+
[Timestamp floor at open](#timestamp-floor-at-open). Unset by default, which leaves the clock
|
|
185
|
+
alone. Its first use in a process must be on a writable primary open; read-only and secondary
|
|
186
|
+
handles can reuse an already-established seed for the same log but cannot establish one.
|
|
187
|
+
If that first seed warned because the log was absent, the log may be created fresh, but a
|
|
188
|
+
nonempty directory that reappears before first use is refused until every handle closes and the
|
|
189
|
+
path reopens with this option. The name must be a single path component that does not start with
|
|
190
|
+
`.`: startup discovery skips such directories, so the log could never be scanned again.
|
|
176
191
|
- `verificationTable: boolean` When `true`, this column family participates in the process-global
|
|
177
192
|
[Verification Table](#verification-table): transaction writes to this column family invalidate
|
|
178
193
|
the verification slot for each written key. Enable this only for column families whose records
|
|
@@ -185,7 +200,13 @@ Creates a new database instance.
|
|
|
185
200
|
### `db.close()`
|
|
186
201
|
|
|
187
202
|
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
|
|
203
|
+
database. A database instance can be reopened once it is closed. A flush failure leaves the native
|
|
204
|
+
database quarantined so `shutdown()` can retry without losing unflushed data; an explicit
|
|
205
|
+
`destroy()` can instead delete it. A failure while waiting for compaction to settle is reported
|
|
206
|
+
after native teardown completes; the optional `compactOnClose` pass itself is best-effort and its
|
|
207
|
+
errors do not fail the close, since a skipped compaction loses no data. All native close errors
|
|
208
|
+
emit `database:closeFailed`. The quarantine applies to both writable and read-only opens because
|
|
209
|
+
both modes share the physical path lifecycle.
|
|
189
210
|
|
|
190
211
|
```typescript
|
|
191
212
|
const db = RocksDatabase.open('foo');
|
|
@@ -212,12 +233,21 @@ when a non-default compression level is set. The database must be open. See
|
|
|
212
233
|
[Compression](#compression).
|
|
213
234
|
|
|
214
235
|
```typescript
|
|
215
|
-
const db = RocksDatabase.open('path/to/db', {
|
|
216
|
-
compression: { algorithm: 'zstd', level: 3 },
|
|
217
|
-
});
|
|
236
|
+
const db = RocksDatabase.open('path/to/db', { compression: { algorithm: 'zstd', level: 3 } });
|
|
218
237
|
console.log(db.compression); // { algorithm: 'zstd', level: 3 }
|
|
219
238
|
```
|
|
220
239
|
|
|
240
|
+
### `db.setCompression(compression)`
|
|
241
|
+
|
|
242
|
+
Dynamically changes the compression algorithm (and optional level) for this database's column
|
|
243
|
+
family on an already-open database — no close, no reopen. See [Compression](#compression) for full
|
|
244
|
+
semantics and caveats.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
const db = RocksDatabase.open('path/to/db', { compression: 'none' });
|
|
248
|
+
db.setCompression({ algorithm: 'zstd', level: 19 });
|
|
249
|
+
```
|
|
250
|
+
|
|
221
251
|
### `db.config(options)`
|
|
222
252
|
|
|
223
253
|
Sets global database settings.
|
|
@@ -226,7 +256,65 @@ Sets global database settings.
|
|
|
226
256
|
- `blockCacheSize: number` The amount of memory in bytes to use to cache uncompressed blocks.
|
|
227
257
|
Defaults to 32MB. Set to `0` (zero) disables block cache for future opened databases. Existing
|
|
228
258
|
block cache for any opened databases is resized immediately. Negative values throw an error.
|
|
259
|
+
- `commitThreads: number` The maximum number of dedicated threads each database opened after the
|
|
260
|
+
call uses to run async `transaction.commit()` calls. Defaults to `min(4, cores)`; accepts
|
|
261
|
+
integers from `1` through `64`. A thread starts only when a commit finds every started thread
|
|
262
|
+
busy, so a database that is never committed to concurrently owns one. Async commits never use
|
|
263
|
+
the libuv threadpool, so slow commits cannot delay `fs`, `dns`, `crypto` or async `get()` calls.
|
|
264
|
+
With more than one thread, RocksDB validates and applies concurrent commits on several cores
|
|
265
|
+
and can combine them into one write: with four worker threads committing 64-key transactions to
|
|
266
|
+
one database, this committed about twice as many transactions per second as one thread.
|
|
267
|
+
Concurrent commits finish, and their promises resolve, in any order, as concurrent
|
|
268
|
+
`commitSync()` calls always have, and their transaction-log entries are appended in the order
|
|
269
|
+
the commits reach the log; a log reader never sees an entry before its transaction's data is
|
|
270
|
+
committed. Log position is not the order in which writes to the same key were applied: a
|
|
271
|
+
commit that conflicts and is retried keeps its original log position but lands after commits
|
|
272
|
+
logged behind it. Its transaction timestamp does not advance across a retry either, so
|
|
273
|
+
replaying entries for one key in timestamp order does not recover true apply order — a
|
|
274
|
+
consumer that needs one must have the producer embed its own last-writer-wins marker.
|
|
275
|
+
Concurrent commit threads make this more frequent; a single commit thread and `commitSync()`
|
|
276
|
+
callers could already produce it. With `1`, a database's async commits run one at a time in
|
|
277
|
+
the order `commit()` was called. Databases already open keep their limit.
|
|
229
278
|
- `compactOnClose: boolean` When `true`, compacts the database on close. Defaults to `false`.
|
|
279
|
+
- `occValidation: 'parallel' | 'serial'` How writable optimistic databases opened after the call
|
|
280
|
+
validate commits for conflicts. Defaults to `'parallel'` (RocksDB's default). Databases already
|
|
281
|
+
open keep their policy. Pessimistic, read-only and secondary opens are unaffected.
|
|
282
|
+
- `'parallel'` locks the commit's keys in a per-database pool of `occLockBuckets` mutexes,
|
|
283
|
+
checks for conflicts, and holds the locks through the write. Commits on different threads
|
|
284
|
+
that touch different buckets can then be batched into one RocksDB write. Non-transactional
|
|
285
|
+
writes (`db.putSync()` and friends) take no bucket, so one that lands between a commit's
|
|
286
|
+
conflict check and its write is overwritten by that commit without an `ERR_BUSY`.
|
|
287
|
+
- `'serial'` checks for conflicts inside RocksDB's write group instead, and allocates no bucket
|
|
288
|
+
pool, which saves its memory. Because the check and the write happen in one step, a
|
|
289
|
+
non-transactional write cannot land between them and be silently overwritten, and a column
|
|
290
|
+
family cannot be dropped between them. The cost: RocksDB never batches a serial-validated
|
|
291
|
+
commit with another writer, including the database's other commit threads, so it gives back
|
|
292
|
+
what `commitThreads` gains. With four worker threads
|
|
293
|
+
committing 64-key transactions to one database it committed about 4,500 transactions per
|
|
294
|
+
second against about 9,900 for `'parallel'`, the same as a single commit thread; it came out
|
|
295
|
+
ahead (about 7%) only at 1,000 keys per transaction with the default bucket count, where
|
|
296
|
+
raising `occLockBuckets` gains more. Concurrent `commitSync()` calls lose throughput the same
|
|
297
|
+
way. It suits a database committed to one transaction at a time (`commitThreads: 1`).
|
|
298
|
+
- `occLockBuckets: number` The number of commit lock buckets each writable optimistic database
|
|
299
|
+
allocates when it is opened with `occValidation: 'parallel'`. Defaults to `65536` (2^16), not
|
|
300
|
+
RocksDB's 2^20. Accepts integers from `16` through `16777216` (2^24). Applies to databases
|
|
301
|
+
opened after the call; databases already open keep their count. Memory is the count times the
|
|
302
|
+
platform mutex size (40 bytes on Linux x64, 64 bytes on macOS), so the default is about 2.5 MiB
|
|
303
|
+
per database on Linux, against about 40 MiB at RocksDB's default. Multiple keys can share a
|
|
304
|
+
bucket, so this is **not a transaction write limit**. A small pool costs throughput only when
|
|
305
|
+
commits to the same database overlap (its `commitThreads`, `commitSync()` callers, or libuv
|
|
306
|
+
commits under `ROCKSDB_JS_COMMIT_THREAD=0`) and their keys share a bucket: colliding commits
|
|
307
|
+
run one at a time and cannot share a RocksDB write. Collisions grow with transaction size. With
|
|
308
|
+
four worker threads and the default four commit threads, 65536 buckets matched 2^20 and 2^22 at
|
|
309
|
+
64 keys per transaction (and 4096 gave about a third fewer commits per second), but at 1,000 keys
|
|
310
|
+
65536 committed 336 transactions per second against 466 at 2^20 and 685 at 2^22, so databases
|
|
311
|
+
with large transactions should raise the count.
|
|
312
|
+
- `lifecycleWaitSeconds: number` How long a synchronous open, destroy, or shutdown waits for a
|
|
313
|
+
_conflicting_ lifecycle operation already in progress on the same path (e.g. another open or
|
|
314
|
+
close) before throwing a retryable timeout error. It does not bound the separate, intentionally
|
|
315
|
+
unbounded wait that `destroy()`/`shutdown()` make for in-flight backups, checkpoints, or other
|
|
316
|
+
async work still using the database — see [`db.destroy()`](#dbdestroy-void). Defaults to `30`
|
|
317
|
+
seconds and must be a positive integer.
|
|
230
318
|
- `verificationTableEntries: number` The number of slots in the process-global
|
|
231
319
|
[Verification Table](#verification-table). Each slot is 8 bytes, so the default of `131072`
|
|
232
320
|
(128K) slots is 1 MB. Set to `0` to disable the verification table. This must be configured
|
|
@@ -254,7 +342,10 @@ Sets global database settings.
|
|
|
254
342
|
```typescript
|
|
255
343
|
RocksDatabase.config({
|
|
256
344
|
blockCacheSize: 100 * 1024 * 1024, // 100MB
|
|
345
|
+
commitThreads: 4, // per database; applies to subsequent opens
|
|
257
346
|
compactOnClose: true,
|
|
347
|
+
occLockBuckets: 1 << 20, // per optimistic database with parallel validation
|
|
348
|
+
occValidation: 'parallel', // or 'serial'; applies to subsequent optimistic opens
|
|
258
349
|
writeBufferManagerAllowStall: false,
|
|
259
350
|
writeBufferManagerCostToCache: false,
|
|
260
351
|
writeBufferManagerSize: 64 * 1024 * 1024, // 64MB
|
|
@@ -574,7 +665,16 @@ db.compactSync({ bottommost: true });
|
|
|
574
665
|
### `db.destroy(): void`
|
|
575
666
|
|
|
576
667
|
Completely removes a database based on the `db` instance's path including all data, column families,
|
|
577
|
-
and files on disk.
|
|
668
|
+
and files on disk. Destruction owns the physical path for the process: it closes every writable and
|
|
669
|
+
read-only handle for that path, waits for registered backups and checkpoints to stop using the
|
|
670
|
+
native database, and prevents another handle from reopening the path until removal finishes. Those
|
|
671
|
+
waits are synchronous and can outlive `lifecycleWaitSeconds` once destruction has claimed the path,
|
|
672
|
+
because releasing the native database beneath an active copy would be unsafe.
|
|
673
|
+
|
|
674
|
+
A previously opened instance does not need to remain open, which allows an explicit `destroy()`
|
|
675
|
+
retry after failed physical cleanup. A never-opened or read-only instance cannot destroy the
|
|
676
|
+
database. `shutdown()` reports a pending cleanup tombstone but never retries deletion; only an
|
|
677
|
+
explicit `destroy()` can remove the path.
|
|
578
678
|
|
|
579
679
|
```typescript
|
|
580
680
|
db.destroy();
|
|
@@ -583,8 +683,8 @@ console.log(fs.existsSync(db.path)); // false
|
|
|
583
683
|
|
|
584
684
|
### `db.drop(): Promise<void>`
|
|
585
685
|
|
|
586
|
-
|
|
587
|
-
|
|
686
|
+
Drops the column family the database was opened with (`name`). For the default column family this
|
|
687
|
+
clears all entries instead.
|
|
588
688
|
|
|
589
689
|
```typescript
|
|
590
690
|
const db = RocksDatabase.open('path/to/db', { name: 'users' });
|
|
@@ -592,9 +692,56 @@ await db.drop();
|
|
|
592
692
|
db.close();
|
|
593
693
|
```
|
|
594
694
|
|
|
695
|
+
#### Dropping column families
|
|
696
|
+
|
|
697
|
+
A drop retires the column family **logically** before it returns: the name is gone from
|
|
698
|
+
`db.columns`, a later `open()` with the same name creates a fresh, empty column family, and any
|
|
699
|
+
transaction that then stages a write to a handle of the dropped family, or commits one it staged
|
|
700
|
+
earlier, is refused whole with `ERR_COLUMN_FAMILY_DROPPED` (`Column family "users" was dropped`).
|
|
701
|
+
That terminal refusal releases the transaction's verification-table intents and bars further
|
|
702
|
+
writes or commit attempts; retained reads continue until the caller aborts the transaction, and
|
|
703
|
+
they still serve that transaction's own staged writes — values no commit will ever produce. A
|
|
704
|
+
caller that catches the refusal instead of letting it propagate must not read a value back through
|
|
705
|
+
the transaction and carry it forward.
|
|
706
|
+
If a staging call fails while the generation is being retired, retirement takes precedence and the
|
|
707
|
+
transaction is refused whole even when the immediate RocksDB failure was a pessimistic lock timeout.
|
|
708
|
+
Handles other threads still hold keep **reading** the dropped data until they close; a
|
|
709
|
+
non-transactional `putSync`/`removeSync` through such a handle is discarded.
|
|
710
|
+
|
|
711
|
+
The **physical** RocksDB drop is deferred behind commits already admitted when the drop lands: a
|
|
712
|
+
commit claims every column family its batch names before it writes its transaction-log batch and
|
|
713
|
+
releases them after RocksDB has applied it, and the physical drop runs from whichever
|
|
714
|
+
side releases last (or, for a commit a mid-flight `close()` tore out of its pipeline, from the next
|
|
715
|
+
drop, open of that name, or close on the database). With no such commit (the common case) `drop()`/`dropSync()` perform the
|
|
716
|
+
physical drop before returning, exactly as before. This is what keeps a drop racing another
|
|
717
|
+
thread's commit from latching RocksDB's fatal `Invalid column family specified in write batch`
|
|
718
|
+
error on the whole database.
|
|
719
|
+
|
|
720
|
+
Consequences to know about:
|
|
721
|
+
|
|
722
|
+
- A commit admitted before the drop completes successfully into the retiring generation, then the
|
|
723
|
+
physical drop removes that generation. Its caller sees a successful commit, but those writes are
|
|
724
|
+
intentionally discarded with the rest of the dropped column family; a same-name reopen creates
|
|
725
|
+
a fresh, empty generation. If the transaction writes to a transaction log, its entries are still
|
|
726
|
+
published; consumers must order the schema drop after those entries.
|
|
727
|
+
- `open()` of a name whose previous generation is still held by an admitted commit waits for the
|
|
728
|
+
full admission-to-reclamation interval (bounded by `ROCKSDB_JS_CF_RECLAIM_WAIT_MS`, default
|
|
729
|
+
`30000`) before creating the fresh column family; if the previous generation's physical drop
|
|
730
|
+
failed, the open retries it once and throws with that error if it fails again.
|
|
731
|
+
- A physical drop that fails (an I/O error writing the MANIFEST) keeps the name retired, is
|
|
732
|
+
retried on the next drop on the database, the next `open()` of that name, or close, and is
|
|
733
|
+
reported through the global `log.warn` event and the `columnFamily.pendingReclaims` stat. When
|
|
734
|
+
the failing drop was the one `drop()` itself ran, the call rejects with that error as well.
|
|
735
|
+
|
|
736
|
+
What is **not** guaranteed: a process that exits while a physical drop is still pending, or
|
|
737
|
+
after one failed, leaves the column family on disk under its name, and the next open of the
|
|
738
|
+
database opens it as a live column family. A backup or checkpoint taken inside that window copies
|
|
739
|
+
it. Callers that need a drop to survive a crash record their own durable tombstone before
|
|
740
|
+
acknowledging it.
|
|
741
|
+
|
|
595
742
|
### `db.dropSync(): void`
|
|
596
743
|
|
|
597
|
-
Synchronous version of `db.drop()
|
|
744
|
+
Synchronous version of `db.drop()`, with the same deferral contract.
|
|
598
745
|
|
|
599
746
|
```typescript
|
|
600
747
|
const db = RocksDatabase.open('path/to/db');
|
|
@@ -620,9 +767,9 @@ Flushes all in-memory data to disk asynchronously.
|
|
|
620
767
|
memtable switch can itself prolong an existing L0 stop-trigger condition rather than clear it.
|
|
621
768
|
That cost is database-wide, covering every column family on the (process-global,
|
|
622
769
|
`worker_threads`-shared) database handle, not just the caller's — and it relocates the hang
|
|
623
|
-
rather than removing it: a stalled write blocks the database's
|
|
624
|
-
|
|
625
|
-
|
|
770
|
+
rather than removing it: a stalled write blocks each of the database's commit threads that
|
|
771
|
+
reaches it, so every async `Transaction.commit()` behind them queues up too, including ones
|
|
772
|
+
from callers that never touched flush. This is a different knob from
|
|
626
773
|
`writeBufferManagerAllowStall` (see [`new RocksDatabase()`](#new-rocksdatabasepath-options)
|
|
627
774
|
options), with nearly opposite polarity: that one governs whether the `WriteBufferManager` may
|
|
628
775
|
stall writers at all, this one governs whether one manual flush is willing to cause a stall
|
|
@@ -796,13 +943,47 @@ const range = db.getKeysCount({ start: 'a', end: 'z' }); // exact number of keys
|
|
|
796
943
|
### `db.getMonotonicTimestamp(): number`
|
|
797
944
|
|
|
798
945
|
Returns the current timestamp as a monotonically increasing timestamp in milliseconds represented as
|
|
799
|
-
a decimal number. This process-wide clock
|
|
946
|
+
a decimal number. This process-wide clock supplies each transaction's initial timestamp, which is
|
|
947
|
+
also the key of the transaction-log batch the transaction is written under.
|
|
800
948
|
|
|
801
949
|
```typescript
|
|
802
950
|
const ts = db.getMonotonicTimestamp();
|
|
803
951
|
console.log(ts); // 1764307857213.739
|
|
804
952
|
```
|
|
805
953
|
|
|
954
|
+
It is a wall-clock (Unix epoch) value made strictly increasing: on a tie or a backward step of the
|
|
955
|
+
host clock it advances by one floating-point ulp per call until the wall clock catches up. That
|
|
956
|
+
keeps transaction timestamps ordered and durable, but it does not measure elapsed time — after a
|
|
957
|
+
backward step, differences between two calls understate real time. Use
|
|
958
|
+
[`steadyClockNow()`](#steadyclocknow-number) for elapsed durations and deadlines.
|
|
959
|
+
|
|
960
|
+
#### Timestamp floor at open
|
|
961
|
+
|
|
962
|
+
The clock is monotonic within a process, not across restarts: a new process reads the wall clock
|
|
963
|
+
again, so a backward step between runs can reissue a timestamp that is already a batch key in this
|
|
964
|
+
node's transaction log. Opening with
|
|
965
|
+
[`timestampFloorLog`](#new-rocksdatabasepath-options) raises the clock
|
|
966
|
+
above every batch key still durable in the named log before the database handle is returned, so no
|
|
967
|
+
transaction can be constructed below it.
|
|
968
|
+
|
|
969
|
+
Name only a log this process **originates**. A log written under a timestamp adopted from another
|
|
970
|
+
node — a replication receiver calling [`txn.setTimestamp()`](#txnsettimestampts-number-void) — is
|
|
971
|
+
keyed by that node's clock; seeding from it would ratchet this process's clock to the fastest of
|
|
972
|
+
those nodes at every restart, and there is no way for the database to tell the two kinds of log
|
|
973
|
+
apart on its own.
|
|
974
|
+
|
|
975
|
+
The named floor is fail closed: an unreadable segment, incomplete scan, malformed framing, or a key
|
|
976
|
+
more than ten years ahead of the wall clock refuses the opted-in open rather than reissuing a durable
|
|
977
|
+
batch key. The floor is process-wide, so it applies to every database open in the process. The scan
|
|
978
|
+
runs while the process serializes database opens and closes, so increasing its budget can delay
|
|
979
|
+
unrelated opens and closes too.
|
|
980
|
+
|
|
981
|
+
Naming an absent log warns so a fresh local log can be configured before its first write. The first
|
|
982
|
+
use may create that fresh log, but if a nonempty directory appears after the warning it is refused:
|
|
983
|
+
those keys were not part of the seed, and a transaction timestamp is already chosen before
|
|
984
|
+
`txn.useLog()` could scan it. Close every handle for the path and reopen with `timestampFloorLog` to
|
|
985
|
+
scan the restored log before using it.
|
|
986
|
+
|
|
806
987
|
### `db.getOldestSnapshotTimestamp(): number`
|
|
807
988
|
|
|
808
989
|
Returns a number representing a unix timestamp of the oldest unreleased snapshot.
|
|
@@ -928,8 +1109,13 @@ Returns a new `ArrayBuffer` with two additional methods:
|
|
|
928
1109
|
- `cancel()` - Removes the callback; future `notify()` calls do nothing
|
|
929
1110
|
|
|
930
1111
|
Note: If a shared buffer already exists for the given `key`, the returned `ArrayBuffer` will
|
|
931
|
-
reference this existing shared buffer.
|
|
932
|
-
|
|
1112
|
+
reference this existing shared buffer. The buffer lives as long as its column family is open (a
|
|
1113
|
+
`drop()` discards it with the column family — except the default column family, which `drop()`
|
|
1114
|
+
only clears, so its buffers survive): it is process-wide state, so a thread dropping or garbage
|
|
1115
|
+
collecting its own view (or exiting) never resets it for the others. Because nothing is ever
|
|
1116
|
+
evicted, key the buffer on a small fixed set of names rather than on unbounded data such as
|
|
1117
|
+
record ids. The notify callback is removed when the `ArrayBuffer` it was registered with is
|
|
1118
|
+
garbage collected, or by `cancel()`.
|
|
933
1119
|
|
|
934
1120
|
```typescript
|
|
935
1121
|
const buffer = new Uint8Array(db.getUserSharedBuffer('isDone', new ArrayBuffer(1)));
|
|
@@ -1585,9 +1771,7 @@ Set the algorithm per column family with the `compression` option when opening a
|
|
|
1585
1771
|
const db = RocksDatabase.open('/path/to/db', { compression: 'zstd' });
|
|
1586
1772
|
|
|
1587
1773
|
// Or an object with an explicit level
|
|
1588
|
-
const db2 = RocksDatabase.open('/path/to/db2', {
|
|
1589
|
-
compression: { algorithm: 'zstd', level: 3 },
|
|
1590
|
-
});
|
|
1774
|
+
const db2 = RocksDatabase.open('/path/to/db2', { compression: { algorithm: 'zstd', level: 3 } });
|
|
1591
1775
|
|
|
1592
1776
|
// Disable compression entirely
|
|
1593
1777
|
const db3 = RocksDatabase.open('/path/to/db3', { compression: 'none' });
|
|
@@ -1604,16 +1788,6 @@ was compiled against, so it varies by build. Always check
|
|
|
1604
1788
|
[`supportedCompression`](#supportedcompression) at runtime — opening with an unavailable algorithm
|
|
1605
1789
|
throws. `'none'` is always available.
|
|
1606
1790
|
|
|
1607
|
-
**Changing the codec of data already written.** Setting `compression` affects files written from
|
|
1608
|
-
that point on. Existing SST and blob files keep the codec they were written with until something
|
|
1609
|
-
rewrites them. An ordinary `compact()` can re-encode non-bottommost levels, but RocksDB leaves the
|
|
1610
|
-
bottommost level alone unless a compaction filter is installed — and that is where most of the data
|
|
1611
|
-
sits, so a plain compaction will not rewrite all existing data. Use
|
|
1612
|
-
[`compact({ bottommost: true })`](#dbcompactoptions-promisevoid) to force the rewrite, once per
|
|
1613
|
-
column family you want migrated. Note also that omitting `compression` on a column family that
|
|
1614
|
-
already exists **inherits** its current codec rather than applying the default — the default
|
|
1615
|
-
applies only when the family is created.
|
|
1616
|
-
|
|
1617
1791
|
**Adopting a codec across a whole database.** Compression is chosen per column family, and RocksDB
|
|
1618
1792
|
opens every family of a database in one call — so by default the families you did not name keep
|
|
1619
1793
|
their persisted algorithm. If your first open targets some other family (a catalog, say), the rest
|
|
@@ -1643,10 +1817,90 @@ changeable, so an explicit change governs files written afterward while existing
|
|
|
1643
1817
|
their original compression until rewritten by compaction. It also applies to blob files (large
|
|
1644
1818
|
values), whose compression otherwise defaults to none.
|
|
1645
1819
|
|
|
1646
|
-
Because
|
|
1647
|
-
a second time in the same process (another
|
|
1648
|
-
`
|
|
1649
|
-
plain reopen (no `compression`) instead
|
|
1820
|
+
Because an open-time `compression` option applies only when a column family is first opened in a
|
|
1821
|
+
process, if the same column family is opened a second time in the same process (another
|
|
1822
|
+
`RocksDatabase` on the same path/`name`, including from a `worker_thread`) with an **explicitly
|
|
1823
|
+
different** algorithm or level, the second open **throws** — a plain reopen (no `compression`) instead
|
|
1824
|
+
inherits the live setting. This is a structural limit of the open-time API: `rocksdb::DB::Open()` opens
|
|
1825
|
+
every column family in the database transitively in one call, so by the time a caller can name a
|
|
1826
|
+
newly-discovered column family explicitly, RocksDB has already opened it. Use `db.setCompression()`
|
|
1827
|
+
below to change the algorithm of an already-open column family without a reopen.
|
|
1828
|
+
|
|
1829
|
+
### `db.setCompression(compression: string | { algorithm: string, level?: number }): void`
|
|
1830
|
+
|
|
1831
|
+
Dynamically changes the compression algorithm (and optional level) for an **already-open** column
|
|
1832
|
+
family, live, with no close/reopen and no conflict with other handles that already have this column
|
|
1833
|
+
family open elsewhere in the process. It accepts the same `compression` shape as the open-time option:
|
|
1834
|
+
|
|
1835
|
+
```typescript
|
|
1836
|
+
const db = RocksDatabase.open('/path/to/db', { compression: 'none' });
|
|
1837
|
+
|
|
1838
|
+
// ... database is upgraded/reconfigured to prefer zstd ...
|
|
1839
|
+
db.setCompression({ algorithm: 'zstd', level: 19 });
|
|
1840
|
+
// new writes are now compressed with zstd/level 19; db is still open and usable.
|
|
1841
|
+
```
|
|
1842
|
+
|
|
1843
|
+
This is backed by RocksDB's `DB::SetOptions()`, which the RocksDB headers document as dynamically
|
|
1844
|
+
mutable for exactly the fields this binding needs: `ColumnFamilyOptions::compression` and
|
|
1845
|
+
`AdvancedColumnFamilyOptions::blob_compression_type` are both annotated `// Dynamically changeable
|
|
1846
|
+
through SetOptions() API` (`rocksdb/options.h`, `rocksdb/advanced_options.h`). `setCompression` sets
|
|
1847
|
+
both together (mirroring the open-time option) so blob-stored large values don't stay uncompressed
|
|
1848
|
+
after a live algorithm change. It was chosen over a per-CF options map on the root/default open call
|
|
1849
|
+
(the alternative design considered) because that alternative doesn't fit the motivating use case at
|
|
1850
|
+
all: a caller that doesn't yet know a column family's desired codec until _after_ reading its own
|
|
1851
|
+
catalog — itself a column family opened by that very same call — has no way to supply per-CF options
|
|
1852
|
+
up front. `SetOptions()` sidesteps the problem structurally: it operates on a column family that is
|
|
1853
|
+
already open, after the caller has had a chance to learn what codec it wants.
|
|
1854
|
+
|
|
1855
|
+
**Verified semantics** (RocksDB's own `SetOptions()` doc explicitly limits the guarantee to what's
|
|
1856
|
+
listed as mutable — this was checked directly against the linked RocksDB build, not assumed):
|
|
1857
|
+
|
|
1858
|
+
- **Governs new writes only, going forward.** The very next flush (memtable → SST) and any future
|
|
1859
|
+
compaction output use the new algorithm; SST/blob files already on disk are untouched by the call
|
|
1860
|
+
itself and keep their old compression until they are naturally rewritten by a later compaction.
|
|
1861
|
+
This exactly matches the open-time `compression` option's existing "governs subsequently written
|
|
1862
|
+
files" semantics — `setCompression` is the live-mutation path to the same effective behavior,
|
|
1863
|
+
without the reopen restriction above. An ordinary `db.compact()`/`db.compactSync()` can leave
|
|
1864
|
+
already-bottommost files untouched because RocksDB skips that level by default when no compaction
|
|
1865
|
+
filter is configured. Use `db.compact({ bottommost: true })` or
|
|
1866
|
+
`db.compactSync({ bottommost: true })` to force those files to be rewritten under the new codec.
|
|
1867
|
+
- **The level is a partial update.** The new level is applied via RocksDB's nested-option syntax
|
|
1868
|
+
(`compression_opts={level=N;}`), which — per RocksDB's own documented example for this same API
|
|
1869
|
+
(`db->SetOptions(cfh, {{"block_based_table_factory", "{prepopulate_block_cache=kDisable;}"}})`)
|
|
1870
|
+
— only touches the field(s) named; every other `CompressionOptions` sub-field (window size,
|
|
1871
|
+
strategy, dictionary training, ...) is left at its current live value. Omitting `level` resets it to
|
|
1872
|
+
the algorithm's default (RocksDB's `kDefaultCompressionLevel` sentinel), matching the open-time
|
|
1873
|
+
option's "an explicit algorithm without a level does not inherit a previously-set level" behavior —
|
|
1874
|
+
it is never silently carried over from before the call.
|
|
1875
|
+
- **A persisted no-op request is free.** If the requested algorithm, blob algorithm, and effective
|
|
1876
|
+
level already match the live options, `setCompression` skips the RocksDB call entirely rather than
|
|
1877
|
+
paying for a no-op OPTIONS-file write. If a prior attempt applied the setting in memory but failed to
|
|
1878
|
+
persist it, the same request retries the persistence step instead. This matters because RocksDB's
|
|
1879
|
+
own `SetOptions()` doc calls it "a slow call because a new OPTIONS file is serialized and persisted
|
|
1880
|
+
for each call. Use only infrequently" — a caller that reconciles many column families against a
|
|
1881
|
+
catalog on every boot will find most of them already at the desired codec, and this keeps that sweep
|
|
1882
|
+
cheap.
|
|
1883
|
+
- **A persistence failure can split live and durable state.** RocksDB applies the new options in
|
|
1884
|
+
memory first, then persists an OPTIONS file reflecting them; the returned status covers the persist
|
|
1885
|
+
step alone. On a failure (e.g. the volume is full or read-only), the column family may already be
|
|
1886
|
+
running the new algorithm in memory while the on-disk OPTIONS file still holds the old one —
|
|
1887
|
+
`setCompression` detects this case and says so explicitly in the thrown error's message, since a
|
|
1888
|
+
later cold reopen (which trusts only the OPTIONS file) will silently revert to the old algorithm.
|
|
1889
|
+
- **Rejected on a read-only database.** `setCompression` persists a new OPTIONS file, which a
|
|
1890
|
+
read-only handle (`{ readOnly: true }`) may not do — it throws `ERR_DATABASE_READONLY`, matching
|
|
1891
|
+
every other mutating operation on this binding.
|
|
1892
|
+
- **Read your write.** `db.compression` reflects the change immediately (it reads live via
|
|
1893
|
+
`DB::GetOptions()`), and so does the already-open-column-family conflict check described above: once
|
|
1894
|
+
`setCompression` has changed the live algorithm, a second explicit `RocksDatabase.open()` on that
|
|
1895
|
+
column family is compared against the **new** value, not the one it was originally opened with.
|
|
1896
|
+
- **Overrides another handle's explicit codec without notice.** The open-time conflict check exists
|
|
1897
|
+
so two handles on the same column family can't disagree about compression — but `setCompression`
|
|
1898
|
+
bypasses it by design: it changes the live setting for every handle on that column family, including
|
|
1899
|
+
one that originally opened with an explicit, different algorithm. There is currently no event or
|
|
1900
|
+
other signal to such a handle when this happens.
|
|
1901
|
+
- **Does not change `RocksDatabase.open`'s existing open-time behavior.** `compression` at open time
|
|
1902
|
+
behaves exactly as before for a column family that is not already open in the process; `setCompression`
|
|
1903
|
+
is purely additive for the already-open case.
|
|
1650
1904
|
|
|
1651
1905
|
### `supportedCompression`
|
|
1652
1906
|
|
|
@@ -1808,8 +2062,12 @@ intended for read-heavy workloads where records carry a monotonically increasing
|
|
|
1808
2062
|
Each record's version is the numeric value stored in the first 8 bytes of its value (interpreted as
|
|
1809
2063
|
a big-endian float64). The table maps `(database, column family, key)` to a single 8-byte slot that
|
|
1810
2064
|
holds the last-known version for that key. Because slots are addressed by a hash, distinct keys may
|
|
1811
|
-
share a slot
|
|
1812
|
-
|
|
2065
|
+
share a slot. A slot stores the version combined with a tag derived from its key, so a version that
|
|
2066
|
+
another key cached in a shared slot — even the same version, as keys written in one transaction
|
|
2067
|
+
often carry — answers fresh only if the two keys' version-and-tag combinations coincide; otherwise
|
|
2068
|
+
the collision costs only a conservative miss (a real read). With the default 131,072 slots that
|
|
2069
|
+
coincidence has a probability of 2^-47 per lookup that finds another key's version in its slot, and
|
|
2070
|
+
each doubling of `verificationTableEntries` doubles it.
|
|
1813
2071
|
|
|
1814
2072
|
This first word is the only version the table derives on its own: it is what a read with
|
|
1815
2073
|
`populateVersion: true` publishes and what a transaction write invalidates against. A producer whose
|
|
@@ -1889,13 +2147,18 @@ Transaction log files are read through read-only memory maps. Understanding how
|
|
|
1889
2147
|
with system memory helps when interpreting [`log.getStats()`](#loggetstats-transactionlogstats)
|
|
1890
2148
|
figures and process memory usage:
|
|
1891
2149
|
|
|
1892
|
-
- **Maps are created lazily
|
|
1893
|
-
anything; a log file is mapped the first time a `log.query()`
|
|
1894
|
-
|
|
1895
|
-
the
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2150
|
+
- **Maps are created lazily; only the current (still-being-written) file's map is held natively.**
|
|
2151
|
+
Writing log entries does not map anything; a log file is mapped the first time a `log.query()`
|
|
2152
|
+
reads from it. The native layer holds the current file's map for as long as it stays current —
|
|
2153
|
+
released when the log file rotates, is purged, or the database is closed. On POSIX, once a file
|
|
2154
|
+
rotates out or is purged, the native layer keeps only a weak reference: the mapping survives
|
|
2155
|
+
solely through JS `Buffer` views over it (including `entry.data`) and is released by garbage
|
|
2156
|
+
collection once none remain, independent of purge or close, and `stats.memory.activeMaps` counts
|
|
2157
|
+
only the current file's strongly-held map — a frozen file's map is weakly held and not counted,
|
|
2158
|
+
even while a JS `Buffer` view keeps it alive. **On Windows this weak-ownership optimization does
|
|
2159
|
+
not apply**: every frozen read re-creates the mapping and re-pins it strongly for the rest of the
|
|
2160
|
+
file's life, so `stats.memory.activeMaps`/`mappedBytes` count every frozen map a process has read,
|
|
2161
|
+
not just the current file's.
|
|
1899
2162
|
- **Mapped bytes are virtual, not resident.** Creating a map reserves address space only. A page
|
|
1900
2163
|
consumes physical RAM (RSS) when it is first read (demand paging). Querying a multi-gigabyte log
|
|
1901
2164
|
can show `memory.mappedBytes` in the gigabytes while actual memory usage barely moves.
|
|
@@ -1911,9 +2174,10 @@ figures and process memory usage:
|
|
|
1911
2174
|
OS-specific differences:
|
|
1912
2175
|
|
|
1913
2176
|
- **POSIX (Linux and macOS):** The active write file is mapped at the full configured
|
|
1914
|
-
`transactionLogMaxSize` (an anonymous reservation with the file's contents overlaid on top),
|
|
1915
|
-
|
|
1916
|
-
|
|
2177
|
+
`transactionLogMaxSize` (an anonymous reservation with the file's contents overlaid on top), or
|
|
2178
|
+
at its size when a single transaction pushed it past that target, so `memory.mappedBytes`
|
|
2179
|
+
over-reports the active file; `memory.overlayBytes` is the file-backed portion and is the closer
|
|
2180
|
+
proxy for real consumption.
|
|
1917
2181
|
- **macOS:** Activity Monitor's "Memory" column reports the physical footprint, which excludes
|
|
1918
2182
|
clean file-backed pages — mapped log data is essentially invisible there even when resident. Use
|
|
1919
2183
|
process RSS (e.g. `process.memoryUsage().rss`, `ps`, or `vmmap <pid>`) to observe it.
|
|
@@ -2212,6 +2476,51 @@ Options:
|
|
|
2212
2476
|
Validating a store that is being actively appended to can spuriously report a torn tail for the
|
|
2213
2477
|
current log file — the tail of an in-flight append is indistinguishable from a crash artifact.
|
|
2214
2478
|
|
|
2479
|
+
### `steadyClockNow(): number`
|
|
2480
|
+
|
|
2481
|
+
Reads the process-wide steady clock. No database handle is needed.
|
|
2482
|
+
|
|
2483
|
+
```typescript
|
|
2484
|
+
import { steadyClockNow } from '@harperfast/rocksdb-js';
|
|
2485
|
+
|
|
2486
|
+
const start = steadyClockNow();
|
|
2487
|
+
// ... work, possibly on other worker threads ...
|
|
2488
|
+
const elapsedMs = steadyClockNow() - start;
|
|
2489
|
+
```
|
|
2490
|
+
|
|
2491
|
+
Contract:
|
|
2492
|
+
|
|
2493
|
+
- **Units / type**: milliseconds with a fractional part, as a `number`. Read from
|
|
2494
|
+
`std::chrono::steady_clock` (`CLOCK_MONOTONIC` on Linux, `CLOCK_MONOTONIC_RAW` on macOS,
|
|
2495
|
+
`QueryPerformanceCounter` on Windows).
|
|
2496
|
+
- **Origin and lifetime**: the origin is unspecified and fixed for the life of the process. Every
|
|
2497
|
+
sample taken in the process is in one domain — the main thread and every `worker_threads` worker,
|
|
2498
|
+
including workers started or restarted at any later time, under Node, Bun and Deno alike. Samples
|
|
2499
|
+
are not meaningful across processes or restarts, and are unrelated to the Unix epoch: never compare
|
|
2500
|
+
them with `Date.now()`, `db.getMonotonicTimestamp()` or transaction timestamps.
|
|
2501
|
+
- **Monotonic, not unique**: a sample taken after another (in real time, on any thread) is `>=` it.
|
|
2502
|
+
Two samples can be equal — in the same clock tick, or when two distinct readings round to the same
|
|
2503
|
+
double — so treat `==` as "not later" and pair the sample with a sequence number if strict ordering
|
|
2504
|
+
is required. On Windows, QPC samples from different threads within ±1 native counter tick also
|
|
2505
|
+
have ambiguous ordering; do not use near-equal clock samples alone to prove causality. No
|
|
2506
|
+
wall-clock ratchet is applied.
|
|
2507
|
+
- **Precision**: the double's spacing grows with distance from the origin: about 2 ns at 100 days,
|
|
2508
|
+
61 ns at 10 years, 0.49 µs at 100 years. Conversion error is below 0.51 µs per sample through
|
|
2509
|
+
100 years from the native origin. This is representation precision, not clock accuracy. Rounding is
|
|
2510
|
+
monotone, so distinct readings can only collapse to equality, never invert. The clock's own
|
|
2511
|
+
resolution is platform-defined (the C++ duration period is 1 ns on these platforms; actual clock
|
|
2512
|
+
resolution can be coarser). The native signed 64-bit nanosecond range is about 292 years in either
|
|
2513
|
+
direction from its origin; conversion to `number` does not narrow it.
|
|
2514
|
+
- **Independent of the wall clock**: `settimeofday`/NTP steps in either direction do not move it. NTP
|
|
2515
|
+
frequency slew is platform-defined (`CLOCK_MONOTONIC` is slewed; `CLOCK_MONOTONIC_RAW` and
|
|
2516
|
+
`QueryPerformanceCounter` are not).
|
|
2517
|
+
- **Suspend**: whether time the host spends suspended counts is platform-defined — `CLOCK_MONOTONIC`
|
|
2518
|
+
excludes it on Linux; current macOS `CLOCK_MONOTONIC_RAW` and Windows QPC include it. Do not
|
|
2519
|
+
assume runtime timers (`setTimeout`) use the same suspend policy. Portable suspend-inclusive
|
|
2520
|
+
deadlines are not provided by this API.
|
|
2521
|
+
- **Cost**: one clock read and a double conversion; no lock, no native allocation, no wall-clock read,
|
|
2522
|
+
no database or log I/O, no per-worker calibration. Safe from any thread. The runtime may allocate a boxed JS number.
|
|
2523
|
+
|
|
2215
2524
|
### `currentThreadId(): number`
|
|
2216
2525
|
|
|
2217
2526
|
Returns the current thread ID.
|
|
@@ -2221,33 +2530,49 @@ import { currentThreadId } from '@harperfast/rocksdb-js';
|
|
|
2221
2530
|
console.log(currentThreadId());
|
|
2222
2531
|
```
|
|
2223
2532
|
|
|
2224
|
-
### `
|
|
2533
|
+
### `getRegistryStatus(): RegistryStatus`
|
|
2225
2534
|
|
|
2226
2535
|
Returns an array containing that status of all active RocksDB instances.
|
|
2227
2536
|
|
|
2228
2537
|
- `path: string` The database path.
|
|
2538
|
+
- `closeError?: string` The native lifecycle error retaining this registry entry.
|
|
2539
|
+
- `destroyCleanupPending?: boolean` The native database is closed, but physical path cleanup must
|
|
2540
|
+
finish before the next open. Call `destroy()` to retry cleanup.
|
|
2229
2541
|
- `refCount: number` The number of JavaScript database instances plus the registry's reference.
|
|
2230
2542
|
- `columnFamiles: object` A map of column family names and their their info.
|
|
2231
|
-
- `userSharedBuffers: number` The count of
|
|
2543
|
+
- `userSharedBuffers: number` The count of user shared buffer keys created since the column family
|
|
2544
|
+
was opened; entries are never evicted, so this never decreases.
|
|
2232
2545
|
- `transactions: number` The count of active transactions.
|
|
2233
2546
|
- `closables: number` The count of active database, transactions, and iterators.
|
|
2234
2547
|
- `locks: number` The count of active locks.
|
|
2235
2548
|
- `listenerCallbacks: number` The count of in-flight callbacks.
|
|
2236
2549
|
|
|
2237
2550
|
```typescript
|
|
2238
|
-
import {
|
|
2239
|
-
console.log(
|
|
2551
|
+
import { getRegistryStatus } from '@harperfast/rocksdb-js';
|
|
2552
|
+
console.log(getRegistryStatus());
|
|
2240
2553
|
```
|
|
2241
2554
|
|
|
2242
2555
|
### `shutdown(): void`
|
|
2243
2556
|
|
|
2244
2557
|
The `shutdown()` will flush all in-memory data to disk and wait for any outstanding compactions to
|
|
2245
2558
|
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
|
|
2559
|
+
listener (on the main thread), to ensure that all data is flushed to disk before the process exits.
|
|
2560
|
+
It throws the first close failure after attempting every claimed database; call it again to retry
|
|
2561
|
+
any descriptor whose native teardown did not complete. It reports pending destroy-cleanup
|
|
2562
|
+
tombstones without deleting their paths; retry those with an explicit `destroy()`.
|
|
2563
|
+
|
|
2564
|
+
Wrap the call: an exception thrown from an `exit` listener skips every `exit` listener registered
|
|
2565
|
+
after it, and sets the exit code to 1 unless an `uncaughtException` handler is installed.
|
|
2247
2566
|
|
|
2248
2567
|
```typescript
|
|
2249
2568
|
import { shutdown } from '@harperfast/rocksdb-js';
|
|
2250
|
-
process.on('exit',
|
|
2569
|
+
process.on('exit', () => {
|
|
2570
|
+
try {
|
|
2571
|
+
shutdown();
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
console.error('rocksdb-js shutdown failed', error);
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2251
2576
|
```
|
|
2252
2577
|
|
|
2253
2578
|
### `versions: { 'rocksdb': string; 'rocksdb-js': string }`
|
|
@@ -2339,9 +2664,7 @@ live database directory would write backup files on top of RocksDB's own files.
|
|
|
2339
2664
|
before anything is written.
|
|
2340
2665
|
|
|
2341
2666
|
```typescript
|
|
2342
|
-
const id = await db.backup('/path/to/backups', {
|
|
2343
|
-
metadata: 'nightly-2026-06-04',
|
|
2344
|
-
});
|
|
2667
|
+
const id = await db.backup('/path/to/backups', { metadata: 'nightly-2026-06-04' });
|
|
2345
2668
|
```
|
|
2346
2669
|
|
|
2347
2670
|
`BackupOptions`:
|
|
@@ -2391,9 +2714,7 @@ await db.backup(Writable.toWeb(createWriteStream('/path/to/backup.tar')));
|
|
|
2391
2714
|
// Restore: `tar -xf backup.tar -C /restored`, then open '/restored'.
|
|
2392
2715
|
|
|
2393
2716
|
// Or gzip it (`tar -xzf backup.tar.gz` to restore):
|
|
2394
|
-
await db.backup(Writable.toWeb(createWriteStream('/path/to/backup.tar.gz')), {
|
|
2395
|
-
gzip: true,
|
|
2396
|
-
});
|
|
2717
|
+
await db.backup(Writable.toWeb(createWriteStream('/path/to/backup.tar.gz')), { gzip: true });
|
|
2397
2718
|
```
|
|
2398
2719
|
|
|
2399
2720
|
`BackupStreamOptions`:
|
|
@@ -2671,7 +2992,7 @@ Available commands:
|
|
|
2671
2992
|
|
|
2672
2993
|
## Development
|
|
2673
2994
|
|
|
2674
|
-
This package requires Node.js 18
|
|
2995
|
+
This package requires Node.js `^22.18.0 || >=24.0.0`, pnpm, and a C++ compiler.
|
|
2675
2996
|
|
|
2676
2997
|
> [!TIP]
|
|
2677
2998
|
> Enable pnpm log streaming to see full build output:
|