@harperfast/rocksdb-js 2.11.0 → 2.12.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/docs/backups.md CHANGED
@@ -80,6 +80,33 @@ verification). Pass `verifyTransactionLogs: false` to check only the RocksDB fil
80
80
  stores (e.g. the `transaction_logs` directory of a closed database) can be validated directly with
81
81
  `validateTransactionLogStore()`.
82
82
 
83
+ Pass `verifyTables: true` to check that **this build** can read the backup, not only that its files
84
+ are intact. Every table file is opened in turn (one at a time), so verification rejects, naming the
85
+ file, when a table uses a `format_version` or a compression algorithm this build does not support.
86
+ That is the case after a downgrade, or when restoring a backup taken by a build that linked a
87
+ compressor this one lacks. Blob files are not checked.
88
+
89
+ Check before a destructive restore. The default `purgeAllFiles` mode deletes the destination before
90
+ copying, so an unreadable backup would otherwise destroy the database it was meant to replace:
91
+
92
+ ```typescript
93
+ import { backups, fileLockRelease, tryFileLock } from '@harperfast/rocksdb-js';
94
+ import { join } from 'node:path';
95
+
96
+ // A shared lock keeps a concurrent delete/purge from changing the backup between the two calls.
97
+ // Shared locks coexist, so restore can still take its own.
98
+ const token = tryFileLock(join(backupDir, '.backup.lock'), true);
99
+ if (!token) {
100
+ throw new Error('A backup, delete, or purge is running on this directory; retry later');
101
+ }
102
+ try {
103
+ await backups.verify(backupDir, backupId, { verifyWithChecksum: true, verifyTables: true });
104
+ await backups.restore(backupDir, dbPath, { backupId });
105
+ } finally {
106
+ fileLockRelease(token);
107
+ }
108
+ ```
109
+
83
110
  ### Restoring
84
111
 
85
112
  ```typescript
package/docs/stats.md CHANGED
@@ -493,7 +493,9 @@ const stats: TransactionLogStats = log.getStats();
493
493
 
494
494
  - `name: string` The name of the transaction log store.
495
495
  - `path: string` Filesystem path to the log store's directory.
496
- - `fileCount: number` Number of sequence log files on disk for this log.
496
+ - `fileCount: number` Number of sequence log files on disk for this log. `0` does not mean the log
497
+ was never written: retention can purge every file of an idle log. A log that has never been written
498
+ reports `currentSequenceNumber` 1; one emptied by retention reports a higher one.
497
499
  - `currentSequenceNumber: number` Sequence number of the active write file.
498
500
  - `oldestSequenceNumber: number` Sequence number of the oldest file still on disk.
499
501
  - `totalSizeBytes: number` Total on-disk size of all of this log's files.
@@ -522,9 +524,10 @@ const stats: TransactionLogStats = log.getStats();
522
524
  - `offset: number` The byte offset within that file.
523
525
  - `purge: object`
524
526
  - `oldestFileAgeMs: number` Age in milliseconds of the oldest file on disk.
525
- - `purgeableFiles: number` Number of files below the retention floor (the `txn.state` sequence,
526
- or the highest sequence when there is no persisted flush position) that are eligible for purge
527
- under the retention policy.
527
+ - `purgeableFiles: number` Number of files past the retention threshold that lie entirely at or
528
+ before the flushed position, including the current file, so the next purge can delete them.
529
+ - `retentionPins: number` Backups currently copying this store's files. Ordinary purges of the
530
+ store are skipped while it is nonzero, so a value that stays nonzero points at a stalled backup.
528
531
  - `retainedUnflushedFiles: number` Number of files past the retention threshold but retained
529
532
  because they are unflushed.
530
533
  - `lastPurgeMs: number` Timestamp of the last purge scan.
@@ -198,9 +198,23 @@ await db.transaction((txn) => {
198
198
  - Log files are automatically rotated when either the index or data file reaches their configured
199
199
  maximum sizes
200
200
  - Rotation happens on the next write after the size limit is exceeded
201
- - Old log files can be automatically purged based on retention policy. The sequence file named by
202
- `txn.state` and every newer file form the live store's retention floor, so an idle store can keep
203
- one bounded file past the cutoff until a later write rotates and flushes it.
201
+ - Old log files can be automatically purged based on retention policy. A file is purgeable once it
202
+ is past the cutoff and lies entirely at or before the flushed position recorded in `txn.state`.
203
+ That includes the file `txn.state` names and the file currently being appended to, so an idle
204
+ store whose entries are all flushed ends up with no log files at all. The next write starts a new,
205
+ higher sequence; `txn.state` stays behind as the record of the highest sequence used, and a
206
+ restart appends past it. Before deleting a store's last file, retention syncs `txn.state` and its
207
+ directory; on a filesystem that rejects a directory fsync it keeps that file and warns once.
208
+ - `txn.state` is rewritten in place (8 bytes at offset 0, not synced on each flush). Retention relies
209
+ on storage writing that range atomically: a power loss that tears it into a mix of old and new
210
+ bytes can name a later position than was flushed, and retention would then treat unflushed
211
+ entries below it as flushed.
212
+ - A reader that starts from the last flushed position, or one already inside a purged file,
213
+ continues at the lowest retained sequence (or at the next write when nothing is retained).
214
+ - Downgrading to a release without this rule after a store's last file was purged is unsafe: older
215
+ releases restart an empty store at sequence 1, below the flushed position, so a replay from
216
+ `txn.state` skips the new writes and retention treats them as flushed. Remove the store's
217
+ `txn.state` before downgrading.
204
218
 
205
219
  ### Error Handling
206
220
 
@@ -251,6 +265,11 @@ handle is returned, so no transaction can be constructed below it. Keys are not
251
265
  across segments, and a segment header records the store's latest timestamp only as of that
252
266
  segment's creation, so there is no shortcut: every segment is read.
253
267
 
268
+ Retention can purge every segment of the named log. The walk then finds no keys and leaves the clock
269
+ alone, so a backward step after restart can reissue a key that was only in purged segments. With
270
+ age-based retention alone that needs a backward step at least as large as the retention period; a
271
+ `purgeLogs({ before })` cutoff closer to now shrinks it to the time since that cutoff.
272
+
254
273
  Name only a log this process originates. A log a replication receiver writes under an adopted origin
255
274
  timestamp is keyed by another node's clock, and seeding from it would ratchet this process's clock
256
275
  to the fastest of those nodes at each restart. Native code cannot tell the two kinds of log apart,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harperfast/rocksdb-js",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "RocksDB binding for Node.js",
5
5
  "homepage": "https://github.com/HarperFast/rocksdb-js",
6
6
  "bugs": [
@@ -38,32 +38,32 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@rollup/plugin-replace": "6.0.3",
41
- "@types/node": "26.6.4",
41
+ "@types/node": "26.6.5",
42
42
  "@vitest/coverage-v8": "4.1.11",
43
43
  "cross-env": "10.1.0",
44
- "dotenv": "18.0.5",
45
- "lefthook": "2.1.16",
44
+ "dotenv": "18.0.7",
45
+ "lefthook": "2.2.1",
46
46
  "lmdb": "3.5.7",
47
47
  "node-gyp": "13.1.0",
48
- "oxfmt": "0.71.0",
49
- "oxlint": "1.86.0",
48
+ "oxfmt": "0.72.0",
49
+ "oxlint": "1.87.0",
50
50
  "prebuildify": "6.0.1",
51
51
  "semver": "7.8.5",
52
52
  "tar": "7.5.22",
53
- "tsdown": "0.23.0",
53
+ "tsdown": "0.23.1",
54
54
  "tslib": "2.8.1",
55
55
  "typescript": "7.0.2",
56
56
  "vitest": "4.1.11"
57
57
  },
58
58
  "optionalDependencies": {
59
- "@harperfast/rocksdb-js-darwin-arm64": "2.11.0",
60
- "@harperfast/rocksdb-js-darwin-x64": "2.11.0",
61
- "@harperfast/rocksdb-js-linux-arm64-glibc": "2.11.0",
62
- "@harperfast/rocksdb-js-linux-arm64-musl": "2.11.0",
63
- "@harperfast/rocksdb-js-linux-x64-glibc": "2.11.0",
64
- "@harperfast/rocksdb-js-linux-x64-musl": "2.11.0",
65
- "@harperfast/rocksdb-js-win32-arm64": "2.11.0",
66
- "@harperfast/rocksdb-js-win32-x64": "2.11.0"
59
+ "@harperfast/rocksdb-js-darwin-arm64": "2.12.0",
60
+ "@harperfast/rocksdb-js-darwin-x64": "2.12.0",
61
+ "@harperfast/rocksdb-js-linux-arm64-glibc": "2.12.0",
62
+ "@harperfast/rocksdb-js-linux-arm64-musl": "2.12.0",
63
+ "@harperfast/rocksdb-js-linux-x64-glibc": "2.12.0",
64
+ "@harperfast/rocksdb-js-linux-x64-musl": "2.12.0",
65
+ "@harperfast/rocksdb-js-win32-arm64": "2.12.0",
66
+ "@harperfast/rocksdb-js-win32-x64": "2.12.0"
67
67
  },
68
68
  "engines": {
69
69
  "node": "^22.18.0 || >=24.0.0"
@@ -81,7 +81,8 @@
81
81
  "build:binding:debug": "node-gyp build --coverage --debug --verbose",
82
82
  "build:bundle": "node scripts/clean-dist.mjs && tsdown -c tsdown.config.ts",
83
83
  "build:bundle:minify": "cross-env MINIFY=1 pnpm build:bundle",
84
- "check": "pnpm type-check && pnpm lint && pnpm fmt:check",
84
+ "check": "pnpm check:code && pnpm fmt:check",
85
+ "check:code": "pnpm type-check && pnpm lint",
85
86
  "clean": "node-gyp clean",
86
87
  "coverage": "node scripts/coverage/main.ts",
87
88
  "coverage:native": "node scripts/native-test/coverage.mjs",