apps/readest-app/src/libs/crdt.README.md
crdt.ts — Hybrid Logical Clock + per-field LWWInternal sync primitive for the polymorphic replicas table. See
apps/readest-app/.claude/plans/vivid-orbiting-thimble.md for the full design.
replicaSyncManager and a Tauri/web HLC store.0000018e7d6ab5c0-00000007-device-uuid
└── physicalMs ─┘ └counter┘ └─deviceId─┘
13 hex chars 8 hex free-form
Lexicographic comparison of the packed string matches temporal order:
hlcCompare(packA, packB) === -1 ⟺ (msA, ctrA) < (msB, ctrB)
This is invariant — see the 1000-sample property test in
__tests__/libs/crdt.test.ts.
Each field in fields_jsonb is wrapped:
{ v: <any json>, t: <Hlc>, s: <deviceId> }
value when who wrote it
mergeFields(local, remote) keeps the envelope with the larger HLC. Ties
on HLC are broken by deviceId lex order (deterministic, both sides
converge).
A deleted_at_ts HLC marks the row deleted. Field writes do NOT revive
a tombstoned row — that is the unsafe pattern. To revive, use
withReincarnation(row, token) which creates a new logical entity.
deleted_at_ts ≠ null
│
┌─ field writes accumulate normally
│ but the row stays deleted
│
reincarnation = 'epoch-N' ← explicit token from importer
│
▼
row is alive again under new logical identity
mergeReplica(local, remote):
fields_jsonb ← per-field LWW
deleted_at_ts ← max(local, remote) (tombstones never disappear)
reincarnation ← newer non-null token; null only clears on newer tombstone
manifest_jsonb ← newer non-null manifest; null rows do not clear it
schema_version ← max
updated_at_ts ← max over fields, tombstone, and row-level ops
CRDT properties verified by tests:
mergeFields(a, b) === mergeFields(b, a)mergeFields(mergeFields(a, b), c) === mergeFields(a, mergeFields(b, c))mergeFields(a, a) === amergeReplica is commutative and idempotentThis is one of the few places in Readest where correctness is non-negotiable. Bugs cause silent data loss across devices.
pnpm test src/__tests__/libs/crdt.test.ts after every change.crypto/ (Lane B) — encrypted-field envelopes plug into setField /
mergeFields transparently. The CRDT machinery sees opaque
ciphertext.replicaSyncManager (Lane E) — owns the HLC generator instance,
observes remote HLCs on pull, persists snapshots to IndexedDB.crdt_merge_replica() Postgres function — must implement the same
merge semantics on the server side, atomically. Tests in PR 1 will
enforce client/server parity.