Files
skills/docs/security-stack/security/vault-sync/src/vector-clock/vector-clock.md
2026-05-29 08:20:56 +01:00

3.9 KiB

vector-clock

(no submodules)

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

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

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.