@harperfast/rocksdb-js 2.2.0 → 2.4.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,238 @@
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
+ ### Restoring
77
+
78
+ ```typescript
79
+ await backups.restore('/path/to/backups', '/path/to/restored-db', {
80
+ backupId: 2, // default: the latest non-corrupt backup
81
+ walDir: '/restored-db', // default: the database directory
82
+ keepLogFiles: false,
83
+ mode: 'purgeAllFiles', // default — see below
84
+ });
85
+
86
+ const restored = new RocksDatabase('/path/to/restored-db');
87
+ restored.open();
88
+ ```
89
+
90
+ Restore modes:
91
+
92
+ - **`purgeAllFiles`** (default): purge the destination directory and restore everything. Destructive
93
+ but always correct.
94
+ - **`keepLatestDbSessionIdFiles`**: efficiently restore over a healthy database, reusing existing
95
+ files that match the backup.
96
+ - **`verifyChecksum`**: reuse existing destination files whose checksums match the backup, replacing
97
+ only mismatched/corrupt files.
98
+
99
+ ### Caveats
100
+
101
+ - **The data is duplicated onto local disk.** A backup is a real, separate copy in the backup
102
+ directory; it is not a hard link to the live database. Budget disk for it.
103
+ - **Incremental sharing is per-directory.** Files are only deduplicated against other backups in the
104
+ same backup directory.
105
+ - **Restore is destructive by default** (`purgeAllFiles`). Never restore into a directory you cannot
106
+ afford to have purged, and never restore into the backup directory itself (this is rejected, but
107
+ do not rely on it).
108
+ - **It needs a writable local directory.** To get a backup off the host you still have to copy the
109
+ directory afterward.
110
+
111
+ ---
112
+
113
+ ## Stream backups
114
+
115
+ ```typescript
116
+ import { createWriteStream } from 'node:fs';
117
+ import { Writable } from 'node:stream';
118
+
119
+ // Any WHATWG WritableStream<Uint8Array> works. A Node stream adapts via Writable.toWeb():
120
+ const out = Writable.toWeb(createWriteStream('/path/to/backup.tar'));
121
+ await db.backup(out);
122
+ ```
123
+
124
+ ### How it works
125
+
126
+ Stream backups produce a **tar archive** of the database's live files and write it to a
127
+ `WritableStream` — with **no intermediate copy on disk**. The native layer enumerates the consistent
128
+ set of live files (via RocksDB's `GetLiveFilesStorageInfo`), reads each one, and the bytes are framed
129
+ into a [USTAR](<https://en.wikipedia.org/wiki/Tar_(computing)#UStar_format>) archive and written to the
130
+ stream. The result is byte-for-byte a snapshot of the database directory, so it unpacks with any tar
131
+ tool into a directory that opens directly as a RocksDB database.
132
+
133
+ **Backpressure is honored end to end.** Exactly one chunk is in flight at a time, and the native
134
+ producer waits for each `stream.write()` to drain before reading the next chunk. A slow consumer
135
+ (e.g. a network or object-store upload) therefore _paces_ the backup rather than buffering it in
136
+ memory — peak memory stays flat regardless of database size.
137
+
138
+ ### Options
139
+
140
+ ```typescript
141
+ await db.backup(stream, {
142
+ flushBeforeBackup: true, // default: true iff opened with disableWAL
143
+ gzip: false, // default: false — set true for a .tar.gz stream
144
+ });
145
+ ```
146
+
147
+ Set `gzip: true` to compress the archive on the fly (via the runtime's `CompressionStream`),
148
+ producing a `.tar.gz` stream. Compression sits downstream of the tar encoder, so backpressure is
149
+ preserved. Note that RocksDB SST files are usually already block-compressed, so the extra savings
150
+ depend on your data and column-family compression settings.
151
+
152
+ ### Restoring
153
+
154
+ There is no dedicated restore call — the archive _is_ the database. Unpack it with any tar tool and
155
+ open the directory:
156
+
157
+ ```sh
158
+ mkdir /path/to/restored-db
159
+ tar -xf backup.tar -C /path/to/restored-db
160
+ # For a gzipped stream (gzip: true), use -xzf backup.tar.gz instead.
161
+ ```
162
+
163
+ ```typescript
164
+ const restored = new RocksDatabase('/path/to/restored-db');
165
+ restored.open();
166
+ ```
167
+
168
+ The archive contains the standard RocksDB files for the snapshot (`CURRENT`, a `MANIFEST-*`, the
169
+ `OPTIONS-*` file, the SST/blob files, and — unless flushed away — the WAL).
170
+
171
+ ### Caveats
172
+
173
+ - **Not resumable.** The stream is a single, ordered pass over a live snapshot; there is no offset or
174
+ manifest to resume against, and the source snapshot is not retained after the call. If the
175
+ connection drops or the consumer errors partway through, the backup is incomplete and must be
176
+ **restarted from the beginning**. Treat a stream backup as valid only once it has been written and
177
+ closed in full. (For interrupted-transfer detection, the archive ends with the standard tar
178
+ end-of-archive marker — a truncated stream is missing it.)
179
+ - **Always a full snapshot.** Unlike directory backups, there is no file sharing or incremental
180
+ mode; every stream copies the entire database.
181
+ - **No management API.** `backups.list` / `verify` / `delete` / `purge` and `backups.restore`
182
+ operate on the `BackupEngine` directory format and do **not** understand tar streams. Listing,
183
+ verification, and retention are the caller's responsibility (e.g. verify the extracted directory
184
+ opens, or carry a checksum alongside the archive).
185
+ - **Compaction cleanup is deferred for the duration of the stream.** File deletions are disabled
186
+ while the snapshot is being read so a compaction cannot delete a file mid-stream. A _very_ slow
187
+ consumer therefore lets obsolete SST files accumulate (transient disk pressure) until the stream
188
+ finishes. This is the cost of streaming with no scratch copy; a fast consumer is unaffected.
189
+ - **The database must stay open for the whole stream.** Closing or destroying the database while a
190
+ stream is in flight aborts it: the backup promise rejects, and `destroy()` throws rather than
191
+ tearing the database down underneath the copy.
192
+ - **A consumer error aborts the backup.** If `stream.write()` rejects (or the stream is aborted), the
193
+ backup promise rejects and the native producer stops.
194
+
195
+ ---
196
+
197
+ ## Checkpoints (related)
198
+
199
+ ```typescript
200
+ await db.createCheckpoint('/path/to/checkpoint');
201
+ const branch = new RocksDatabase('/path/to/checkpoint');
202
+ branch.open(); // an independent, writable database
203
+ ```
204
+
205
+ A checkpoint is not a backup in the `BackupEngine` sense — it is a point-in-time, fully independent
206
+ **sibling database**. It is included here because it is the fastest way to make a local copy and
207
+ shares the same "copy the whole database" shape.
208
+
209
+ ### How it works
210
+
211
+ RocksDB **hard-links** the SST and blob files into the target directory when that directory is on the
212
+ **same filesystem** as the database, and **copies** them otherwise. Other files (the manifest, etc.)
213
+ are always copied. On the same filesystem this makes a checkpoint near-instant and near-zero extra
214
+ space — the hard links share the underlying data blocks until compaction diverges them.
215
+
216
+ ### Caveats
217
+
218
+ - **Hard linking requires the same filesystem and filesystem/OS support.** When the target is on a
219
+ different filesystem or volume, RocksDB falls back to a full **copy** (slower, full space). The
220
+ same fallback applies on platforms/filesystems that don't support hard links — notably **Windows**,
221
+ where checkpoints generally **copy** rather than link. Do not assume a checkpoint is cheap unless
222
+ you know it lands on the same Unix filesystem as the database.
223
+ - **The target path must not already exist.** RocksDB creates the checkpoint directory; an existing
224
+ path is rejected with `Create checkpoint failed: target path exists`. Parent directories are
225
+ created for you, and cleanup of the directory is the caller's responsibility.
226
+ - **It writes to local disk.** Unlike a stream backup, a checkpoint is materialized on disk (as links
227
+ or copies); it is not a way to avoid touching the filesystem.
228
+
229
+ ---
230
+
231
+ ## Choosing a mechanism
232
+
233
+ - **Scheduled, retained, local backups** → directory backups. You get incremental storage, a
234
+ listing/verify/purge API, and a restore command.
235
+ - **Send a backup off-host without scratch disk** (upload to object storage, pipe over a socket) →
236
+ stream backups. Just remember they are full snapshots and cannot be resumed.
237
+ - **A fast local copy to branch from or snapshot before a risky operation** → checkpoints, ideally on
238
+ the same filesystem.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harperfast/rocksdb-js",
3
- "version": "2.2.0",
3
+ "version": "2.4.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": "25.9.3",
42
- "@vitest/coverage-v8": "4.1.9",
41
+ "@types/node": "26.1.0",
42
+ "@vitest/coverage-v8": "4.1.10",
43
43
  "cross-env": "10.1.0",
44
44
  "dotenv": "17.4.2",
45
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",
46
+ "lmdb": "3.5.6",
47
+ "node-gyp": "13.0.1",
48
+ "oxfmt": "0.58.0",
49
+ "oxlint": "1.73.0",
50
50
  "prebuildify": "6.0.1",
51
- "semver": "7.8.4",
52
- "tar": "7.5.16",
51
+ "semver": "7.8.5",
52
+ "tar": "7.5.19",
53
53
  "tsdown": "0.22.3",
54
54
  "tslib": "2.8.1",
55
- "tsx": "4.22.4",
55
+ "tsx": "4.23.0",
56
56
  "typescript": "6.0.3",
57
- "vitest": "4.1.9"
57
+ "vitest": "4.1.10"
58
58
  },
59
59
  "optionalDependencies": {
60
- "@harperfast/rocksdb-js-darwin-arm64": "2.2.0",
61
- "@harperfast/rocksdb-js-darwin-x64": "2.2.0",
62
- "@harperfast/rocksdb-js-linux-arm64-glibc": "2.2.0",
63
- "@harperfast/rocksdb-js-linux-arm64-musl": "2.2.0",
64
- "@harperfast/rocksdb-js-linux-x64-glibc": "2.2.0",
65
- "@harperfast/rocksdb-js-linux-x64-musl": "2.2.0",
66
- "@harperfast/rocksdb-js-win32-arm64": "2.2.0",
67
- "@harperfast/rocksdb-js-win32-x64": "2.2.0"
60
+ "@harperfast/rocksdb-js-darwin-arm64": "2.4.0",
61
+ "@harperfast/rocksdb-js-darwin-x64": "2.4.0",
62
+ "@harperfast/rocksdb-js-linux-arm64-glibc": "2.4.0",
63
+ "@harperfast/rocksdb-js-linux-arm64-musl": "2.4.0",
64
+ "@harperfast/rocksdb-js-linux-x64-glibc": "2.4.0",
65
+ "@harperfast/rocksdb-js-linux-x64-musl": "2.4.0",
66
+ "@harperfast/rocksdb-js-win32-arm64": "2.4.0",
67
+ "@harperfast/rocksdb-js-win32-x64": "2.4.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",