@sc-voice/tools 3.34.0 → 3.35.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/index.mjs +2 -1
- package/package.json +5 -7
- package/src/js/uuidv7.mjs +171 -0
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sc-voice/tools",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.35.0",
|
|
4
4
|
"description": "Utilities for SC-Voice",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"files": [
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
"src"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "
|
|
15
|
-
"test:test": "
|
|
16
|
-
"test:one": "mocha --config test/mocha-config.json --recursive"
|
|
14
|
+
"test": "vitest run --config test/vitest.config.mjs",
|
|
15
|
+
"test:test": "vitest --config test/vitest.config.mjs"
|
|
17
16
|
},
|
|
18
17
|
"repository": {
|
|
19
18
|
"type": "git",
|
|
@@ -30,14 +29,13 @@
|
|
|
30
29
|
"bugs": {
|
|
31
30
|
"url": "https://github.com/sc-voice/nx-scv/issues"
|
|
32
31
|
},
|
|
33
|
-
"homepage": "https://github.com/sc-voice/nx-scv/
|
|
32
|
+
"homepage": "https://github.com/sc-voice/nx-scv/pkg/tools/#readme",
|
|
34
33
|
"devDependencies": {
|
|
35
34
|
"@biomejs/biome": "1.9.4",
|
|
36
35
|
"avro-js": "^1.12.0",
|
|
37
36
|
"deepl-node": "^1.15.0",
|
|
38
37
|
"eslint": "^9.17.0",
|
|
39
|
-
"
|
|
40
|
-
"should": "^13.2.3"
|
|
38
|
+
"@sc-voice/vitest": "^4.0.0"
|
|
41
39
|
},
|
|
42
40
|
"dependencies": {
|
|
43
41
|
"uuid": "^11.1.0"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// MonotonicityState - Ensures successive UUIDs are always strictly increasing
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
class MonotonicityState {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.previousTimestamp = 0n;
|
|
10
|
+
this.sequence = 0;
|
|
11
|
+
this.offset = 0n;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
nextMillisWithSequence(timeIntervalMs) {
|
|
15
|
+
// Convert to BigInt if needed
|
|
16
|
+
const currentTime = BigInt(Math.floor(timeIntervalMs));
|
|
17
|
+
let currentMillis = currentTime + this.offset;
|
|
18
|
+
|
|
19
|
+
if (this.previousTimestamp === currentMillis) {
|
|
20
|
+
// Same millisecond: increment sequence counter
|
|
21
|
+
this.sequence += 1; // Don't mask yet - check for overflow
|
|
22
|
+
} else if (currentMillis < this.previousTimestamp) {
|
|
23
|
+
// Clock went backward: increment sequence and adjust offset
|
|
24
|
+
this.sequence += 1;
|
|
25
|
+
this.offset = this.previousTimestamp - currentMillis;
|
|
26
|
+
currentMillis = this.previousTimestamp;
|
|
27
|
+
} else {
|
|
28
|
+
// Time advanced: reset sequence
|
|
29
|
+
this.offset = 0n;
|
|
30
|
+
this.sequence = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// If sequence overflows 12 bits, increment time and reset sequence
|
|
34
|
+
if (this.sequence > 0xFFF) {
|
|
35
|
+
this.sequence = 0;
|
|
36
|
+
currentMillis += 1n;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Now apply 12-bit mask for the returned value
|
|
40
|
+
const maskedSequence = this.sequence & 0xFFF;
|
|
41
|
+
|
|
42
|
+
this.previousTimestamp = currentMillis;
|
|
43
|
+
return { millis: currentMillis, sequence: maskedSequence };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Global monotonicity state (thread-safe via single JS event loop)
|
|
48
|
+
const monotonicityState = new MonotonicityState();
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// UUIDV7 Class
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
class UUIDV7 {
|
|
55
|
+
/**
|
|
56
|
+
* Creates a monotonically increasing UUIDV7.
|
|
57
|
+
* Two successive calls will always produce different, strictly increasing UUIDs.
|
|
58
|
+
*
|
|
59
|
+
* @returns {string} UUID in format: 019cc8b2-e91e-7000-8669-15ac4a704757
|
|
60
|
+
*/
|
|
61
|
+
static create() {
|
|
62
|
+
const now = Date.now(); // milliseconds since epoch
|
|
63
|
+
const { millis, sequence } = monotonicityState.nextMillisWithSequence(now);
|
|
64
|
+
|
|
65
|
+
// Generate random bytes for the UUID
|
|
66
|
+
const randomBuf = randomBytes(16); // Get 16 random bytes
|
|
67
|
+
|
|
68
|
+
// Build UUID bytes (16 bytes total)
|
|
69
|
+
const uuid = Buffer.alloc(16);
|
|
70
|
+
|
|
71
|
+
// Bytes 0-5: timestamp in milliseconds (48 bits)
|
|
72
|
+
// Convert BigInt to bytes in big-endian
|
|
73
|
+
const millisBigInt = millis;
|
|
74
|
+
uuid[0] = Number((millisBigInt >> 40n) & 0xFFn);
|
|
75
|
+
uuid[1] = Number((millisBigInt >> 32n) & 0xFFn);
|
|
76
|
+
uuid[2] = Number((millisBigInt >> 24n) & 0xFFn);
|
|
77
|
+
uuid[3] = Number((millisBigInt >> 16n) & 0xFFn);
|
|
78
|
+
uuid[4] = Number((millisBigInt >> 8n) & 0xFFn);
|
|
79
|
+
uuid[5] = Number(millisBigInt & 0xFFn);
|
|
80
|
+
|
|
81
|
+
// Bytes 6-8: 12-bit sequence counter + version + variant
|
|
82
|
+
// Place 12-bit sequence as UInt16 big-endian in bytes 6-7, then apply version/variant masks
|
|
83
|
+
// Sequence is 12 bits: bit11-bit0, represented as 0x0000-0x0FFF
|
|
84
|
+
// When converted to big-endian bytes:
|
|
85
|
+
// bytes[6] = (sequence >> 8) & 0xFF (contains bits 11-8 in lower nibble, upper bits are 0)
|
|
86
|
+
// bytes[7] = sequence & 0xFF (contains bits 7-0)
|
|
87
|
+
uuid[6] = ((sequence >> 8) & 0x0F) | 0x70; // Upper 4 bits of sequence + version 0x7
|
|
88
|
+
uuid[7] = sequence & 0xFF; // Lower 8 bits of sequence
|
|
89
|
+
|
|
90
|
+
// Bytes 9-15: random data
|
|
91
|
+
randomBuf.copy(uuid, 9, 0, 7); // Copy 7 bytes from randomBuf[0:7] to uuid[9:16]
|
|
92
|
+
|
|
93
|
+
// Byte 8: variant (2 bits) + remaining random bits
|
|
94
|
+
uuid[8] = (randomBuf[7] & 0x3F) | 0x80; // Keep lower 6 bits of random + variant 0b10
|
|
95
|
+
|
|
96
|
+
return bytesToUuidString(uuid);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Compare two UUIDs lexicographically.
|
|
101
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
102
|
+
*
|
|
103
|
+
* @param {string} a - UUID
|
|
104
|
+
* @param {string} b - UUID
|
|
105
|
+
* @returns {number} -1, 0, or 1
|
|
106
|
+
*/
|
|
107
|
+
static compare(a, b) {
|
|
108
|
+
const aBytes = typeof a === 'string' ? uuidStringToBytes(a) : a;
|
|
109
|
+
const bBytes = typeof b === 'string' ? uuidStringToBytes(b) : b;
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < 16; i++) {
|
|
112
|
+
if (aBytes[i] < bBytes[i]) return -1;
|
|
113
|
+
if (aBytes[i] > bBytes[i]) return 1;
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if UUID a < UUID b
|
|
120
|
+
*
|
|
121
|
+
* @param {string} a - UUID
|
|
122
|
+
* @param {string} b - UUID
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
static isLessThan(a, b) {
|
|
126
|
+
return UUIDV7.compare(a, b) < 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if UUID a > UUID b
|
|
131
|
+
*
|
|
132
|
+
* @param {string} a - UUID
|
|
133
|
+
* @param {string} b - UUID
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
static isGreaterThan(a, b) {
|
|
137
|
+
return UUIDV7.compare(a, b) > 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default UUIDV7;
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// UUID String Formatting
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Convert UUID bytes to standard string format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
149
|
+
*/
|
|
150
|
+
function bytesToUuidString(bytes) {
|
|
151
|
+
const hex = bytes.toString('hex');
|
|
152
|
+
return [
|
|
153
|
+
hex.slice(0, 8),
|
|
154
|
+
hex.slice(8, 12),
|
|
155
|
+
hex.slice(12, 16),
|
|
156
|
+
hex.slice(16, 20),
|
|
157
|
+
hex.slice(20),
|
|
158
|
+
].join('-');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convert UUID string to bytes.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} uuidString - UUID in format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
165
|
+
* @returns {Buffer} 16 bytes
|
|
166
|
+
*/
|
|
167
|
+
function uuidStringToBytes(uuidString) {
|
|
168
|
+
const hex = uuidString.replace(/-/g, '');
|
|
169
|
+
return Buffer.from(hex, 'hex');
|
|
170
|
+
}
|
|
171
|
+
|