@harperfast/rocksdb-js 2.10.0 → 2.11.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/stats.md CHANGED
@@ -32,53 +32,54 @@ const keysWrittenHistogram: StatsHistogramData = db.getStat('rocksdb.db.flush.mi
32
32
 
33
33
  By default, `db.getStats()` returns properties only.
34
34
 
35
- | Name | Description | Type |
36
- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
37
- | `columnFamily.pendingReclaims` | Number of dropped column families of this database whose physical `DropColumnFamily` has not completed: a commit admitted before the drop still holds it, or the drop failed and is retried on the next drop, open of that name, or close (see "Dropping column families" in the README). | gauge |
38
- | `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 |
39
- | `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 |
40
- | `rocksdb.block-cache-capacity` | Capacity in bytes of the block cache. | gauge |
41
- | `rocksdb.block-cache-pinned-usage` | Bytes occupied by pinned block cache entries. | gauge |
42
- | `rocksdb.block-cache-usage` | Bytes currently used by block cache entries. | gauge |
43
- | `rocksdb.compaction-pending` | `1` if at least one compaction is pending, else `0`. | gauge |
44
- | `rocksdb.cur-size-active-mem-table` | Approximate size in bytes of the active memtable. | gauge |
45
- | `rocksdb.cur-size-all-mem-tables` | Approximate size in bytes of the active and unflushed immutable memtables. | gauge |
46
- | `rocksdb.current-super-version-number` | Current super-version number, incremented on each LSM change. | gauge |
47
- | `rocksdb.estimate-live-data-size` | Estimated size in bytes of the live data. | gauge |
48
- | `rocksdb.estimate-num-keys` | Estimated number of live keys. | gauge |
49
- | `rocksdb.estimate-pending-compaction-bytes` | Estimated bytes compaction must rewrite to rebalance the LSM tree. | gauge |
50
- | `rocksdb.live-blob-file-size` | Total size in bytes of blob files in the current version. | gauge |
51
- | `rocksdb.live-sst-files-size` | Total size in bytes of SST files in the current version. | gauge |
52
- | `rocksdb.mem-table-flush-pending` | `1` if a memtable flush is pending, else `0`. | gauge |
53
- | `rocksdb.num-blob-files` | Number of blob files in the current version. | gauge |
54
- | `rocksdb.num-deletes-active-mem-table` | Number of delete entries in the active memtable. | gauge |
55
- | `rocksdb.num-entries-active-mem-table` | Number of entries in the active memtable. | gauge |
56
- | `rocksdb.num-immutable-mem-table` | Number of immutable memtables not yet flushed. | gauge |
57
- | `rocksdb.num-immutable-mem-table-flushed` | Number of immutable memtables already flushed. | gauge |
58
- | `rocksdb.num-live-versions` | Number of live LSM versions (high values indicate versions held by iterators/snapshots). | gauge |
59
- | `rocksdb.num-running-compactions` | Number of compactions currently running. | gauge |
60
- | `rocksdb.num-running-flushes` | Number of flushes currently running. | gauge |
61
- | `rocksdb.num-snapshots` | Number of unreleased snapshots. Any nonzero value blocks reclamation of obsolete versions behind the oldest one. | gauge |
62
- | `rocksdb.oldest-snapshot-time` | Unix timestamp of the oldest unreleased snapshot, or `0`. | gauge |
63
- | `rocksdb.size-all-mem-tables` | Approximate size in bytes of active, unflushed, and pinned memtables. | gauge |
64
- | `rocksdb.total-blob-file-size` | Total size in bytes of all blob files across all versions. | gauge |
65
- | `rocksdb.total-sst-files-size` | Total size in bytes of all SST files across all versions. | gauge |
66
- | `txnlog.activeMaps` | Number of transaction log files currently memory-mapped, summed across all logs. | gauge |
67
- | `txnlog.bytesWritten` | Cumulative bytes written to transaction logs (entry payload plus entry header, excluding file headers), summed across all logs. | ticker |
68
- | `txnlog.fileCount` | Total number of transaction log files on disk, summed across all logs. | gauge |
69
- | `txnlog.logCount` | Number of transaction log stores attached to the database. | gauge |
70
- | `txnlog.mappedBytes` | Virtual address space in bytes reserved for memory-mapped log files, summed across all logs (on POSIX the active write file maps the full configured `maxFileSize`, so this is allocated address space, not resident memory). | gauge |
71
- | `txnlog.overlayBytes` | File-backed overlay portion in bytes (POSIX only; `0` on Windows), summed across all logs — a closer proxy for real memory consumption than `mappedBytes`. | gauge |
72
- | `txnlog.pendingTransactions` | Number of transactions bound to a log but not yet written via `writeBatch()`, summed across all logs. | gauge |
73
- | `txnlog.replayGapBytes` | Bytes between the last flushed position and the write head (written but not yet flushed to the database), summed across all logs. | gauge |
74
- | `txnlog.totalSizeBytes` | Total on-disk size in bytes of all transaction log files, summed across all logs. | gauge |
75
- | `txnlog.transactionsWritten` | Cumulative number of transactions successfully written to the logs (lifetime total), summed across all logs. | ticker |
76
- | `txnlog.uncommittedTransactions` | Number of transactions written to a log but not yet committed to RocksDB, summed across all logs. | gauge |
77
- | `writeBufferManager.bufferSize` | The `WriteBufferManager`'s memtable-memory budget in bytes, read live. `0` when no manager is configured. **Process-wide**, not per-database — see below. | gauge |
78
- | `writeBufferManager.memoryUsage` | Total memtable memory in bytes charged against the manager. **Process-wide.** | gauge |
79
- | `writeBufferManager.mutableMemoryUsage` | The share of `writeBufferManager.memoryUsage` held by active (mutable) memtables; the rest is awaiting flush or retained write history. **Process-wide.** | gauge |
80
- | `writeBufferManager.stallActive` | `1` while the manager is stalling writes, else `0`. **Process-wide.** | gauge |
81
- | `writeBufferManager.stallActiveMs` | How long the current stall has been active, in milliseconds; `0` when not stalled. Sampled once a second by the stall watchdog. **Process-wide.** | gauge |
35
+ | Name | Description | Type |
36
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
37
+ | `columnFamily.pendingReclaims` | Number of dropped column families of this database whose physical `DropColumnFamily` has not completed: a commit admitted before the drop still holds it, or the drop failed and is retried on the next drop, open of that name, or close (see "Dropping column families" in the README). | gauge |
38
+ | `commitPipeline.commitQueueDepth` | Number of async commits queued for the database's commit threads but not yet started. In the default single-lane mode each queued commit is the whole commit (log write + RocksDB commit) regardless of `commitThreads`; only under the two-lane pipeline (`ROCKSDB_JS_COMMIT_THREAD=2`) does this queue cover just the RocksDB commit, with the log write queued separately (see `commitPipeline.logQueueDepth`). | gauge |
39
+ | `commitPipeline.commitThreads` | Number of commit threads the database has started, at most the `commitThreads` setting it was opened with. Threads start only when commits overlap. | gauge |
40
+ | `commitPipeline.logQueueDepth` | Number of async commits queued on the database's transaction-log lane but not yet started (always `0` unless `ROCKSDB_JS_COMMIT_THREAD=2`). | gauge |
41
+ | `rocksdb.block-cache-capacity` | Capacity in bytes of the block cache. | gauge |
42
+ | `rocksdb.block-cache-pinned-usage` | Bytes occupied by pinned block cache entries. | gauge |
43
+ | `rocksdb.block-cache-usage` | Bytes currently used by block cache entries. | gauge |
44
+ | `rocksdb.compaction-pending` | `1` if at least one compaction is pending, else `0`. | gauge |
45
+ | `rocksdb.cur-size-active-mem-table` | Approximate size in bytes of the active memtable. | gauge |
46
+ | `rocksdb.cur-size-all-mem-tables` | Approximate size in bytes of the active and unflushed immutable memtables. | gauge |
47
+ | `rocksdb.current-super-version-number` | Current super-version number, incremented on each LSM change. | gauge |
48
+ | `rocksdb.estimate-live-data-size` | Estimated size in bytes of the live data. | gauge |
49
+ | `rocksdb.estimate-num-keys` | Estimated number of live keys. | gauge |
50
+ | `rocksdb.estimate-pending-compaction-bytes` | Estimated bytes compaction must rewrite to rebalance the LSM tree. | gauge |
51
+ | `rocksdb.live-blob-file-size` | Total size in bytes of blob files in the current version. | gauge |
52
+ | `rocksdb.live-sst-files-size` | Total size in bytes of SST files in the current version. | gauge |
53
+ | `rocksdb.mem-table-flush-pending` | `1` if a memtable flush is pending, else `0`. | gauge |
54
+ | `rocksdb.num-blob-files` | Number of blob files in the current version. | gauge |
55
+ | `rocksdb.num-deletes-active-mem-table` | Number of delete entries in the active memtable. | gauge |
56
+ | `rocksdb.num-entries-active-mem-table` | Number of entries in the active memtable. | gauge |
57
+ | `rocksdb.num-immutable-mem-table` | Number of immutable memtables not yet flushed. | gauge |
58
+ | `rocksdb.num-immutable-mem-table-flushed` | Number of immutable memtables already flushed. | gauge |
59
+ | `rocksdb.num-live-versions` | Number of live LSM versions (high values indicate versions held by iterators/snapshots). | gauge |
60
+ | `rocksdb.num-running-compactions` | Number of compactions currently running. | gauge |
61
+ | `rocksdb.num-running-flushes` | Number of flushes currently running. | gauge |
62
+ | `rocksdb.num-snapshots` | Number of unreleased snapshots. Any nonzero value blocks reclamation of obsolete versions behind the oldest one. | gauge |
63
+ | `rocksdb.oldest-snapshot-time` | Unix timestamp of the oldest unreleased snapshot, or `0`. | gauge |
64
+ | `rocksdb.size-all-mem-tables` | Approximate size in bytes of active, unflushed, and pinned memtables. | gauge |
65
+ | `rocksdb.total-blob-file-size` | Total size in bytes of all blob files across all versions. | gauge |
66
+ | `rocksdb.total-sst-files-size` | Total size in bytes of all SST files across all versions. | gauge |
67
+ | `txnlog.activeMaps` | Number of transaction log files currently memory-mapped, summed across all logs. | gauge |
68
+ | `txnlog.bytesWritten` | Cumulative bytes written to transaction logs (entry payload plus entry header, excluding file headers), summed across all logs. | ticker |
69
+ | `txnlog.fileCount` | Total number of transaction log files on disk, summed across all logs. | gauge |
70
+ | `txnlog.logCount` | Number of transaction log stores attached to the database. | gauge |
71
+ | `txnlog.mappedBytes` | Virtual address space in bytes reserved for memory-mapped log files, summed across all logs (on POSIX the active write file maps the full configured `maxFileSize`, so this is allocated address space, not resident memory). | gauge |
72
+ | `txnlog.overlayBytes` | File-backed overlay portion in bytes (POSIX only; `0` on Windows), summed across all logs — a closer proxy for real memory consumption than `mappedBytes`. | gauge |
73
+ | `txnlog.pendingTransactions` | Number of transactions bound to a log but not yet written via `writeBatch()`, summed across all logs. | gauge |
74
+ | `txnlog.replayGapBytes` | Bytes between the last flushed position and the write head (written but not yet flushed to the database), summed across all logs. | gauge |
75
+ | `txnlog.totalSizeBytes` | Total on-disk size in bytes of all transaction log files, summed across all logs. | gauge |
76
+ | `txnlog.transactionsWritten` | Cumulative number of transactions successfully written to the logs (lifetime total), summed across all logs. | ticker |
77
+ | `txnlog.uncommittedTransactions` | Number of transactions written to a log but not yet committed to RocksDB, summed across all logs. | gauge |
78
+ | `writeBufferManager.bufferSize` | The `WriteBufferManager`'s memtable-memory budget in bytes, read live. `0` when no manager is configured. **Process-wide**, not per-database — see below. | gauge |
79
+ | `writeBufferManager.memoryUsage` | Total memtable memory in bytes charged against the manager. **Process-wide.** | gauge |
80
+ | `writeBufferManager.mutableMemoryUsage` | The share of `writeBufferManager.memoryUsage` held by active (mutable) memtables; the rest is awaiting flush or retained write history. **Process-wide.** | gauge |
81
+ | `writeBufferManager.stallActive` | `1` while the manager is stalling writes, else `0`. **Process-wide.** | gauge |
82
+ | `writeBufferManager.stallActiveMs` | How long the current stall has been active, in milliseconds; `0` when not stalled. Sampled once a second by the stall watchdog. **Process-wide.** | gauge |
82
83
 
83
84
  #### The `writeBufferManager.*` keys are process-wide
84
85
 
@@ -225,7 +225,8 @@ monotonic clock when it is constructed but is appended when it commits, so under
225
225
  entry can carry a smaller timestamp; an entry that adopted an origin timestamp with
226
226
  `txn.setTimestamp()` carries that origin's clock instead. Timestamps are not unique: the monotonic
227
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
228
+ backwards can reissue one unless the floor below is seeded, and `txn.setTimestamp()` can assign any
229
+ value — including one already
229
230
  in the log — to as many transactions as the caller likes. Deduplicating on the timestamp alone is
230
231
  therefore never safe; a consumer that needs identity has to supply it (Harper pairs the timestamp
231
232
  with the originating node).
@@ -236,6 +237,124 @@ running maxima — and a query seeks to the lower bound of that index, which is
236
237
  or before every entry in the requested range. Reading forward from there and filtering is what makes
237
238
  range queries correct on an unordered file.
238
239
 
240
+ ## The Timestamp Floor At Open
241
+
242
+ A transaction's timestamp is the key of the batch it is written under, so it has to stay unique
243
+ within the log it is written to. The process clock (`db.getMonotonicTimestamp()`) guarantees that
244
+ only within one process: a new process reads the wall clock again, so a backward step between runs
245
+ can reissue a key that is already durable in the log.
246
+
247
+ Opening with the `timestampFloorLog` option names the log whose keys this process originates. Every
248
+ segment of that store is then walked once, after open-time recovery has decided which bytes are
249
+ still durable, and the process clock is raised above the largest key found — before the database
250
+ handle is returned, so no transaction can be constructed below it. Keys are not ordered within or
251
+ across segments, and a segment header records the store's latest timestamp only as of that
252
+ segment's creation, so there is no shortcut: every segment is read.
253
+
254
+ Name only a log this process originates. A log a replication receiver writes under an adopted origin
255
+ timestamp is keyed by another node's clock, and seeding from it would ratchet this process's clock
256
+ to the fastest of those nodes at each restart. Native code cannot tell the two kinds of log apart,
257
+ which is why the caller names it and why an unset option leaves the clock alone.
258
+
259
+ The walk is the cost of the guarantee, and it is paid on the calling thread before `open()` returns.
260
+ It reads entry headers through a shared 64 KiB window and seeks past payloads, so it scales with the
261
+ number of entries in the named log rather than its size. Measured on Linux with a warm page cache,
262
+ against the same database opened without the option:
263
+
264
+ | Named log | Added open time (median) |
265
+ | --------------------------------- | ------------------------ |
266
+ | 25,000 entries, 99 MB, 7 segments | ~31 ms |
267
+ | 250,000 entries, 19 MB | ~273 ms |
268
+
269
+ Roughly a millisecond per thousand entries. There is a second term on Windows only. A segment there
270
+ is pre-extended to `transactionLogMaxSize` when it is mapped and keeps that physical size for life —
271
+ rotation does not truncate it — so a segment's bytes past its last entry are zero padding, and the
272
+ walk has to read that padding to prove nothing durable hides in it (see fail closed, below). So on
273
+ Windows the walk also scales with `segments x transactionLogMaxSize`. Measured on Linux against
274
+ deliberately Windows-shaped segments, 80 MiB of padding across 5 segments added ~148 ms to the open
275
+ with a warm page cache; a cold cache or slower storage is proportionally worse, and this has not been
276
+ measured on Windows itself. Size the budget below with that in mind before enabling the option on a
277
+ Windows deployment with a long retention window.
278
+
279
+ Retention bounds a log's age, not its entry count, so
280
+ the walk is bounded directly instead: `ROCKSDB_JS_TIMESTAMP_FLOOR_SCAN_MS` (default `2000`) caps it,
281
+ newest segment first, and a walk that runs out of budget rejects an open that names the log. The
282
+ rejection reports the segments and bytes the walk covered, so the budget can be sized from what the
283
+ store actually costs.
284
+ The deadline is checked before each physical read; sequential headers share a 64 KiB window, so the
285
+ small amount of already-buffered header parsing can finish after the deadline but one large segment
286
+ cannot run unbounded.
287
+ The value is honored literally between `0` (scan nothing, and reject) and one day, and there is no
288
+ unbounded setting — the failure it bounds is an `open()` that does not return, so a deployment that
289
+ would rather wait raises the number. Above a day it is clamped, because the deadline is a monotonic
290
+ clock time point and a larger value overflows its resolution and wraps into the past.
291
+ A database opened without `timestampFloorLog` pays none of it. The walk runs while database opens
292
+ and closes are serialized process-wide, so increasing its budget can delay unrelated opens and
293
+ closes too — and the budget is **per database path**, not per process, so a process that opens twenty
294
+ opted-in databases can spend twenty times the number set here, serialized.
295
+
296
+ The seed is fail closed when `timestampFloorLog` is set, and it asks a stronger question of each
297
+ segment than recovery does. Recovery decides where it is safe to truncate; this has to establish that
298
+ it observed every durable key, so the walk may stop short of a segment's extent only on a proof that
299
+ no complete frame can remain: the framed entries reach the extent, or everything past them is zero,
300
+ or fewer than 13 bytes remain (too few to open an entry header). Anything else — a framing break, a
301
+ declared length that overruns, bytes that are neither framed nor padding — rejects the open, as does a
302
+ segment that cannot be opened or scanned, an exhausted budget, or a key more than ten years ahead of
303
+ the wall clock. Entries past a break can still be durable and `query()` resyncs to them, which is
304
+ exactly why this walk cannot treat them as absent.
305
+
306
+ The first open that names `timestampFloorLog` must be the writable primary. A read-only or secondary
307
+ handle cannot establish a cross-process snapshot: a primary could durably append a larger key as the
308
+ handle scans. Once this process has seeded that same log through a writable primary, read-only and
309
+ secondary descriptors reuse the established seed without rescanning. A handle that cannot meet this
310
+ rule must omit the option.
311
+
312
+ The writable recovery scan is separately bounded by `ROCKSDB_JS_TRANSACTION_LOG_RECOVERY_SCAN_MS`
313
+ (default `2000`). At a zero end-of-entries marker it proves every remaining byte is zero before a
314
+ POSIX opener durably truncates copied Windows padding; on timeout, non-zero data, or a failed sync it
315
+ refuses the writable open rather than allowing a later `O_APPEND` write to become invisible past the
316
+ marker.
317
+
318
+ Discovery only registers segments whose filename is one the writer could have produced —
319
+ `<sequence>.txnlog`, a positive decimal with no padding or suffix. A `.txnlog` that is not, or a
320
+ second file claiming a sequence already registered, is ignored and counted as a segment discovery
321
+ could not place, which refuses an opted-in open and names the file. This matters because the
322
+ filename _is_ the segment's identity: a `1 copy.txnlog` next to `1.txnlog` used to take its place in
323
+ the registry, so the real segment was never read and the floor was raised without its keys. **An
324
+ ordinary open is affected too** — such a file used to be exposed as a real segment and could be
325
+ recovered or appended relative to; it is now ignored, so a directory holding manually renamed or
326
+ copied segments starts from the canonical sequence state instead. Rename only after establishing
327
+ which file is canonical. Note that a canonical name is a syntax check, not provenance: a valid
328
+ `9.txnlog` copied in from another node passes it, so directory integrity and naming the log this
329
+ process originates stay the caller's responsibility.
330
+
331
+ A scan that succeeds after using most of its budget warns, so the growth that would otherwise turn
332
+ into a refusal on some later restart is visible before it does.
333
+
334
+ A `timestampFloorLog` naming a log the database does not have warns — including on a database with no
335
+ logs at all, which is where a misspelled name is most likely: a newly named locally originated log has
336
+ no existing keys to seed. Callers that need restart-safe uniqueness must provision and name their
337
+ local log consistently before its first write; a missing name can otherwise be a configuration mistake
338
+ that leaves no key to scan.
339
+
340
+ After that warning, the first use can create a genuinely fresh or empty named directory. It cannot
341
+ adopt a nonempty directory that reappears before that first use: its keys were not covered by the
342
+ missing-log seed, and a transaction's timestamp already exists before `txn.useLog()` could inspect it.
343
+ That use — and a read-only or secondary re-open that discovers the directory — refuses. Close every
344
+ handle for the path and reopen with `timestampFloorLog` so the restored log is scanned before use.
345
+
346
+ A floor raised more than a second above the wall clock also warns. Only a backward clock step puts
347
+ durable keys ahead of now, and from that point the process issues timestamps off the floor rather than
348
+ tracking wall time — and each restart re-seeds from the keys it just wrote — so it stays ahead until
349
+ the clock catches up.
350
+
351
+ The seed belongs to the physical database path, not to one handle. The first open of a path resolves
352
+ it; a later open of the same path — including a read-only or secondary open, which gets its own
353
+ descriptor — finds it already resolved and does not walk the log again, which also means it never
354
+ scans a store the first handle may be appending to. An open that names a _different_ log than the one
355
+ already resolved for the path, or names one when the path was opened without it, is rejected rather
356
+ than appearing to work.
357
+
239
358
  ### Sequential Read
240
359
 
241
360
  1. Parse file header to get version
@@ -90,10 +90,10 @@ work.
90
90
  Keep `setTimestamp()` as the adoption API, reject invalid timestamp values, and reject changes once
91
91
  the transaction is no longer pending or has staged a write or log entry. Preserve the existing
92
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
93
+ `committedPosition` freezes a timestamp whose batch already landed. Remove the second-word read API
94
+ and flag from this PR; they belong to Harper's record codec. The clock-floor-at-open implementation
95
+ is retained as an explicit, caller-named option: it scans only the locally originated log, and
96
+ fails the opted-in open when it cannot prove a complete, plausible floor. The existing
97
97
  `latestTimestamp` behavior used to populate new transaction-log segment headers remains unchanged.
98
98
 
99
99
  Document what is true about log keys today, which the issue also asked for: entries are appended in
@@ -117,7 +117,69 @@ and making every read a state transition still could not prove that a producer's
117
117
  the transaction timestamp. The enforced boundary therefore remains the first native staging event;
118
118
  producer ordering is documented explicitly.
119
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.
120
+ The clock-floor scan's far-future check stays in the open path rather than changing appends: an
121
+ implausible key refuses the explicitly opted-in open, while normal append and segment-header
122
+ behavior remains unchanged. A floor based on unnamed logs remains out of scope because it cannot
123
+ distinguish locally issued keys from adopted peer keys.
124
+
125
+ ## Clock-floor fail-closed amendment
126
+
127
+ The task owner explicitly selected restart-safe key uniqueness over availability when
128
+ `timestampFloorLog` is configured. This changes that option's failure behavior only; omitting the
129
+ option preserves the existing process-local clock behavior.
130
+
131
+ ### Approaches considered
132
+
133
+ #### Do less
134
+
135
+ Warn and raise the floor from the readable prefix. Rejected: an unreadable segment, framing break,
136
+ or exhausted budget can hide a durable key, leaving the next process able to issue the same key.
137
+
138
+ #### Different layer
139
+
140
+ Make `getMonotonicTimestamp()` persist its own global floor. Rejected: the timestamp source cannot
141
+ know which transaction-log keys this process originated, and a global persisted value would add a
142
+ separate durability protocol without proving it covers the caller's log.
143
+
144
+ #### Deeper cause
145
+
146
+ Require every transaction-log append to use an externally durable counter. Rejected: that changes
147
+ the transaction timestamp contract and replication/replay adoption path, far beyond the opt-in
148
+ startup safety check requested here.
149
+
150
+ #### Chosen
151
+
152
+ After recovery, scan every segment in the caller-named local log under the bounded deadline. Reject
153
+ the open before raising the process-wide floor if the scan is incomplete or sees an implausibly
154
+ future key. A read-only torn tail shorter than an entry header remains valid: no frame can begin in it, so its
155
+ contiguous prefix provably covers every durable entry. A longer one is refused, because nothing
156
+ distinguishes a partial append from corruption hiding a complete higher-keyed frame. The named-log-missing warning remains non-fatal so a fresh local log can be named
157
+ before its first append.
158
+
159
+ ### First-use guard for an absent log
160
+
161
+ The warning remains usable for a fresh log, but it cannot authorize adopting old data later in the
162
+ same process. `SeedTimestampFloor` records that the named log was absent. Before a writable first
163
+ use creates a store, the registry rejects a nonempty directory with that name; it also rejects a
164
+ later read-only or secondary descriptor that discovers one. A transaction obtains its timestamp
165
+ before `txn.useLog()`, so rescanning at lazy resolution could not repair an already-issued key.
166
+ Closing every handle removes that per-path state; the next opted-in open discovers and scans the
167
+ restored log before a transaction can be constructed. A process-created fresh log clears the guard,
168
+ so normal destroy-and-recreate log maintenance remains available.
169
+
170
+ ### Verification route
171
+
172
+ The forked integration fixture writes a named log in one process and opens it in another. It verifies
173
+ successful seeding, then independently corrupts a segment, exhausts the deadline, skips discovery,
174
+ creates a mid-file framing break, writes an implausible key, and restores a named directory after an
175
+ absent seed; each unsafe case must reject the opted-in open. Native tests cover scan classification
176
+ and the repository build/check gates validate the binding and TypeScript surface.
177
+
178
+ ### Late opt-in and the scan budget
179
+
180
+ A handle that names `timestampFloorLog` after the path was opened without it is rejected rather than
181
+ warned: the descriptor cannot rerun its one-time seed, and accepting the request would suggest
182
+ restart-safe uniqueness that is not in force. The scan budget stays bounded by default. An unbounded
183
+ scan can hold serialized opens indefinitely; the bounded scan instead rejects an opted-in open when
184
+ it cannot prove the floor, and an operator who needs more time raises
185
+ `ROCKSDB_JS_TIMESTAMP_FLOOR_SCAN_MS`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harperfast/rocksdb-js",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "RocksDB binding for Node.js",
5
5
  "homepage": "https://github.com/HarperFast/rocksdb-js",
6
6
  "bugs": [
@@ -38,15 +38,15 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@rollup/plugin-replace": "6.0.3",
41
- "@types/node": "26.6.1",
41
+ "@types/node": "26.6.4",
42
42
  "@vitest/coverage-v8": "4.1.11",
43
43
  "cross-env": "10.1.0",
44
- "dotenv": "17.4.2",
45
- "lefthook": "2.1.14",
46
- "lmdb": "3.5.6",
47
- "node-gyp": "13.0.2",
48
- "oxfmt": "0.68.0",
49
- "oxlint": "1.83.0",
44
+ "dotenv": "18.0.5",
45
+ "lefthook": "2.1.16",
46
+ "lmdb": "3.5.7",
47
+ "node-gyp": "13.1.0",
48
+ "oxfmt": "0.71.0",
49
+ "oxlint": "1.86.0",
50
50
  "prebuildify": "6.0.1",
51
51
  "semver": "7.8.5",
52
52
  "tar": "7.5.22",
@@ -56,14 +56,14 @@
56
56
  "vitest": "4.1.11"
57
57
  },
58
58
  "optionalDependencies": {
59
- "@harperfast/rocksdb-js-darwin-arm64": "2.10.0",
60
- "@harperfast/rocksdb-js-darwin-x64": "2.10.0",
61
- "@harperfast/rocksdb-js-linux-arm64-glibc": "2.10.0",
62
- "@harperfast/rocksdb-js-linux-arm64-musl": "2.10.0",
63
- "@harperfast/rocksdb-js-linux-x64-glibc": "2.10.0",
64
- "@harperfast/rocksdb-js-linux-x64-musl": "2.10.0",
65
- "@harperfast/rocksdb-js-win32-arm64": "2.10.0",
66
- "@harperfast/rocksdb-js-win32-x64": "2.10.0"
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"
67
67
  },
68
68
  "engines": {
69
69
  "node": "^22.18.0 || >=24.0.0"