-
Notifications
You must be signed in to change notification settings - Fork 149
/
Copy pathuuidv7.ts
268 lines (240 loc) · 8.87 KB
/
uuidv7.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/**
* uuidv7: An experimental implementation of the proposed UUID Version 7
*
* @license Apache-2.0
* @copyright 2021-2023 LiosK
* @packageDocumentation
*
* from https://github.com/LiosK/uuidv7/blob/e501462ea3d23241de13192ceae726956f9b3b7d/src/index.ts
*/
// polyfill for IE11
import { window } from './utils/globals'
import { isNumber, isUndefined } from './utils/type-utils'
if (!Math.trunc) {
Math.trunc = function (v) {
return v < 0 ? Math.ceil(v) : Math.floor(v)
}
}
// polyfill for IE11
if (!Number.isInteger) {
Number.isInteger = function (value) {
return isNumber(value) && isFinite(value) && Math.floor(value) === value
}
}
const DIGITS = '0123456789abcdef'
/** Represents a UUID as a 16-byte byte array. */
export class UUID {
/** @param bytes - The 16-byte byte array representation. */
constructor(readonly bytes: Readonly<Uint8Array>) {
if (bytes.length !== 16) {
throw new TypeError('not 128-bit length')
}
}
/**
* Builds a byte array from UUIDv7 field values.
*
* @param unixTsMs - A 48-bit `unix_ts_ms` field value.
* @param randA - A 12-bit `rand_a` field value.
* @param randBHi - The higher 30 bits of 62-bit `rand_b` field value.
* @param randBLo - The lower 32 bits of 62-bit `rand_b` field value.
*/
static fromFieldsV7(unixTsMs: number, randA: number, randBHi: number, randBLo: number): UUID {
if (
!Number.isInteger(unixTsMs) ||
!Number.isInteger(randA) ||
!Number.isInteger(randBHi) ||
!Number.isInteger(randBLo) ||
unixTsMs < 0 ||
randA < 0 ||
randBHi < 0 ||
randBLo < 0 ||
unixTsMs > 0xffff_ffff_ffff ||
randA > 0xfff ||
randBHi > 0x3fff_ffff ||
randBLo > 0xffff_ffff
) {
throw new RangeError('invalid field value')
}
const bytes = new Uint8Array(16)
bytes[0] = unixTsMs / 2 ** 40
bytes[1] = unixTsMs / 2 ** 32
bytes[2] = unixTsMs / 2 ** 24
bytes[3] = unixTsMs / 2 ** 16
bytes[4] = unixTsMs / 2 ** 8
bytes[5] = unixTsMs
bytes[6] = 0x70 | (randA >>> 8)
bytes[7] = randA
bytes[8] = 0x80 | (randBHi >>> 24)
bytes[9] = randBHi >>> 16
bytes[10] = randBHi >>> 8
bytes[11] = randBHi
bytes[12] = randBLo >>> 24
bytes[13] = randBLo >>> 16
bytes[14] = randBLo >>> 8
bytes[15] = randBLo
return new UUID(bytes)
}
/** @returns The 8-4-4-4-12 canonical hexadecimal string representation. */
toString(): string {
let text = ''
for (let i = 0; i < this.bytes.length; i++) {
text = text + DIGITS.charAt(this.bytes[i] >>> 4) + DIGITS.charAt(this.bytes[i] & 0xf)
if (i === 3 || i === 5 || i === 7 || i === 9) {
text += '-'
}
}
if (text.length !== 36) {
// We saw one customer whose bundling code was mangling the UUID generation
// rather than accept a bad UUID, we throw an error here.
throw new Error('Invalid UUIDv7 was generated')
}
return text
}
/** Creates an object from `this`. */
clone(): UUID {
return new UUID(this.bytes.slice(0))
}
/** Returns true if `this` is equivalent to `other`. */
equals(other: UUID): boolean {
return this.compareTo(other) === 0
}
/**
* Returns a negative integer, zero, or positive integer if `this` is less
* than, equal to, or greater than `other`, respectively.
*/
compareTo(other: UUID): number {
for (let i = 0; i < 16; i++) {
const diff = this.bytes[i] - other.bytes[i]
if (diff !== 0) {
return Math.sign(diff)
}
}
return 0
}
}
/** Encapsulates the monotonic counter state. */
class V7Generator {
private _timestamp = 0
private _counter = 0
private readonly _random = new DefaultRandom()
/**
* Generates a new UUIDv7 object from the current timestamp, or resets the
* generator upon significant timestamp rollback.
*
* This method returns monotonically increasing UUIDs unless the up-to-date
* timestamp is significantly (by ten seconds or more) smaller than the one
* embedded in the immediately preceding UUID. If such a significant clock
* rollback is detected, this method resets the generator and returns a new
* UUID based on the current timestamp.
*/
generate(): UUID {
const value = this.generateOrAbort()
if (!isUndefined(value)) {
return value
} else {
// reset state and resume
this._timestamp = 0
const valueAfterReset = this.generateOrAbort()
if (isUndefined(valueAfterReset)) {
throw new Error('Could not generate UUID after timestamp reset')
}
return valueAfterReset
}
}
/**
* Generates a new UUIDv7 object from the current timestamp, or returns
* `undefined` upon significant timestamp rollback.
*
* This method returns monotonically increasing UUIDs unless the up-to-date
* timestamp is significantly (by ten seconds or more) smaller than the one
* embedded in the immediately preceding UUID. If such a significant clock
* rollback is detected, this method aborts and returns `undefined`.
*/
generateOrAbort(): UUID | undefined {
const MAX_COUNTER = 0x3ff_ffff_ffff
const ROLLBACK_ALLOWANCE = 10_000 // 10 seconds
const ts = Date.now()
if (ts > this._timestamp) {
this._timestamp = ts
this._resetCounter()
} else if (ts + ROLLBACK_ALLOWANCE > this._timestamp) {
// go on with previous timestamp if new one is not much smaller
this._counter++
if (this._counter > MAX_COUNTER) {
// increment timestamp at counter overflow
this._timestamp++
this._resetCounter()
}
} else {
// abort if clock went backwards to unbearable extent
return undefined
}
return UUID.fromFieldsV7(
this._timestamp,
Math.trunc(this._counter / 2 ** 30),
this._counter & (2 ** 30 - 1),
this._random.nextUint32()
)
}
/** Initializes the counter at a 42-bit random integer. */
private _resetCounter(): void {
this._counter = this._random.nextUint32() * 0x400 + (this._random.nextUint32() & 0x3ff)
}
}
/** A global flag to force use of cryptographically strong RNG. */
declare const UUIDV7_DENY_WEAK_RNG: boolean
/** Stores `crypto.getRandomValues()` available in the environment. */
let getRandomValues: <T extends Uint8Array | Uint32Array>(buffer: T) => T = (buffer) => {
// fall back on Math.random() unless the flag is set to true
// TRICKY: don't use the isUndefined method here as can't pass the reference
if (typeof UUIDV7_DENY_WEAK_RNG !== 'undefined' && UUIDV7_DENY_WEAK_RNG) {
throw new Error('no cryptographically strong RNG available')
}
for (let i = 0; i < buffer.length; i++) {
buffer[i] = Math.trunc(Math.random() * 0x1_0000) * 0x1_0000 + Math.trunc(Math.random() * 0x1_0000)
}
return buffer
}
// detect Web Crypto API
if (window && !isUndefined(window.crypto) && crypto.getRandomValues) {
getRandomValues = (buffer) => crypto.getRandomValues(buffer)
}
/**
* Wraps `crypto.getRandomValues()` and compatibles to enable buffering; this
* uses a small buffer by default to avoid unbearable throughput decline in some
* environments as well as the waste of time and space for unused values.
*/
class DefaultRandom {
private readonly _buffer = new Uint32Array(8)
private _cursor = Infinity
nextUint32(): number {
if (this._cursor >= this._buffer.length) {
getRandomValues(this._buffer)
this._cursor = 0
}
return this._buffer[this._cursor++]
}
}
let defaultGenerator: V7Generator | undefined
/**
* Generates a UUIDv7 string.
*
* @returns The 8-4-4-4-12 canonical hexadecimal string representation
* ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
*/
export const uuidv7 = (): string => uuidv7obj().toString()
/** Generates a UUIDv7 object. */
const uuidv7obj = (): UUID => (defaultGenerator || (defaultGenerator = new V7Generator())).generate()
export const uuid7ToTimestampMs = (uuid: string): number => {
// remove hyphens
const hex = uuid.replace(/-/g, '')
// ensure that it's a version 7 UUID
if (hex.length !== 32) {
throw new Error('Not a valid UUID')
}
if (hex[12] !== '7') {
throw new Error('Not a UUIDv7')
}
// the first 6 bytes are the timestamp, which means that we can read only the first 12 hex characters
return parseInt(hex.substring(0, 12), 16)
}