@harperfast/rocksdb-js 2.11.0 → 2.13.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
@@ -171,11 +171,12 @@ A separate native reproduction confirmed [#860](https://github.com/HarperFast/ro
171
171
  `tryClose()` releases its written-position lock before latching admission closed.
172
172
  A transaction can bind, finish `writeBatch()` and reduce the pending count back to zero
173
173
  between those checks. A temporary scheduling hook before phase 3 reproduced
174
- `pending=0`, one real uncommitted position, and `tryClose() == true`. This is a
174
+ `pending=0`, one real uncommitted position, and `tryClose() == true`. This was a
175
175
  pre-existing close-protocol defect, not fixed by the completion-lock change. Its
176
176
  production trigger is a concurrent destructive log purge; no customer incident or
177
- JavaScript-level data loss was reproduced. Keep the binding lock until a separately
178
- reviewed admission/drain fix resolves that handoff.
177
+ JavaScript-level data loss was reproduced. It is now fixed without changing the binding
178
+ lock: every bind advances a generation that `tryClose()` rechecks before latching (see
179
+ "Conditional close" in `src/binding/transaction_log/DESIGN.md`).
179
180
 
180
181
  The investigation also found [#859](https://github.com/HarperFast/rocksdb-js/issues/859):
181
182
  the DB-instance stress test awaits an empty promise array in its worker-close phase.
package/docs/stats.md CHANGED
@@ -189,6 +189,10 @@ which includes all of the stats above plus the following:
189
189
  | `rocksdb.blobdb.gc.num.new.files` | Number of new blob files generated by garbage collection (legacy stacked BlobDB). | ticker |
190
190
  | `rocksdb.blobdb.get.micros` | Distribution of `Get()` latencies (microseconds) (legacy stacked BlobDB). | histogram |
191
191
  | `rocksdb.blobdb.key.size` | Distribution of key sizes for BlobDB operations. | histogram |
192
+ | `rocksdb.blobdb.lazy.partial.bytes.saved` | Bytes not read from blob files thanks to partial (byte-range) lazy reads: the value size minus the bytes actually read. | ticker |
193
+ | `rocksdb.blobdb.lazy.partial.read.count` | Number of lazy blob reads that read only a byte range of an uncompressed blob value. | ticker |
194
+ | `rocksdb.blobdb.lazy.read.bytes` | Bytes read from storage while resolving lazy wide-column results (`GetEntityLazy`/`MultiGetEntityLazy`). | ticker |
195
+ | `rocksdb.blobdb.lazy.read.count` | Number of storage reads issued while resolving lazy wide-column results (`GetEntityLazy`/`MultiGetEntityLazy`). | ticker |
192
196
  | `rocksdb.blobdb.multiget.micros` | Distribution of `MultiGet()` latencies (microseconds) (legacy stacked BlobDB). | histogram |
193
197
  | `rocksdb.blobdb.next.micros` | Distribution of iterator `Next()` latencies (microseconds) (legacy stacked BlobDB). | histogram |
194
198
  | `rocksdb.blobdb.num.get` | Number of `Get()` calls (legacy stacked BlobDB). | ticker |
@@ -493,7 +497,9 @@ const stats: TransactionLogStats = log.getStats();
493
497
 
494
498
  - `name: string` The name of the transaction log store.
495
499
  - `path: string` Filesystem path to the log store's directory.
496
- - `fileCount: number` Number of sequence log files on disk for this log.
500
+ - `fileCount: number` Number of sequence log files on disk for this log. `0` does not mean the log
501
+ was never written: retention can purge every file of an idle log. A log that has never been written
502
+ reports `currentSequenceNumber` 1; one emptied by retention reports a higher one.
497
503
  - `currentSequenceNumber: number` Sequence number of the active write file.
498
504
  - `oldestSequenceNumber: number` Sequence number of the oldest file still on disk.
499
505
  - `totalSizeBytes: number` Total on-disk size of all of this log's files.
@@ -522,9 +528,10 @@ const stats: TransactionLogStats = log.getStats();
522
528
  - `offset: number` The byte offset within that file.
523
529
  - `purge: object`
524
530
  - `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.
531
+ - `purgeableFiles: number` Number of files past the retention threshold that lie entirely at or
532
+ before the flushed position, including the current file, so the next purge can delete them.
533
+ - `retentionPins: number` Backups currently copying this store's files. Ordinary purges of the
534
+ store are skipped while it is nonzero, so a value that stays nonzero points at a stalled backup.
528
535
  - `retainedUnflushedFiles: number` Number of files past the retention threshold but retained
529
536
  because they are unflushed.
530
537
  - `lastPurgeMs: number` Timestamp of the last purge scan.
@@ -87,7 +87,7 @@ separate properties. No promise is made that runtime timers use the same suspend
87
87
  ## Verification
88
88
 
89
89
  The end-to-end route is the real N-API export through `src/index.ts`, parent/worker bracketing on
90
- Node, Bun and Deno, staggered/restarted and concurrent workers (including `workerFirst >= parentBefore + stagger - tolerance`), elapsed
90
+ Node and Bun (Deno: local `pnpm test:deno` only, #947), staggered/restarted and concurrent workers (including `workerFirst >= parentBefore + stagger - tolerance`), elapsed
91
91
  progression, and built ESM/CJS
92
92
  exports. Temporarily substituting a worker-local clock must make those brackets fail, proving
93
93
  they reject the motivating defect.
@@ -58,7 +58,7 @@ The token is used to validate that the file is indeed a transaction log.
58
58
 
59
59
  #### `version`
60
60
 
61
- The transaction log file format version. Currently, version `1` is the latest.
61
+ The transaction log file format version. Currently, version `1` is the latest. Opening a database whose newest transaction-log segments include one with a higher version (written by a newer release) fails with an error naming the segment, rather than skipping it, and this release never appends after such a segment. A malformed or unreadable segment is still skipped.
62
62
 
63
63
  #### `timestamp`
64
64
 
@@ -84,9 +84,10 @@ The size of the data entry directly following the transaction header.
84
84
 
85
85
  Transaction entry related flags.
86
86
 
87
- | Flag | Value | Description |
88
- | --------------------------------- | ------ | ------------------------------------------ |
89
- | `TRANSACTION_LOG_ENTRY_LAST_FLAG` | `0x01` | Indicates the last entry for a transaction |
87
+ | Flag | Value | Description |
88
+ | ------------------------------------ | ------ | --------------------------------------------------------------------------------------------------- |
89
+ | `TRANSACTION_LOG_ENTRY_LAST_FLAG` | `0x01` | Indicates the last entry for a transaction |
90
+ | `TRANSACTION_LOG_ENTRY_ABORTED_FLAG` | `0x02` | On a transaction's first entry: the transaction did not commit, and readers skip all of its entries |
90
91
 
91
92
  ## Encoding Details
92
93
 
@@ -198,9 +199,23 @@ await db.transaction((txn) => {
198
199
  - Log files are automatically rotated when either the index or data file reaches their configured
199
200
  maximum sizes
200
201
  - 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.
202
+ - Old log files can be automatically purged based on retention policy. A file is purgeable once it
203
+ is past the cutoff and lies entirely at or before the flushed position recorded in `txn.state`.
204
+ That includes the file `txn.state` names and the file currently being appended to, so an idle
205
+ store whose entries are all flushed ends up with no log files at all. The next write starts a new,
206
+ higher sequence; `txn.state` stays behind as the record of the highest sequence used, and a
207
+ restart appends past it. Before deleting a store's last file, retention syncs `txn.state` and its
208
+ directory; on a filesystem that rejects a directory fsync it keeps that file and warns once.
209
+ - `txn.state` is rewritten in place (8 bytes at offset 0, not synced on each flush). Retention relies
210
+ on storage writing that range atomically: a power loss that tears it into a mix of old and new
211
+ bytes can name a later position than was flushed, and retention would then treat unflushed
212
+ entries below it as flushed.
213
+ - A reader that starts from the last flushed position, or one already inside a purged file,
214
+ continues at the lowest retained sequence (or at the next write when nothing is retained).
215
+ - Downgrading to a release without this rule after a store's last file was purged is unsafe: older
216
+ releases restart an empty store at sequence 1, below the flushed position, so a replay from
217
+ `txn.state` skips the new writes and retention treats them as flushed. Remove the store's
218
+ `txn.state` before downgrading.
204
219
 
205
220
  ### Error Handling
206
221
 
@@ -251,6 +266,11 @@ handle is returned, so no transaction can be constructed below it. Keys are not
251
266
  across segments, and a segment header records the store's latest timestamp only as of that
252
267
  segment's creation, so there is no shortcut: every segment is read.
253
268
 
269
+ Retention can purge every segment of the named log. The walk then finds no keys and leaves the clock
270
+ alone, so a backward step after restart can reissue a key that was only in purged segments. With
271
+ age-based retention alone that needs a backward step at least as large as the retention period; a
272
+ `purgeLogs({ before })` cutoff closer to now shrinks it to the time since that cutoff.
273
+
254
274
  Name only a log this process originates. A log a replication receiver writes under an adopted origin
255
275
  timestamp is keyed by another node's clock, and seeding from it would ratchet this process's clock
256
276
  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.13.0",
4
4
  "description": "RocksDB binding for Node.js",
5
5
  "homepage": "https://github.com/HarperFast/rocksdb-js",
6
6
  "bugs": [
@@ -38,39 +38,39 @@
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",
46
- "lmdb": "3.5.7",
44
+ "dotenv": "18.0.9",
45
+ "lefthook": "2.2.2",
46
+ "lmdb": "3.5.9",
47
47
  "node-gyp": "13.1.0",
48
- "oxfmt": "0.71.0",
49
- "oxlint": "1.86.0",
48
+ "oxfmt": "0.75.0",
49
+ "oxlint": "1.90.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.3",
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.13.0",
60
+ "@harperfast/rocksdb-js-darwin-x64": "2.13.0",
61
+ "@harperfast/rocksdb-js-linux-arm64-glibc": "2.13.0",
62
+ "@harperfast/rocksdb-js-linux-arm64-musl": "2.13.0",
63
+ "@harperfast/rocksdb-js-linux-x64-glibc": "2.13.0",
64
+ "@harperfast/rocksdb-js-linux-x64-musl": "2.13.0",
65
+ "@harperfast/rocksdb-js-win32-arm64": "2.13.0",
66
+ "@harperfast/rocksdb-js-win32-x64": "2.13.0"
67
67
  },
68
68
  "engines": {
69
69
  "node": "^22.18.0 || >=24.0.0"
70
70
  },
71
71
  "gypfile": true,
72
72
  "rocksdb": {
73
- "version": "11.8.1"
73
+ "version": "11.12.0"
74
74
  },
75
75
  "scripts": {
76
76
  "bench": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs bench --passWithNoTests --outputJson benchmark-results.json",
@@ -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",