@harperfast/rocksdb-js 2.7.1 → 2.9.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.
@@ -198,7 +198,9 @@ 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
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
204
 
203
205
  ### Error Handling
204
206
 
@@ -218,10 +220,21 @@ The CLI exposes the same check as `verify-logs [name]`.
218
220
 
219
221
  ## Reading The Transaction Log
220
222
 
221
- Log entries are not guaranteed to be in order, but are guaranteed to have a monotonic timestamp.
222
- When reading the transaction log file, each transaction entry header must be read, then sorted and
223
- indexed. Using this index, queries can find all entries within a time range using a binary search
224
- and seek to get the associated entry data.
223
+ Log entries are not in timestamp order. A transaction claims its timestamp from the process-wide
224
+ monotonic clock when it is constructed but is appended when it commits, so under concurrency a later
225
+ entry can carry a smaller timestamp; an entry that adopted an origin timestamp with
226
+ `txn.setTimestamp()` carries that origin's clock instead. Timestamps are not unique: the monotonic
227
+ clock never issues the same value twice within a process, but a restart after the wall clock moved
228
+ backwards can reissue one, and `txn.setTimestamp()` can assign any value — including one already
229
+ in the log — to as many transactions as the caller likes. Deduplicating on the timestamp alone is
230
+ therefore never safe; a consumer that needs identity has to supply it (Harper pairs the timestamp
231
+ with the originating node).
232
+
233
+ When reading the transaction log file, each transaction entry header must be read and indexed. The
234
+ index records only the entries whose timestamp is greater than every earlier one in the file — a
235
+ running maxima — and a query seeks to the lower bound of that index, which is guaranteed to sit at
236
+ or before every entry in the requested range. Reading forward from there and filtering is what makes
237
+ range queries correct on an unordered file.
225
238
 
226
239
  ### Sequential Read
227
240
 
@@ -0,0 +1,123 @@
1
+ # Transaction timestamp integrity
2
+
3
+ ## Intent
4
+
5
+ Harper's source-record version and a RocksDB transaction's write identity are separate values.
6
+ Harper's `RecordEncoder` and `PrimaryRocksDatabase` already own the record value layout and decode
7
+ its metadata; rocksdb-js owns the transaction timestamp and uses it as the transaction-log batch
8
+ key. This change only makes overriding that timestamp safe for Harper's two legitimate adopters,
9
+ replication apply and crash replay.
10
+
11
+ ## Invariant
12
+
13
+ Within one transaction attempt, the timestamp cannot change after the transaction leaves `Pending`
14
+ or stages a database write or transaction-log entry. A retry may adopt again only when the failed
15
+ attempt did not durably write its log batch; once a batch is durable, its timestamp remains frozen
16
+ across retry resets. Encoding a transaction timestamp into record bytes remains the producer's
17
+ obligation because native code cannot observe when a producer reads and copies the timestamp.
18
+
19
+ ## Requirements traced to Harper
20
+
21
+ - HarperFast/harper#2065 intentionally allowed a source record's version to differ from the
22
+ transaction timestamp.
23
+ - HarperFast/harper#2409 fixed the immediate subscription regression by carrying both values in
24
+ Harper's audit representation.
25
+ - HarperFast/harper#2411 and HarperFast/harper-pro#790 require replay and replication respectively
26
+ to adopt the origin transaction timestamp through `setTimestamp()` before applying writes.
27
+ - HarperFast/harper#2412 owns the optional second version word in `RecordEncoder`, including its
28
+ encoding, decoding, compatibility gates, and call-site audit.
29
+
30
+ Checked against Harper's source rather than the issue text: `resources/RecordEncoder.ts` decodes the
31
+ value header itself (producing `localTime`, `version` and the metadata flags) and owns every flag bit
32
+ in that word, and `resources/PrimaryRocksDatabase.ts#getEntry()` overrides the base read entirely —
33
+ it calls `super.getSync()`/`super.get()` and reads the encoder's output. So nothing in Harper would
34
+ call a rocksdb-js decoder for the second word. Harper's three `setTimestamp()` call sites
35
+ (`resources/DatabaseTransaction.ts` at the read-transaction, save and retry-replay paths) each set it
36
+ on a freshly constructed transaction before any write, which is what this change makes enforceable.
37
+
38
+ One consequence for #2412 that is not in its call-site audit: `PrimaryRocksDatabase#getEntry()` passes
39
+ the cached entry's `version` as `expectedVersion` to the verification table, and the table keys on the
40
+ value's **first** word. Once `version` becomes the record version, that argument has to become
41
+ `localTime`, or cached reads stop hitting the fast path — and a record version that happened to equal
42
+ the key's current first word would report a stale copy as fresh.
43
+
44
+ ## Approaches considered
45
+
46
+ ### Different layer
47
+
48
+ Teach rocksdb-js to define and decode Harper's second version word. Rejected because Harper's
49
+ `PrimaryRocksDatabase#getEntry()` already overrides the base read path and receives the structured
50
+ entry produced by Harper's `RecordEncoder`; a second decoder in rocksdb-js would duplicate the
51
+ layout and cannot supply Harper's other metadata fields.
52
+
53
+ Reserving Harper's proposed `0x20000` bit in rocksdb-js without decoding it was also considered.
54
+ Rejected because rocksdb-js does not allocate the producer's metadata namespace: Harper already
55
+ defines the other optional-field flags in that word, while rocksdb-js knows `0x10000` only because
56
+ the native verification table gives that specific bit behavior. A bit with no native behavior or
57
+ public rocksdb-js contract belongs beside Harper's encoder and downgrade gate.
58
+
59
+ ### Deeper cause
60
+
61
+ Remove `setTimestamp()` and redesign replay and replication so rocksdb-js always issues a new local
62
+ timestamp. Rejected because Harper's record-to-log lookup and per-origin resume cursor require the
63
+ origin transaction timestamp to remain the transaction-log key on every receiver.
64
+
65
+ A construction-time timestamp option was considered as a safer replacement for the setter. It
66
+ would add another public API and migrate the replay and replication callers, but could not remove
67
+ the existing setter compatibly; therefore it would not eliminate the state it is meant to prevent.
68
+
69
+ ### Do less
70
+
71
+ Make no rocksdb-js change and rely on Harper always calling `setTimestamp()` before staging writes.
72
+ Rejected because the current public API permits a later call, after the producer has encoded the old
73
+ timestamp or the log batch has captured it, which can split one transaction across two identities.
74
+
75
+ For the separate clock-rollback concern, seeding the process clock from the largest durable key
76
+ across the database's transaction logs was considered and rejected on Harper's own log layout.
77
+ `resources/RocksTransactionLogStore.ts` opens one log per origin node — `useLog('local')` for locally
78
+ originated writes and `logById(nodeId)` for each peer — and a receiver deliberately keeps the origin's
79
+ clock as the key in the peer's log (HarperFast/harper-pro#790). A "largest key in the database" seed
80
+ therefore reads adopted peer keys as if they were locally issued: one peer running ahead ratchets this
81
+ node's clock on its next restart, and the node then originates those keys to its own peers. Reading
82
+ the headers only to warn has the same ambiguity — normal peer skew would be reported as a local clock
83
+ rollback. A correct floor has to be told which log is locally originated (a per-log opt-in, or an
84
+ explicit "raise the floor to this value" call Harper makes from its own `local` log), which is a
85
+ different API than the one the issue sketched and is not a prerequisite for the dual-version record
86
+ work.
87
+
88
+ ### Chosen
89
+
90
+ Keep `setTimestamp()` as the adoption API, reject invalid timestamp values, and reject changes once
91
+ the transaction is no longer pending or has staged a write or log entry. Preserve the existing
92
+ retry behavior: a reset with no durable batch clears staged writes and may adopt again, while
93
+ `committedPosition` freezes a timestamp whose batch already landed. Remove the second-word read API,
94
+ flag, and clock-floor-at-open implementation from this PR. The first two belong to Harper's record
95
+ codec; the clock floor addresses a pre-existing restart/clock-rollback concern and, as specified,
96
+ would seed from adopted origin keys as readily as from locally issued ones. The existing
97
+ `latestTimestamp` behavior used to populate new transaction-log segment headers remains unchanged.
98
+
99
+ Document what is true about log keys today, which the issue also asked for: entries are appended in
100
+ commit order rather than timestamp order, an adopted key carries another node's clock, keys are
101
+ unique only within a process, and `findPositionByTimestamp` is a running-maxima seek rather than a
102
+ sorted binary search.
103
+
104
+ ## Verification route
105
+
106
+ The existing transaction tests cover all transaction modes, invalid inputs, post-write and
107
+ post-dispatch rejection, log-entry staging, and a coordinated retry whose log batch is already
108
+ durable. README and AGENTS documentation will state the enforceable transaction-layer rule and the
109
+ producer-owned record boundary. The repository build, native tests, full Vitest suite, and checks
110
+ will run before push.
111
+
112
+ ## Alternatives rejected
113
+
114
+ Freezing the timestamp on the first `getTimestamp()` call would break existing documented and tested
115
+ read-then-override behavior. A getter is observational for callers that do not encode its result,
116
+ and making every read a state transition still could not prove that a producer's record bytes match
117
+ the transaction timestamp. The enforced boundary therefore remains the first native staging event;
118
+ producer ordering is documented explicitly.
119
+
120
+ Retaining the removed clock-floor branch's far-future header bound would change the append path using
121
+ a ten-year policy introduced only for that floor. This change instead restores `latestTimestamp` and
122
+ segment-header behavior exactly to `origin/main`; any provenance-aware clock rollback design belongs
123
+ in separate work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harperfast/rocksdb-js",
3
- "version": "2.7.1",
3
+ "version": "2.9.0",
4
4
  "description": "RocksDB binding for Node.js",
5
5
  "homepage": "https://github.com/HarperFast/rocksdb-js",
6
6
  "bugs": [
@@ -32,60 +32,59 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "@harperfast/extended-iterable": "1.0.3",
36
- "msgpackr": "2.0.5",
37
- "ordered-binary": "1.6.1"
35
+ "@harperfast/extended-iterable": "^1.0.3",
36
+ "msgpackr": "^2.0.6",
37
+ "ordered-binary": "^1.6.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@rollup/plugin-replace": "6.0.3",
41
- "@types/node": "26.1.2",
42
- "@vitest/coverage-v8": "4.1.10",
41
+ "@types/node": "26.4.1",
42
+ "@vitest/coverage-v8": "4.1.11",
43
43
  "cross-env": "10.1.0",
44
44
  "dotenv": "17.4.2",
45
- "lefthook": "2.1.10",
45
+ "lefthook": "2.1.12",
46
46
  "lmdb": "3.5.6",
47
- "node-gyp": "13.0.1",
48
- "oxfmt": "0.62.0",
49
- "oxlint": "1.77.0",
47
+ "node-gyp": "13.0.2",
48
+ "oxfmt": "0.66.0",
49
+ "oxlint": "1.81.0",
50
50
  "prebuildify": "6.0.1",
51
51
  "semver": "7.8.5",
52
52
  "tar": "7.5.22",
53
- "tsdown": "0.22.14",
53
+ "tsdown": "0.23.0",
54
54
  "tslib": "2.8.1",
55
- "tsx": "4.23.5",
56
55
  "typescript": "7.0.2",
57
- "vitest": "4.1.10"
56
+ "vitest": "4.1.11"
58
57
  },
59
58
  "optionalDependencies": {
60
- "@harperfast/rocksdb-js-darwin-arm64": "2.7.1",
61
- "@harperfast/rocksdb-js-darwin-x64": "2.7.1",
62
- "@harperfast/rocksdb-js-linux-arm64-glibc": "2.7.1",
63
- "@harperfast/rocksdb-js-linux-arm64-musl": "2.7.1",
64
- "@harperfast/rocksdb-js-linux-x64-glibc": "2.7.1",
65
- "@harperfast/rocksdb-js-linux-x64-musl": "2.7.1",
66
- "@harperfast/rocksdb-js-win32-arm64": "2.7.1",
67
- "@harperfast/rocksdb-js-win32-x64": "2.7.1"
59
+ "@harperfast/rocksdb-js-darwin-arm64": "2.9.0",
60
+ "@harperfast/rocksdb-js-darwin-x64": "2.9.0",
61
+ "@harperfast/rocksdb-js-linux-arm64-glibc": "2.9.0",
62
+ "@harperfast/rocksdb-js-linux-arm64-musl": "2.9.0",
63
+ "@harperfast/rocksdb-js-linux-x64-glibc": "2.9.0",
64
+ "@harperfast/rocksdb-js-linux-x64-musl": "2.9.0",
65
+ "@harperfast/rocksdb-js-win32-arm64": "2.9.0",
66
+ "@harperfast/rocksdb-js-win32-x64": "2.9.0"
68
67
  },
69
68
  "engines": {
70
69
  "node": "^22.18.0 || >=24.0.0"
71
70
  },
72
71
  "gypfile": true,
73
72
  "rocksdb": {
74
- "version": "11.1.2"
73
+ "version": "11.8.1"
75
74
  },
76
75
  "scripts": {
77
76
  "bench": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs bench --passWithNoTests --outputJson benchmark-results.json",
78
77
  "bench:bun": "cross-env CI=1 bun --bun bench",
79
78
  "bench:deno": "cross-env CI=1 deno run --allow-all --sloppy-imports ./node_modules/vitest/vitest.mjs bench",
80
- "build": "pnpm build:bundle && pnpm rebuild",
79
+ "build": "pnpm build:bundle && pnpm run rebuild",
81
80
  "build:binding": "node-gyp build",
82
81
  "build:binding:debug": "node-gyp build --coverage --debug --verbose",
83
82
  "build:bundle": "node scripts/clean-dist.mjs && tsdown -c tsdown.config.ts",
84
83
  "build:bundle:minify": "cross-env MINIFY=1 pnpm build:bundle",
85
84
  "check": "pnpm type-check && pnpm lint && pnpm fmt:check",
86
85
  "clean": "node-gyp clean",
87
- "coverage": "tsx scripts/coverage/main.ts",
88
- "coverage:native": "tsx scripts/native-test/coverage.mjs",
86
+ "coverage": "node scripts/coverage/main.ts",
87
+ "coverage:native": "node scripts/native-test/coverage.mjs",
89
88
  "fmt": "oxfmt",
90
89
  "fmt:check": "oxfmt --check",
91
90
  "lint": "oxlint",
@@ -95,7 +94,7 @@
95
94
  "test:bun": "cross-env CI=1 bun --bun --config=bunfig.toml ./node_modules/vitest/vitest.mjs --reporter=verbose",
96
95
  "test:deno": "cross-env CI=1 deno run --allow-all --sloppy-imports --v8-flags=--expose-gc ./node_modules/vitest/vitest.mjs",
97
96
  "test:stress": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs --reporter=verbose --config=vitest-stress.config.ts",
98
- "test:native": "tsx scripts/native-test/run.mjs",
97
+ "test:native": "node scripts/native-test/run.mjs",
99
98
  "type-check": "tsc --noEmit"
100
99
  }
101
100
  }