first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# vector-clock
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`vector-clock.ts` implements the causal-ordering primitives used by every
other module in `@suaveplan/vault-sync`. Vector clocks let two divergent
peers detect whether one branch is the causal ancestor of the other or
whether the edits were concurrent — a prerequisite for safe CRDT merges,
durable queue ordering, and explicit conflict reporting in `SyncEngine`.
The implementation deliberately avoids secret-dependent branches: every
comparison uses integer arithmetic against `nodeId`-keyed counters with no
short-circuit on values that could leak side-channel information.
## Features
- `increment(clock, nodeId)` — pure, allocates a fresh clock
- `merge(left, right)` — component-wise maximum, missing keys treated as zero
- `compare(left, right)` — returns `"<" | ">" | "==" | "concurrent"`
- `happensBefore(left, right)` — Lamport happens-before predicate
- `lwwTotalOrder(left, right)` — total ordering for the `"lww"` strategy with
`(clock, updatedAt, nodeId)` tie-breaking
- `empty()` — canonical empty vector clock factory
- All operations are pure and side-effect free; inputs are never mutated
- Saturation guard: counter never exceeds `Number.MAX_SAFE_INTEGER`
## Usage: Basic Example
```ts
import {
compare,
increment,
merge,
} from "@suaveplan/vault-sync/vector-clock/vector-clock";
const a = increment({}, "node-a");
const b = increment({}, "node-b");
console.log(compare(a, b)); // "concurrent"
console.log(merge(a, b)); // { "node-a": 1, "node-b": 1 }
```
## Usage: Advanced Example
```ts
import {
increment,
lwwTotalOrder,
merge,
} from "@suaveplan/vault-sync/vector-clock/vector-clock";
import type { VectorClockState } from "@suaveplan/vault-sync/types";
interface Snapshot {
clock: VectorClockState;
updatedAt: number;
nodeId: string;
}
function applyMutation(local: Snapshot, nodeId: string, now: number): Snapshot {
return {
clock: increment(local.clock, nodeId),
updatedAt: now,
nodeId,
};
}
function pickWinner(left: Snapshot, right: Snapshot): Snapshot {
return lwwTotalOrder(left, right) === -1 ? left : right;
}
// Reconcile a remote snapshot into local state.
function reconcile(local: Snapshot, remote: Snapshot): Snapshot {
const winner = pickWinner(local, remote);
return {
clock: merge(local.clock, remote.clock),
updatedAt: winner.updatedAt,
nodeId: winner.nodeId,
};
}
```
## API Reference
- `increment(clock: VectorClockState, nodeId: string): VectorClockState` —
returns a new clock with `nodeId`'s counter incremented by `1`. Throws
`VaultSyncError(VECTOR_CLOCK.REGRESSION)` if the counter is already at
`Number.MAX_SAFE_INTEGER`.
- `merge(left, right): VectorClockState` — element-wise max of two clocks.
- `compare(left, right): "<" | ">" | "==" | "concurrent"` — total relation
classifying causal order.
- `happensBefore(left, right): boolean` — `true` iff `compare === "<"`.
- `lwwTotalOrder(left, right): -1 | 0 | 1` — total order for last-write-wins
resolution: vector-clock dominance, then `updatedAt`, then `nodeId`.
- `empty(): VectorClockState` — `{}` literal helper.
## Implementation Notes
The clock is modelled as `Readonly<Record<string, number>>` rather than a
class so the data is trivially serialisable and comparable by deep equality
without custom `equals()`. `compare` enumerates the union of node ids; both
absent counters default to `0`, which preserves the identity that an empty
clock causally precedes any non-empty one. The `lwwTotalOrder` resolves
remaining ties deterministically so two peers with identical inputs always
elect the same winner — an essential property for offline-first
convergence. Saturation at `Number.MAX_SAFE_INTEGER` is treated as a hard
error rather than rolling over, since silent rollover would silently break
causal ordering.