Skip to content

Commit bdd5424

Browse files
committed
Initial implementation
0 parents  commit bdd5424

File tree

13 files changed

+926
-0
lines changed

13 files changed

+926
-0
lines changed

.github/workflows/publish.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Publish
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
id-token: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: denoland/setup-deno@v1
17+
with:
18+
deno-version: v2.x
19+
20+
- run: deno task check
21+
22+
- run: npx jsr publish

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.DS_Store
2+
3+
# Logs
4+
logs
5+
*.log
6+
npm-debug.log*
7+
yarn-debug.log*
8+
yarn-error.log*
9+
lerna-debug.log*
10+
11+
*.tsbuildinfo
12+
coverage
13+
node_modules/
14+
jspm_packages/
15+
.env
16+
.env.test
17+
18+
dist

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"deno.enable": true,
3+
"editor.defaultFormatter": "denoland.vscode-deno",
4+
"npm.autoDetect": "off"
5+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Etienne Dldc
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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

deno.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@dldc/hybrid-logical-clock",
3+
"version": "0.0.0",
4+
"exports": "./mod.ts",
5+
"imports": {
6+
"@std/expect": "jsr:@std/expect@^1.0.15"
7+
},
8+
"tasks": {
9+
"test:run": "deno test -A",
10+
"test:watch": "deno test --watch",
11+
"bump": "deno run -A jsr:@mys/bump@1",
12+
"deps:outdated": "deno outdated",
13+
"deps:update": "deno outdated --update --latest --interactive",
14+
"check": "deno fmt --check . && deno lint . && deno check **/*.ts && deno task test:run",
15+
"test:coverage": "deno test -A --coverage=coverage && deno coverage coverage --html"
16+
},
17+
"lint": {
18+
"rules": {
19+
"exclude": [
20+
"no-explicit-any"
21+
]
22+
}
23+
}
24+
}

deno.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./src/defaults.ts";
2+
export * from "./src/hlc.ts";
3+
export * from "./src/types.ts";

src/defaults.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* The default maximum allowed drift (in milliseconds) between local and remote physical clocks.
3+
* Used to detect and prevent excessive clock skew. Default: 5 minutes.
4+
*/
5+
export const DEFAULT_MAX_DRIFT = 5 * 60 * 1000; // 5 minutes
6+
7+
/**
8+
* The default function to get the current wall clock time (in ms since epoch).
9+
* By default, uses Date.now().
10+
*/
11+
export const DEFAULT_WALL_CLOCK_TIME = () => Date.now();
12+
13+
/**
14+
* The default function to generate a unique node identifier for a clock instance.
15+
* By default, uses crypto.randomUUID().
16+
*/
17+
export const DEFAULT_CREATE_NODE_ID = () => crypto.randomUUID();
18+
19+
/**
20+
* The number of digits used for the logical clock counter in string representations.
21+
* Default: 8 digits (e.g., 00000001).
22+
*/
23+
export const LOGICAL_CLOCK_LENGTH = 8; // 8 digits
24+
25+
/**
26+
* The maximum value for the logical clock counter.
27+
* Default: 99999999 (8 digits).
28+
*/
29+
export const MAX_LOGICAL_CLOCK = Math.pow(10, LOGICAL_CLOCK_LENGTH) - 1; // 99999999
30+
31+
/**
32+
* The maximum allowed epoch timestamp (in ms since epoch).
33+
* After year 9999, ISO format adds a + sign to the year which makes it not sortable.
34+
* Default: 9999-12-31T23:59:59.999Z
35+
*/
36+
export const MAX_EPOCH = 253402300799999; // 9999-12-31T23:59:59.999Z

0 commit comments

Comments
 (0)