@harperfast/rocksdb-js 2.3.0 → 2.5.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 +205 -12
- package/bin/rocksdb-js.mjs +67 -3
- package/dist/index.cjs +600 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1610 -1365
- package/dist/index.d.mts +1610 -1365
- package/dist/index.mjs +601 -57
- package/dist/index.mjs.map +1 -1
- package/docs/backups.md +253 -0
- package/docs/stats.md +2 -0
- package/docs/transaction-log.md +10 -0
- package/package.json +23 -23
package/docs/backups.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Backups
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A backup is a **whole-database** copy: every column family, the manifest, and (by default) the
|
|
6
|
+
write-ahead log are captured as a consistent point-in-time snapshot. Backups are not scoped to an
|
|
7
|
+
individual store or column family.
|
|
8
|
+
|
|
9
|
+
There are two ways to produce a backup, plus a closely related third mechanism for fast local
|
|
10
|
+
copies:
|
|
11
|
+
|
|
12
|
+
| | Directory backup | Stream backup | Checkpoint |
|
|
13
|
+
| ------------------- | -------------------------------------- | -------------------------------------- | -------------------------------------- |
|
|
14
|
+
| **API** | `db.backup(dir)` | `db.backup(stream)` | `db.createCheckpoint(path)` |
|
|
15
|
+
| **Output** | a backup directory | a tar (optionally gzipped) byte stream | an independent database directory |
|
|
16
|
+
| **Local disk copy** | full copy into the directory | none — streamed out | hard links (same filesystem) or a copy |
|
|
17
|
+
| **Incremental** | yes — files shared across backups | no — always a full snapshot | n/a — each is independent |
|
|
18
|
+
| **Restore** | `backups.restore()` | extract the tar, then open | open the directory directly |
|
|
19
|
+
| **Resumable** | n/a | no | n/a |
|
|
20
|
+
| **Returns** | a numeric backup id | `void` | `void` |
|
|
21
|
+
| **Management API** | list / verify / delete / purge | none | none |
|
|
22
|
+
| **Best for** | scheduled local backups with retention | off-host upload with no scratch disk | fast branching / snapshots |
|
|
23
|
+
|
|
24
|
+
Directory and stream backups flush the memtable before copying **by default only when the database
|
|
25
|
+
was opened with `disableWAL`** (otherwise unflushed data would be missing); when the WAL is enabled
|
|
26
|
+
it is captured in the snapshot and replayed on restore, so committed data is never lost either way.
|
|
27
|
+
Checkpoints always flush the memtable.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Directory backups
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const id = await db.backup('/path/to/backups');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### How it works
|
|
38
|
+
|
|
39
|
+
Directory backups use RocksDB's `BackupEngine`. The live SST/blob files, manifest, and WAL are
|
|
40
|
+
**copied** into the backup directory. Parent directories are created as needed, and the call
|
|
41
|
+
resolves with a monotonically increasing **backup id**.
|
|
42
|
+
|
|
43
|
+
Multiple backups can live in the same directory. With file sharing enabled (the default), unchanged
|
|
44
|
+
SST/blob files are stored **once** and referenced by later backups — so the second and subsequent
|
|
45
|
+
backups into the same directory are effectively **incremental** and only copy what changed. Shared
|
|
46
|
+
files are reference-counted and removed only when no remaining backup references them.
|
|
47
|
+
|
|
48
|
+
### Options
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
await db.backup('/path/to/backups', {
|
|
52
|
+
flushBeforeBackup: true, // default: true iff opened with disableWAL
|
|
53
|
+
metadata: 'nightly-2026-06-26', // arbitrary string returned by backups.list()
|
|
54
|
+
shareTableFiles: true, // default: true — enables incremental backups
|
|
55
|
+
shareFilesWithChecksum: true, // default: true — dedup shared files by checksum
|
|
56
|
+
backupLogFiles: true, // default: true — include the WAL
|
|
57
|
+
sync: true, // default: true — fsync backup files
|
|
58
|
+
maxBackgroundOperations: 1, // default: 1 — copy threads
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Managing backups
|
|
63
|
+
|
|
64
|
+
The `backups` namespace operates on a backup directory and does **not** require an open database:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { backups } from '@harperfast/rocksdb-js';
|
|
68
|
+
|
|
69
|
+
await backups.list('/path/to/backups'); // BackupInfo[]
|
|
70
|
+
await backups.restore('/path/to/backups', '/restored'); // restore latest (or { backupId })
|
|
71
|
+
await backups.verify('/path/to/backups', id, { verifyWithChecksum: true });
|
|
72
|
+
await backups.delete('/path/to/backups', id); // remove one backup
|
|
73
|
+
await backups.purge('/path/to/backups', 3); // keep the newest 3
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`backups.verify` also validates the backup's transaction log snapshot when one was captured
|
|
77
|
+
(`transactionLogs: true`): every log file's header and entry framing must be intact, in strict mode
|
|
78
|
+
(snapshots are copied on committed entry boundaries, so even a recoverable torn tail fails
|
|
79
|
+
verification). Pass `verifyTransactionLogs: false` to check only the RocksDB files. Standalone
|
|
80
|
+
stores (e.g. the `transaction_logs` directory of a closed database) can be validated directly with
|
|
81
|
+
`validateTransactionLogStore()`.
|
|
82
|
+
|
|
83
|
+
### Restoring
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
await backups.restore('/path/to/backups', '/path/to/restored-db', {
|
|
87
|
+
backupId: 2, // default: the latest non-corrupt backup
|
|
88
|
+
walDir: '/restored-db', // default: the database directory
|
|
89
|
+
keepLogFiles: false,
|
|
90
|
+
mode: 'purgeAllFiles', // default — see below
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const restored = new RocksDatabase('/path/to/restored-db');
|
|
94
|
+
restored.open();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Restore modes:
|
|
98
|
+
|
|
99
|
+
- **`purgeAllFiles`** (default): purge the destination directory and restore everything. Destructive
|
|
100
|
+
but always correct.
|
|
101
|
+
- **`keepLatestDbSessionIdFiles`**: efficiently restore over a healthy database, reusing existing
|
|
102
|
+
files that match the backup.
|
|
103
|
+
- **`verifyChecksum`**: reuse existing destination files whose checksums match the backup, replacing
|
|
104
|
+
only mismatched/corrupt files.
|
|
105
|
+
|
|
106
|
+
Restore reads the backup directory and holds its lock in shared mode, so multiple restores run
|
|
107
|
+
concurrently but a writer (`db.backup()` / `delete` / `purge`) and a restore exclude each other.
|
|
108
|
+
Because the restore only reads, the source may be a **read-only backup directory** — an
|
|
109
|
+
immutable/WORM store or a read-only-mounted volume — even though writers require a writable one. The
|
|
110
|
+
lock is taken read-only on the existing lock file; if the media is read-only for every process it
|
|
111
|
+
degrades to a no-op (no writer can be racing), but a plain permission denial fails loudly rather than
|
|
112
|
+
skip coordination, since a more-privileged writer could still be active.
|
|
113
|
+
|
|
114
|
+
### Caveats
|
|
115
|
+
|
|
116
|
+
- **The data is duplicated onto local disk.** A backup is a real, separate copy in the backup
|
|
117
|
+
directory; it is not a hard link to the live database. Budget disk for it.
|
|
118
|
+
- **Incremental sharing is per-directory.** Files are only deduplicated against other backups in the
|
|
119
|
+
same backup directory.
|
|
120
|
+
- **Restore is destructive by default** (`purgeAllFiles`). Never restore into a directory you cannot
|
|
121
|
+
afford to have purged, and never restore into the backup directory itself (this is rejected, but
|
|
122
|
+
do not rely on it).
|
|
123
|
+
- **It needs a writable local directory.** To get a backup off the host you still have to copy the
|
|
124
|
+
directory afterward.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Stream backups
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { createWriteStream } from 'node:fs';
|
|
132
|
+
import { Writable } from 'node:stream';
|
|
133
|
+
|
|
134
|
+
// Any WHATWG WritableStream<Uint8Array> works. A Node stream adapts via Writable.toWeb():
|
|
135
|
+
const out = Writable.toWeb(createWriteStream('/path/to/backup.tar'));
|
|
136
|
+
await db.backup(out);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### How it works
|
|
140
|
+
|
|
141
|
+
Stream backups produce a **tar archive** of the database's live files and write it to a
|
|
142
|
+
`WritableStream` — with **no intermediate copy on disk**. The native layer enumerates the consistent
|
|
143
|
+
set of live files (via RocksDB's `GetLiveFilesStorageInfo`), reads each one, and the bytes are framed
|
|
144
|
+
into a [USTAR](<https://en.wikipedia.org/wiki/Tar_(computing)#UStar_format>) archive and written to the
|
|
145
|
+
stream. The result is byte-for-byte a snapshot of the database directory, so it unpacks with any tar
|
|
146
|
+
tool into a directory that opens directly as a RocksDB database.
|
|
147
|
+
|
|
148
|
+
**Backpressure is honored end to end.** Exactly one chunk is in flight at a time, and the native
|
|
149
|
+
producer waits for each `stream.write()` to drain before reading the next chunk. A slow consumer
|
|
150
|
+
(e.g. a network or object-store upload) therefore _paces_ the backup rather than buffering it in
|
|
151
|
+
memory — peak memory stays flat regardless of database size.
|
|
152
|
+
|
|
153
|
+
### Options
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
await db.backup(stream, {
|
|
157
|
+
flushBeforeBackup: true, // default: true iff opened with disableWAL
|
|
158
|
+
gzip: false, // default: false — set true for a .tar.gz stream
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Set `gzip: true` to compress the archive on the fly (via the runtime's `CompressionStream`),
|
|
163
|
+
producing a `.tar.gz` stream. Compression sits downstream of the tar encoder, so backpressure is
|
|
164
|
+
preserved. Note that RocksDB SST files are usually already block-compressed, so the extra savings
|
|
165
|
+
depend on your data and column-family compression settings.
|
|
166
|
+
|
|
167
|
+
### Restoring
|
|
168
|
+
|
|
169
|
+
There is no dedicated restore call — the archive _is_ the database. Unpack it with any tar tool and
|
|
170
|
+
open the directory:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
mkdir /path/to/restored-db
|
|
174
|
+
tar -xf backup.tar -C /path/to/restored-db
|
|
175
|
+
# For a gzipped stream (gzip: true), use -xzf backup.tar.gz instead.
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const restored = new RocksDatabase('/path/to/restored-db');
|
|
180
|
+
restored.open();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The archive contains the standard RocksDB files for the snapshot (`CURRENT`, a `MANIFEST-*`, the
|
|
184
|
+
`OPTIONS-*` file, the SST/blob files, and — unless flushed away — the WAL).
|
|
185
|
+
|
|
186
|
+
### Caveats
|
|
187
|
+
|
|
188
|
+
- **Not resumable.** The stream is a single, ordered pass over a live snapshot; there is no offset or
|
|
189
|
+
manifest to resume against, and the source snapshot is not retained after the call. If the
|
|
190
|
+
connection drops or the consumer errors partway through, the backup is incomplete and must be
|
|
191
|
+
**restarted from the beginning**. Treat a stream backup as valid only once it has been written and
|
|
192
|
+
closed in full. (For interrupted-transfer detection, the archive ends with the standard tar
|
|
193
|
+
end-of-archive marker — a truncated stream is missing it.)
|
|
194
|
+
- **Always a full snapshot.** Unlike directory backups, there is no file sharing or incremental
|
|
195
|
+
mode; every stream copies the entire database.
|
|
196
|
+
- **No management API.** `backups.list` / `verify` / `delete` / `purge` and `backups.restore`
|
|
197
|
+
operate on the `BackupEngine` directory format and do **not** understand tar streams. Listing,
|
|
198
|
+
verification, and retention are the caller's responsibility (e.g. verify the extracted directory
|
|
199
|
+
opens, or carry a checksum alongside the archive).
|
|
200
|
+
- **Compaction cleanup is deferred for the duration of the stream.** File deletions are disabled
|
|
201
|
+
while the snapshot is being read so a compaction cannot delete a file mid-stream. A _very_ slow
|
|
202
|
+
consumer therefore lets obsolete SST files accumulate (transient disk pressure) until the stream
|
|
203
|
+
finishes. This is the cost of streaming with no scratch copy; a fast consumer is unaffected.
|
|
204
|
+
- **The database must stay open for the whole stream.** Closing or destroying the database while a
|
|
205
|
+
stream is in flight aborts it: the backup promise rejects, and `destroy()` throws rather than
|
|
206
|
+
tearing the database down underneath the copy.
|
|
207
|
+
- **A consumer error aborts the backup.** If `stream.write()` rejects (or the stream is aborted), the
|
|
208
|
+
backup promise rejects and the native producer stops.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Checkpoints (related)
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
await db.createCheckpoint('/path/to/checkpoint');
|
|
216
|
+
const branch = new RocksDatabase('/path/to/checkpoint');
|
|
217
|
+
branch.open(); // an independent, writable database
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A checkpoint is not a backup in the `BackupEngine` sense — it is a point-in-time, fully independent
|
|
221
|
+
**sibling database**. It is included here because it is the fastest way to make a local copy and
|
|
222
|
+
shares the same "copy the whole database" shape.
|
|
223
|
+
|
|
224
|
+
### How it works
|
|
225
|
+
|
|
226
|
+
RocksDB **hard-links** the SST and blob files into the target directory when that directory is on the
|
|
227
|
+
**same filesystem** as the database, and **copies** them otherwise. Other files (the manifest, etc.)
|
|
228
|
+
are always copied. On the same filesystem this makes a checkpoint near-instant and near-zero extra
|
|
229
|
+
space — the hard links share the underlying data blocks until compaction diverges them.
|
|
230
|
+
|
|
231
|
+
### Caveats
|
|
232
|
+
|
|
233
|
+
- **Hard linking requires the same filesystem and filesystem/OS support.** When the target is on a
|
|
234
|
+
different filesystem or volume, RocksDB falls back to a full **copy** (slower, full space). The
|
|
235
|
+
same fallback applies on platforms/filesystems that don't support hard links — notably **Windows**,
|
|
236
|
+
where checkpoints generally **copy** rather than link. Do not assume a checkpoint is cheap unless
|
|
237
|
+
you know it lands on the same Unix filesystem as the database.
|
|
238
|
+
- **The target path must not already exist.** RocksDB creates the checkpoint directory; an existing
|
|
239
|
+
path is rejected with `Create checkpoint failed: target path exists`. Parent directories are
|
|
240
|
+
created for you, and cleanup of the directory is the caller's responsibility.
|
|
241
|
+
- **It writes to local disk.** Unlike a stream backup, a checkpoint is materialized on disk (as links
|
|
242
|
+
or copies); it is not a way to avoid touching the filesystem.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Choosing a mechanism
|
|
247
|
+
|
|
248
|
+
- **Scheduled, retained, local backups** → directory backups. You get incremental storage, a
|
|
249
|
+
listing/verify/purge API, and a restore command.
|
|
250
|
+
- **Send a backup off-host without scratch disk** (upload to object storage, pipe over a socket) →
|
|
251
|
+
stream backups. Just remember they are full snapshots and cannot be resumed.
|
|
252
|
+
- **A fast local copy to branch from or snapshot before a risky operation** → checkpoints, ideally on
|
|
253
|
+
the same filesystem.
|
package/docs/stats.md
CHANGED
|
@@ -34,6 +34,8 @@ By default, `db.getStats()` returns properties only.
|
|
|
34
34
|
|
|
35
35
|
| Name | Description | Type |
|
|
36
36
|
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
|
|
37
|
+
| `commitPipeline.commitQueueDepth` | Number of async commits queued on the database's commit lane but not yet started (in the default single-lane mode this covers the whole commit: log write + RocksDB commit). | gauge |
|
|
38
|
+
| `commitPipeline.logQueueDepth` | Number of async commits queued on the database's transaction-log lane but not yet started (always `0` in the default single-lane mode; see `ROCKSDB_JS_COMMIT_THREAD=2`). | gauge |
|
|
37
39
|
| `rocksdb.block-cache-capacity` | Capacity in bytes of the block cache. | gauge |
|
|
38
40
|
| `rocksdb.block-cache-pinned-usage` | Bytes occupied by pinned block cache entries. | gauge |
|
|
39
41
|
| `rocksdb.block-cache-usage` | Bytes currently used by block cache entries. | gauge |
|
package/docs/transaction-log.md
CHANGED
|
@@ -206,6 +206,16 @@ await db.transaction((txn) => {
|
|
|
206
206
|
- Commit with unknown transaction ID throws an error
|
|
207
207
|
- Invalid data is detected during read operations
|
|
208
208
|
|
|
209
|
+
### Validation
|
|
210
|
+
|
|
211
|
+
A store directory can be validated offline with `validateTransactionLogStore(path, options?)`: it
|
|
212
|
+
checks every log file's header (token, version) and entry framing using the same scan as open-time
|
|
213
|
+
crash recovery, plus file-name/sequence continuity and the `txn.state` side file. A torn tail,
|
|
214
|
+
sequence gap, or implausible `txn.state` position is a warning by default (a torn tail is
|
|
215
|
+
recoverable — open-time recovery truncates it losslessly) and an error with `{ strict: true }`,
|
|
216
|
+
which `backups.verify()` uses for backup snapshots since an intact snapshot can have none of them.
|
|
217
|
+
The CLI exposes the same check as `verify-logs [name]`.
|
|
218
|
+
|
|
209
219
|
## Reading The Transaction Log
|
|
210
220
|
|
|
211
221
|
Log entries are not guaranteed to be in order, but are guaranteed to have a monotonic timestamp.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harperfast/rocksdb-js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "RocksDB binding for Node.js",
|
|
5
5
|
"homepage": "https://github.com/HarperFast/rocksdb-js",
|
|
6
6
|
"bugs": [
|
|
@@ -38,40 +38,40 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@rollup/plugin-replace": "6.0.3",
|
|
41
|
-
"@types/node": "26.
|
|
42
|
-
"@vitest/coverage-v8": "4.1.
|
|
41
|
+
"@types/node": "26.1.1",
|
|
42
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
43
43
|
"cross-env": "10.1.0",
|
|
44
44
|
"dotenv": "17.4.2",
|
|
45
|
-
"lefthook": "2.1.
|
|
46
|
-
"lmdb": "3.5.
|
|
47
|
-
"node-gyp": "13.0.
|
|
48
|
-
"oxfmt": "0.
|
|
49
|
-
"oxlint": "1.
|
|
45
|
+
"lefthook": "2.1.10",
|
|
46
|
+
"lmdb": "3.5.6",
|
|
47
|
+
"node-gyp": "13.0.1",
|
|
48
|
+
"oxfmt": "0.59.0",
|
|
49
|
+
"oxlint": "1.74.0",
|
|
50
50
|
"prebuildify": "6.0.1",
|
|
51
|
-
"semver": "7.8.
|
|
52
|
-
"tar": "7.5.
|
|
53
|
-
"tsdown": "0.22.
|
|
51
|
+
"semver": "7.8.5",
|
|
52
|
+
"tar": "7.5.20",
|
|
53
|
+
"tsdown": "0.22.9",
|
|
54
54
|
"tslib": "2.8.1",
|
|
55
|
-
"tsx": "4.
|
|
56
|
-
"typescript": "
|
|
57
|
-
"vitest": "4.1.
|
|
55
|
+
"tsx": "4.23.1",
|
|
56
|
+
"typescript": "7.0.2",
|
|
57
|
+
"vitest": "4.1.10"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
|
-
"@harperfast/rocksdb-js-darwin-arm64": "2.
|
|
61
|
-
"@harperfast/rocksdb-js-darwin-x64": "2.
|
|
62
|
-
"@harperfast/rocksdb-js-linux-arm64-glibc": "2.
|
|
63
|
-
"@harperfast/rocksdb-js-linux-arm64-musl": "2.
|
|
64
|
-
"@harperfast/rocksdb-js-linux-x64-glibc": "2.
|
|
65
|
-
"@harperfast/rocksdb-js-linux-x64-musl": "2.
|
|
66
|
-
"@harperfast/rocksdb-js-win32-arm64": "2.
|
|
67
|
-
"@harperfast/rocksdb-js-win32-x64": "2.
|
|
60
|
+
"@harperfast/rocksdb-js-darwin-arm64": "2.5.0",
|
|
61
|
+
"@harperfast/rocksdb-js-darwin-x64": "2.5.0",
|
|
62
|
+
"@harperfast/rocksdb-js-linux-arm64-glibc": "2.5.0",
|
|
63
|
+
"@harperfast/rocksdb-js-linux-arm64-musl": "2.5.0",
|
|
64
|
+
"@harperfast/rocksdb-js-linux-x64-glibc": "2.5.0",
|
|
65
|
+
"@harperfast/rocksdb-js-linux-x64-musl": "2.5.0",
|
|
66
|
+
"@harperfast/rocksdb-js-win32-arm64": "2.5.0",
|
|
67
|
+
"@harperfast/rocksdb-js-win32-x64": "2.5.0"
|
|
68
68
|
},
|
|
69
69
|
"engines": {
|
|
70
70
|
"node": "^22.18.0 || >=24.0.0"
|
|
71
71
|
},
|
|
72
72
|
"gypfile": true,
|
|
73
73
|
"rocksdb": {
|
|
74
|
-
"version": "11.1.
|
|
74
|
+
"version": "11.1.2"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"bench": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs bench --passWithNoTests --outputJson benchmark-results.json",
|