|
| 1 | +# Hybrid Logical Clock |
| 2 | + |
| 3 | +A robust, minimal, and standards-compliant Hybrid Logical Clock (HLC) |
| 4 | +implementation for Deno and JavaScript/TypeScript. HLCs are used in distributed |
| 5 | +systems to generate timestamps that combine physical and logical time, enabling |
| 6 | +causality tracking and event ordering even in the presence of clock skew. |
| 7 | + |
| 8 | +## Installation |
| 9 | + |
| 10 | +```sh |
| 11 | +deno add jsr:@dldc/hybrid-logical-clock |
| 12 | +``` |
| 13 | + |
| 14 | +## Overview |
| 15 | + |
| 16 | +This package provides a simple and safe way to generate and merge Hybrid Logical |
| 17 | +Clock timestamps. It is ideal for distributed systems, CRDTs, event sourcing, |
| 18 | +and any scenario where you need to track causality and order events across |
| 19 | +nodes. |
| 20 | + |
| 21 | +- **Monotonic and Causally Consistent**: Ensures timestamps never go backward |
| 22 | + and reflect causal relationships. |
| 23 | +- **Customizable**: Supports custom node IDs, wall clock sources, and drift |
| 24 | + limits. |
| 25 | +- **Safe**: Detects and throws on excessive clock drift or logical counter |
| 26 | + overflow. |
| 27 | + |
| 28 | +## Usage Example |
| 29 | + |
| 30 | +```ts |
| 31 | +import { |
| 32 | + compareHLCTimestamps, |
| 33 | + createHLC, |
| 34 | + parseHLCTimestamp, |
| 35 | +} from "@dldc/hybrid-logical-clock"; |
| 36 | + |
| 37 | +// Create a new HLC instance (nodeId is optional) |
| 38 | +const hlc = createHLC({ nodeId: "node-1" }); |
| 39 | + |
| 40 | +// Generate a timestamp for a local/send event |
| 41 | +const t1 = hlc.send(); |
| 42 | + |
| 43 | +// Merge a remote timestamp (e.g., from another node) |
| 44 | +const remote = parseHLCTimestamp("2025-05-22T12:34:56.789Z|00000001|node-2"); |
| 45 | +const t2 = hlc.receive(remote!); |
| 46 | + |
| 47 | +// Serialize/deserialize |
| 48 | +const str = t2.toString(); |
| 49 | +const parsed = parseHLCTimestamp(str); |
| 50 | + |
| 51 | +// Compare timestamps |
| 52 | +const cmp = compareHLCTimestamps(t1, t2); // -1, 0, or 1 |
| 53 | +``` |
| 54 | + |
| 55 | +## Library Specificities |
| 56 | + |
| 57 | +- **Drift Detection**: If the physical time difference between local and remote |
| 58 | + exceeds `maxDrift` (default: 5 minutes), an error is thrown. |
| 59 | +- **Logical Counter Overflow**: If the logical counter exceeds 99,999,999, an |
| 60 | + error is thrown. |
| 61 | +- **Timestamps**: Timestamps are objects with `{ ts, cl, id, toString() }` and |
| 62 | + serialize to ISO8601|logical|nodeId. |
| 63 | +- **Customizable**: You can provide your own node ID, wall clock function, and |
| 64 | + drift limit. |
| 65 | + |
| 66 | +## API Reference |
| 67 | + |
| 68 | +### `createHLC(options?: HLCInstanceOptions): HLCInstance` |
| 69 | + |
| 70 | +Creates a new Hybrid Logical Clock instance. |
| 71 | + |
| 72 | +**Options:** |
| 73 | + |
| 74 | +- `nodeId?: string` — Unique identifier for this node (default: random UUID). |
| 75 | +- `getWallClockTime?: () => number` — Function to get current time (default: |
| 76 | + `Date.now()`). |
| 77 | +- `maxDrift?: number` — Maximum allowed drift in ms (default: 5 minutes). |
| 78 | + |
| 79 | +**Returns:** `HLCInstance` object: |
| 80 | + |
| 81 | +- `nodeId: string` — The node's unique ID. |
| 82 | +- `send(): HLCTimestamp` — Generate a new timestamp for a local/send event. |
| 83 | +- `receive(remote: HLCTimestamp): HLCTimestamp` — Merge a remote timestamp. |
| 84 | +- `MIN_TIMESTAMP: HLCTimestamp` — Minimum possible timestamp for this node. |
| 85 | +- `MAX_TIMESTAMP: HLCTimestamp` — Maximum possible timestamp for this node. |
| 86 | + |
| 87 | +--- |
| 88 | + |
| 89 | +### `HLCTimestamp` |
| 90 | + |
| 91 | +Represents a timestamp: |
| 92 | + |
| 93 | +- `ts: number` — Physical time (ms since epoch). |
| 94 | +- `cl: number` — Logical counter. |
| 95 | +- `id: string` — Node ID. |
| 96 | +- `toString(): string` — String representation |
| 97 | + (`YYYY-MM-DDTHH:mm:ss.sssZ|00000001|nodeId`). |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +### `compareHLCTimestamps(t1: HLCTimestamp, t2: HLCTimestamp): number` |
| 102 | + |
| 103 | +Compares two timestamps. |
| 104 | + |
| 105 | +- Returns `-1` if `t1 < t2`, `0` if equal, `1` if `t1 > t2`. |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +### `serializeHLC(hlc: HLCTimestamp): string` |
| 110 | + |
| 111 | +Serializes a timestamp to string. |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +### `parseHLCTimestamp(str: string): HLCTimestamp | null` |
| 116 | + |
| 117 | +Parses a string into a timestamp, or returns `null` if invalid. |
| 118 | + |
| 119 | +--- |
| 120 | + |
| 121 | +## Example: Handling Events |
| 122 | + |
| 123 | +```ts |
| 124 | +const hlcA = createHLC({ nodeId: "A" }); |
| 125 | +const hlcB = createHLC({ nodeId: "B" }); |
| 126 | + |
| 127 | +// Node A sends an event |
| 128 | +const tsA = hlcA.send(); |
| 129 | + |
| 130 | +// Node B receives the event from A |
| 131 | +const tsB = hlcB.receive(tsA); |
| 132 | + |
| 133 | +// Now, tsB > tsA and reflects the causal relationship |
| 134 | +``` |
| 135 | + |
| 136 | +## License |
| 137 | + |
| 138 | +MIT |
0 commit comments