Skip to content

Commit fa20f9c

Browse files
committed
refactor: clearer custom timestamp generation logic
1 parent d4ed041 commit fa20f9c

File tree

3 files changed

+110
-55
lines changed

3 files changed

+110
-55
lines changed

src/index.ts

+16-55
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,13 @@
1-
function addHyphens(id: string) {
2-
return (
3-
id.substring(0, 8) +
4-
"-" +
5-
id.substring(8, 12) +
6-
"-" +
7-
id.substring(12, 16) +
8-
"-" +
9-
id.substring(16, 20) +
10-
"-" +
11-
id.substring(20)
12-
);
13-
}
14-
15-
// Generates the 12 [rand_a] and 62 bits [rand_b] parts in number and bigint formats.
16-
function genRandParts() {
17-
const v4 = crypto.randomUUID();
18-
19-
return {
20-
randA: parseInt(v4.slice(15, 18), 16),
21-
randB: BigInt("0x" + v4.replace(/-/g, "")) & ((1n << 62n) - 1n),
22-
};
23-
}
1+
import { TimestampUUIDv7 } from "./timestamp-uuid";
2+
import { addHyphens, genRandParts } from "./utils";
243

254
export class UUIDv7 {
265
#lastTimestamp: number = -1;
27-
#lastCustomTimestamp: number = -1;
286
#lastRandA: number;
297
#lastRandB: bigint;
308
#lastUUID: bigint = -1n;
319
#encodeAlphabet: string;
10+
#timestampUUID: TimestampUUIDv7;
3211

3312
/**
3413
* Generates a new `UUIDv7` instance.
@@ -46,6 +25,7 @@ export class UUIDv7 {
4625
}
4726

4827
this.#encodeAlphabet = opts?.encodeAlphabet ?? "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
28+
this.#timestampUUID = new TimestampUUIDv7();
4929
}
5030

5131
/**
@@ -56,29 +36,25 @@ export class UUIDv7 {
5636
gen(customTimestamp?: number) {
5737
const hasCustomTimestamp = typeof customTimestamp === "number";
5838

59-
if (hasCustomTimestamp && (customTimestamp < 0 || customTimestamp > 2 ** 48 - 1)) {
60-
throw new Error("uuidv7 gen error: custom timestamp must be between 0 and 2 ** 48 - 1");
39+
if (hasCustomTimestamp) {
40+
return this.#timestampUUID.gen(customTimestamp);
6141
}
6242

6343
let uuid = this.#lastUUID;
6444

6545
while (this.#lastUUID >= uuid) {
66-
const timestamp = hasCustomTimestamp ? customTimestamp : Date.now();
46+
const timestamp = Date.now();
6747

6848
let randA: number;
6949
let randB: bigint;
7050

71-
// Generate new [rand_a] and [rand_b] parts if (or):
72-
// - custom timestamp is provided and is different from the last custom stored one;
73-
// - custom timestamp is not provided and current one is ahead of the last stored one.
74-
if (hasCustomTimestamp ? timestamp !== this.#lastCustomTimestamp : timestamp > this.#lastTimestamp) {
51+
// Generate new [rand_a] and [rand_b] parts if current timestamp one is ahead of the last stored one.
52+
if (timestamp > this.#lastTimestamp) {
7553
const parts = genRandParts();
7654
randA = parts.randA;
7755
randB = parts.randB;
78-
} else if (!hasCustomTimestamp && timestamp < this.#lastTimestamp) {
79-
// If custom timestamp is not provided and current timestamp is behind the last stored one,
80-
// it means that the system clock went backwards. So wait until it goes ahead before generating new UUIDs.
81-
// If custom timestamp is provided, this doesn't matter, since timestamp is always fixed.
56+
} else if (timestamp < this.#lastTimestamp) {
57+
// The system clock went backwards. So wait until it goes ahead before generating new UUIDs.
8258
continue;
8359
} else {
8460
// Otherwise, current timestamp is the same as the previous stored one.
@@ -101,16 +77,10 @@ export class UUIDv7 {
10177
randA = randA + 1;
10278

10379
// If the [rand_a] part overflows its 12 bits,
80+
// Skip this loop iteration, since both [rand_a] and [rand_b] counters have overflowed.
81+
// This ensures monotonicity per instance.
10482
if (randA > 2 ** 12 - 1) {
105-
// if custom timestamp is provided, generate new [rand_a] part.
106-
// This breaks monotonicity but keeps custom timestamp the same and generates a new ID.
107-
if (hasCustomTimestamp) {
108-
randA = genRandParts().randA;
109-
} else {
110-
// if custom timestamp is not provided, skip this loop iteration, since both
111-
// [rand_a] and [rand_b] counters have overflowed. This ensures monotonicity per instance.
112-
continue;
113-
}
83+
continue;
11484
}
11585
}
11686
}
@@ -133,19 +103,10 @@ export class UUIDv7 {
133103
this.#lastRandA = randA;
134104
this.#lastRandB = randB;
135105

136-
// If custom timestamp is provided, always break the loop, since a valid UUIDv7 for this timestamp
137-
// was generated.
138-
if (hasCustomTimestamp) {
139-
this.#lastCustomTimestamp = timestamp;
140-
break;
141-
} else {
142-
this.#lastTimestamp = timestamp;
143-
}
106+
this.#lastTimestamp = timestamp;
144107
}
145108

146-
if (!hasCustomTimestamp) {
147-
this.#lastUUID = uuid;
148-
}
109+
this.#lastUUID = uuid;
149110

150111
return addHyphens(uuid.toString(16).padStart(32, "0"));
151112
}

src/timestamp-uuid.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { addHyphens, genRandParts } from "./utils";
2+
3+
export class TimestampUUIDv7 {
4+
#lastTimestamp: number = -1;
5+
#lastRandA: number;
6+
#lastRandB: bigint;
7+
8+
/**
9+
* Generates a new UUIDv7 with custom timestamp.
10+
*
11+
* @param timestamp Custom timestamp in milliseconds
12+
* @returns
13+
*/
14+
gen(timestamp: number) {
15+
if (timestamp < 0 || timestamp > 2 ** 48 - 1) {
16+
throw new Error("uuidv7 gen error: custom timestamp must be between 0 and 2 ** 48 - 1");
17+
}
18+
19+
let uuid = 0n;
20+
let randA: number;
21+
let randB: bigint;
22+
23+
if (timestamp !== this.#lastTimestamp) {
24+
const parts = genRandParts();
25+
randA = parts.randA;
26+
randB = parts.randB;
27+
} else {
28+
// Keep the same [rand_a] part by default.
29+
randA = this.#lastRandA;
30+
31+
// Random increment value is between 1 and 2 ** 32 (4,294,967,296).
32+
randB = this.#lastRandB + BigInt(crypto.getRandomValues(new Uint32Array(1))[0]! + 1);
33+
34+
// In the rare case that [rand_b] overflows its 62 bits after the increment,
35+
if (randB > 2n ** 62n - 1n) {
36+
const newParts = genRandParts();
37+
// When [rand_b] overflows its 62 bits, always generate a new random part for it.
38+
randB = newParts.randB;
39+
40+
// this will use [rand_a] part as an additional counter, incrementing it by 1.
41+
randA = randA + 1;
42+
43+
// If the [rand_a] part overflows its 12 bits, use a new value for it.
44+
if (randA > 2 ** 12 - 1) {
45+
randA = newParts.randA;
46+
}
47+
}
48+
}
49+
50+
// [unix_ts_ms] timestamp in milliseconds - 48 bits
51+
uuid = BigInt(timestamp) << 80n;
52+
53+
// [ver] version "7" - 4 bits
54+
uuid = uuid | (0b0111n << 76n);
55+
56+
// [rand_a] secondary randomly seeded counter - 12 bits
57+
uuid = uuid | (BigInt(randA) << 64n);
58+
59+
// [var] variant 0b10 - 2 bits
60+
uuid = uuid | (0b10n << 62n);
61+
62+
// [rand_b] primary randomly seeded counter - 62 bits
63+
uuid = uuid | randB;
64+
65+
this.#lastRandA = randA;
66+
this.#lastRandB = randB;
67+
this.#lastTimestamp = timestamp;
68+
69+
return addHyphens(uuid.toString(16).padStart(32, "0"));
70+
}
71+
}

src/utils.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export function addHyphens(id: string) {
2+
return (
3+
id.substring(0, 8) +
4+
"-" +
5+
id.substring(8, 12) +
6+
"-" +
7+
id.substring(12, 16) +
8+
"-" +
9+
id.substring(16, 20) +
10+
"-" +
11+
id.substring(20)
12+
);
13+
}
14+
15+
// Generates the 12 [rand_a] and 62 bits [rand_b] parts in number and bigint formats.
16+
export function genRandParts() {
17+
const v4 = crypto.randomUUID();
18+
19+
return {
20+
randA: parseInt(v4.slice(15, 18), 16),
21+
randB: BigInt("0x" + v4.replace(/-/g, "")) & ((1n << 62n) - 1n),
22+
};
23+
}

0 commit comments

Comments
 (0)