HarperFast / HarperFast/harper
Adopt the dual-clock record model on RocksDB: first word = transaction timestamp/log key, source/origin version in a distinct second word (stages 0b and 2)
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
## Direction (rewritten 2026-09-02)
Continues from #2409 (stage 0a, merged). The model, settled after [rocksdb-js#813](https://github.com/HarperFast/rocksdb-js/pull/813) (native commit-time stamping) was closed unmerged:
| Word | Meaning | Assigned by | Uniqueness |
|---|---|---|---|
| **First word** (`localTime`) | Write identity: verification-table freshness, record→log-entry lookup, subscription staleness, transaction grouping | rocksdb-js `startTimestamp` at transaction construction. A replication receiver or crash replay adopts the **origin's** first word via `setTimestamp`; no other caller may. | Unique within one origin's log. Identity is (nodeId, first word); records already carry nodeId. |
| **Second word** (`version`, under `HAS_DISTINCT_VERSION_FLAG`) | Record version: LWW in `precedesExistingVersion`, CRDT ordering, `@updatedTime`/`@createdTime`, ETag/Last-Modified, replication | Source `lastModified` (capped per #2065) or the origin's version. Absent ⇒ version == first word (today's encoding). | Legitimately non-unique |
| **Log batch key** | Resume cursor in the per-origin log | Equals the first word of every record in the batch | Per-origin log |
Invariant: **a record's first word equals the batch key in the origin node's log.** `auditStore.get(firstWord, tableId, id, nodeId)` (`Table.ts` ~2737/2801/2822 → `RocksTransactionLogStore.ts` ~171) depends on it. No native commit-time stamping (rocksdb-js#811 is rescoped to a read surface, `setTimestamp` hardening and a clock-floor seed). Per-node origin-clock logs and the resume-cursor model are unchanged; the previous plan's log-key-domain flip and per-hop cursors are gone.
## Stage 0b — normalize to LMDB semantics (no format change)
- On RocksDB, `AuditRecord.version` = record version and `localTime` = log key, the roles LMDB already has; the #2409 `recordVersion` alias gets absorbed.
- **Coordinated harper-pro PR** ([harper-pro#790](https://github.com/HarperFast/harper-pro/issues/790)): the receiver sets the apply transaction's timestamp from `localTime` (the origin's first word) and applies `version` per write; cursor sites move to `localTime`.
- Trap to pin: under normalization `removeAuditEntry`'s tombstone compare and the blob orphan scan would start matching record versions. A version match is not write identity; both must be pinned to a `localTime` compare in the same change.
- `event.version` delivered to subscribers: `version` = record version, `localTime` = log position, pinned by a transport-level test (ruling D3).
## Stage 2 — distinct-version format
- Encoder writes the first word = `transaction.timestamp` always (restoring first word == log key for fills, which #2065 broke and #2409 papered over on the read side), and an 8-byte second word under `HAS_DISTINCT_VERSION_FLAG` only when version ≠ first word. Old records decode as version == first word; lazy migration, no bulk rewrite.
- **Call-site audit**: every `precedesExistingVersion(...)` and `auditStore.get(...)` argument sorted into identity (first word) vs ordering (version). Known so far: `previousVersion` is stored from `existingEntry.version` on RocksDB (`RecordEncoder.ts` ~1019) and must become the first word or the out-of-order walk (`Table.ts` ~2836) breaks as soon as a distinct version exists; `additionalAuditRefs[].version` stays a version; the incoming write's ordering value is its version (its transaction timestamp unless a source/origin supplied one), compared against the existing `version`.
- `@updatedTime` (`Table.ts` ~2537) is stamped from the version, not the transaction timestamp. ETag/Last-Modified already read `entry.version` via `context.lastModified`; verify, don't change.
- `VERSION_NOT_UNIQUE_FLAG` stays (#766 is not retired). Re-key its producer (`RecordEncoder.ts` ~886, which compares versions today) to first-word equality: set when the new first word ≤ the existing first word, which is the cross-origin same-millisecond tie-break rewrite. Deletes stay excluded as today.
- Remove `timestamp` from the public `Context` (`ResourceInterface.ts` ~127, undocumented). Keep an internal-only carrier for the receiver (`Table.ts` ~1046) and replay (`replayLogs.ts` ~235); the fill token (`Table.ts` ~6221) then only ever sees the transaction's own timestamp, closing #2065's open item on unvalidated caller timestamps.
- #2065's `max(token, now)` cap on source versions stays: a future-dated version still wins LWW against every later local write.
- **Two REQUIRED gates, format-only now**: a peer gate (second-word bytes must not reach peers below the format floor; no protocol negotiation exists today) and a local downgrade gate (a durable in-store format floor written before or atomically with the first two-word record; package-version prompting cannot teach an already-released binary to refuse the store).
- Write-identity blocker unchanged: same-key writes chained in one transaction share a first word. Prove they collapse to one durable mutation or add an intra-transaction ordinal before any cleanup migrates to first-word identity.
- Corrupt-value outcomes are context-specific (drop/disconnect for peer input; skip publication where safe; controlled recovery failure for WAL-only state), validated per identity at its true ingress point.
## Dependencies
[rocksdb-js#811](https://github.com/HarperFast/rocksdb-js/issues/811) (rescoped), [harper-pro#790](https://github.com/HarperFast/harper-pro/issues/790), [harper#2411](https://github.com/HarperFast/harper/issues/2411) (replay applies version per write; first word via `setTimestamp`).
Contributor guide
Research direction
Start by tracing RecordEncoder.ts, Table.ts, RocksTransactionLogStore.ts, replayLogs.ts, and ResourceInterface.ts against the stated dependencies and invariants. Verify identity uses the first word/localTime while ordering uses version, then validate peer and local downgrade gates, receiver/replay behavior, and the required transport and same-key write proofs; the issue names no specific test files to run.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- databases, distributed-systems
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100