@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.
@@ -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 |
@@ -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.0",
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.0.0",
42
- "@vitest/coverage-v8": "4.1.9",
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.9",
46
- "lmdb": "3.5.5",
47
- "node-gyp": "13.0.0",
48
- "oxfmt": "0.55.0",
49
- "oxlint": "1.70.0",
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.4",
52
- "tar": "7.5.16",
53
- "tsdown": "0.22.3",
51
+ "semver": "7.8.5",
52
+ "tar": "7.5.20",
53
+ "tsdown": "0.22.9",
54
54
  "tslib": "2.8.1",
55
- "tsx": "4.22.4",
56
- "typescript": "6.0.3",
57
- "vitest": "4.1.9"
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.3.0",
61
- "@harperfast/rocksdb-js-darwin-x64": "2.3.0",
62
- "@harperfast/rocksdb-js-linux-arm64-glibc": "2.3.0",
63
- "@harperfast/rocksdb-js-linux-arm64-musl": "2.3.0",
64
- "@harperfast/rocksdb-js-linux-x64-glibc": "2.3.0",
65
- "@harperfast/rocksdb-js-linux-x64-musl": "2.3.0",
66
- "@harperfast/rocksdb-js-win32-arm64": "2.3.0",
67
- "@harperfast/rocksdb-js-win32-x64": "2.3.0"
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.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",