@kubun/hlc 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +87 -1
- package/package.json +9 -9
package/lib/index.js
CHANGED
|
@@ -1 +1,87 @@
|
|
|
1
|
-
|
|
1
|
+
// The serialized counter is fixed-width zero-padded so that lexicographic string
|
|
2
|
+
// order matches numeric order. The pad width therefore caps the counter: any value
|
|
3
|
+
// reaching this ceiling would emit an extra digit and silently break that ordering.
|
|
4
|
+
const COUNTER_PAD = 8;
|
|
5
|
+
const COUNTER_CEILING = 1e8;
|
|
6
|
+
export class HLC {
|
|
7
|
+
#nodeID;
|
|
8
|
+
#lastWallTime = 0;
|
|
9
|
+
#lastCounter = 0;
|
|
10
|
+
#counterCeiling;
|
|
11
|
+
constructor(params){
|
|
12
|
+
this.#nodeID = params.nodeID;
|
|
13
|
+
this.#counterCeiling = Math.min(params.counterCeiling ?? COUNTER_CEILING, COUNTER_CEILING);
|
|
14
|
+
}
|
|
15
|
+
get nodeID() {
|
|
16
|
+
return this.#nodeID;
|
|
17
|
+
}
|
|
18
|
+
// Apply a candidate counter at the current wall time. If it would reach the
|
|
19
|
+
// serialization ceiling, advance the wall time by 1ms and reset the counter to 0
|
|
20
|
+
// instead of emitting a 9th digit. This keeps the result strictly greater than the
|
|
21
|
+
// previous timestamp (counter ceiling < a +1ms wall time) while preserving the
|
|
22
|
+
// lexicographic == numeric ordering invariant.
|
|
23
|
+
#applyCounter(candidate) {
|
|
24
|
+
if (candidate >= this.#counterCeiling) {
|
|
25
|
+
this.#lastWallTime += 1;
|
|
26
|
+
this.#lastCounter = 0;
|
|
27
|
+
} else {
|
|
28
|
+
this.#lastCounter = candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
now() {
|
|
32
|
+
const physicalTime = Date.now();
|
|
33
|
+
if (physicalTime > this.#lastWallTime) {
|
|
34
|
+
this.#lastWallTime = physicalTime;
|
|
35
|
+
this.#lastCounter = 0;
|
|
36
|
+
} else {
|
|
37
|
+
this.#applyCounter(this.#lastCounter + 1);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
wallTime: this.#lastWallTime,
|
|
41
|
+
counter: this.#lastCounter,
|
|
42
|
+
nodeID: this.#nodeID
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
receive(remote) {
|
|
46
|
+
const physicalTime = Date.now();
|
|
47
|
+
if (physicalTime > this.#lastWallTime && physicalTime > remote.wallTime) {
|
|
48
|
+
this.#lastWallTime = physicalTime;
|
|
49
|
+
this.#lastCounter = 0;
|
|
50
|
+
} else if (remote.wallTime > this.#lastWallTime) {
|
|
51
|
+
this.#lastWallTime = remote.wallTime;
|
|
52
|
+
this.#applyCounter(remote.counter + 1);
|
|
53
|
+
} else if (this.#lastWallTime > remote.wallTime) {
|
|
54
|
+
this.#applyCounter(this.#lastCounter + 1);
|
|
55
|
+
} else {
|
|
56
|
+
this.#applyCounter(Math.max(this.#lastCounter, remote.counter) + 1);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
wallTime: this.#lastWallTime,
|
|
60
|
+
counter: this.#lastCounter,
|
|
61
|
+
nodeID: this.#nodeID
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
static serialize(ts) {
|
|
65
|
+
const iso = new Date(ts.wallTime).toISOString();
|
|
66
|
+
const counter = ts.counter.toString().padStart(COUNTER_PAD, '0');
|
|
67
|
+
return `${iso}:${counter}:${ts.nodeID}`;
|
|
68
|
+
}
|
|
69
|
+
static parse(s) {
|
|
70
|
+
const zIndex = s.indexOf('Z');
|
|
71
|
+
const wallTime = new Date(s.slice(0, zIndex + 1)).getTime();
|
|
72
|
+
const rest = s.slice(zIndex + 2);
|
|
73
|
+
const colonIndex = rest.indexOf(':');
|
|
74
|
+
const counter = Number.parseInt(rest.slice(0, colonIndex), 10);
|
|
75
|
+
const nodeID = rest.slice(colonIndex + 1);
|
|
76
|
+
return {
|
|
77
|
+
wallTime,
|
|
78
|
+
counter,
|
|
79
|
+
nodeID
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
static compare(a, b) {
|
|
83
|
+
if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;
|
|
84
|
+
if (a.counter !== b.counter) return a.counter - b.counter;
|
|
85
|
+
return a.nodeID < b.nodeID ? -1 : a.nodeID > b.nodeID ? 1 : 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/hlc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "see LICENSE.md",
|
|
3
|
+
"version": "0.12.0",
|
|
5
4
|
"keywords": [],
|
|
5
|
+
"license": "see LICENSE.md",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"type": "module",
|
|
7
|
-
"main": "lib/index.js",
|
|
8
|
-
"types": "lib/index.d.ts",
|
|
9
8
|
"exports": {
|
|
10
9
|
".": "./lib/index.js"
|
|
11
10
|
},
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"types": "lib/index.d.ts",
|
|
12
13
|
"files": [
|
|
13
14
|
"lib/*",
|
|
14
15
|
"LICENSE.md"
|
|
15
16
|
],
|
|
16
|
-
"sideEffects": false,
|
|
17
17
|
"scripts": {
|
|
18
|
+
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
18
19
|
"build:clean": "del lib",
|
|
19
|
-
"build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
|
|
20
|
+
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
20
21
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
21
22
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
22
|
-
"
|
|
23
|
+
"test": "pnpm run test:types && pnpm run test:unit",
|
|
23
24
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|
|
24
|
-
"test:unit": "vitest run"
|
|
25
|
-
"test": "pnpm run test:types && pnpm run test:unit"
|
|
25
|
+
"test:unit": "vitest run"
|
|
26
26
|
}
|
|
27
27
|
}
|