uniku 0.0.11 → 0.0.13
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/README.md +17 -12
- package/build/{bytes-BBjKLOBR.mjs → bytes-NRtBOHK5.mjs} +1 -1
- package/build/{bytes-BBjKLOBR.mjs.map → bytes-NRtBOHK5.mjs.map} +1 -1
- package/build/cuid2/cuid2.d.mts.map +1 -1
- package/build/cuid2/cuid2.mjs +1 -1
- package/build/cuid2/cuid2.mjs.map +1 -1
- package/build/ksuid/ksuid.d.mts.map +1 -1
- package/build/ksuid/ksuid.mjs +1 -1
- package/build/ksuid/ksuid.mjs.map +1 -1
- package/build/nanoid/nanoid.d.mts.map +1 -1
- package/build/nanoid/nanoid.mjs.map +1 -1
- package/build/random-CWCVvNLb.mjs +2 -0
- package/build/random-CWCVvNLb.mjs.map +1 -0
- package/build/ulid/ulid.d.mts.map +1 -1
- package/build/ulid/ulid.mjs +1 -1
- package/build/ulid/ulid.mjs.map +1 -1
- package/build/uuid/v4.mjs +1 -1
- package/build/uuid/v7.d.mts.map +1 -1
- package/build/uuid/v7.mjs +1 -1
- package/build/uuid/v7.mjs.map +1 -1
- package/package.json +13 -30
- package/src/common/bytes.ts +59 -0
- package/src/common/random.ts +66 -0
- package/src/cuid2/cuid2.ts +211 -0
- package/src/errors.ts +56 -0
- package/src/ksuid/base62.ts +103 -0
- package/src/ksuid/ksuid.ts +195 -0
- package/src/nanoid/nanoid.ts +248 -0
- package/src/ulid/crockford.ts +249 -0
- package/src/ulid/ulid.ts +184 -0
- package/src/uuid/common/hex.ts +24 -0
- package/src/uuid/common/uuid.ts +157 -0
- package/src/uuid/v4.ts +119 -0
- package/src/uuid/v7.ts +182 -0
- package/build/random-DcKA4vF7.mjs +0 -2
- package/build/random-DcKA4vF7.mjs.map +0 -1
package/src/ulid/ulid.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { incrementBytesInPlace, writeTimestamp48 } from '../common/bytes'
|
|
2
|
+
import { rng } from '../common/random'
|
|
3
|
+
import { BufferError, InvalidInputError } from '../errors'
|
|
4
|
+
import { bytesToUlid, decodeToBytes, decodeUlidTime, encodeRandom, encodeTime } from './crockford'
|
|
5
|
+
|
|
6
|
+
export type UlidOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* 16 bytes of random data to use for ULID generation.
|
|
9
|
+
* Only the first 10 bytes are used.
|
|
10
|
+
*/
|
|
11
|
+
random?: Uint8Array
|
|
12
|
+
/**
|
|
13
|
+
* Timestamp in milliseconds since Unix epoch.
|
|
14
|
+
* Defaults to Date.now().
|
|
15
|
+
*/
|
|
16
|
+
msecs?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Ulid = {
|
|
20
|
+
(): string
|
|
21
|
+
<TBuf extends Uint8Array = Uint8Array>(options: UlidOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
22
|
+
(options?: UlidOptions, buf?: undefined, offset?: number): string
|
|
23
|
+
toBytes(id: string): Uint8Array
|
|
24
|
+
fromBytes(bytes: Uint8Array): string
|
|
25
|
+
timestamp(id: string): number
|
|
26
|
+
isValid(id: unknown): id is string
|
|
27
|
+
/** The nil ULID (all zeros) */
|
|
28
|
+
NIL: string
|
|
29
|
+
/** The max ULID (maximum valid value) */
|
|
30
|
+
MAX: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validation regex: first char [0-7] to prevent overflow, rest from Crockford alphabet
|
|
34
|
+
const ULID_REGEX = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i
|
|
35
|
+
|
|
36
|
+
type UlidState = {
|
|
37
|
+
msecs: number
|
|
38
|
+
lastRandom: Uint8Array
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Module-level state for maintaining monotonic ordering within the same millisecond.
|
|
43
|
+
*
|
|
44
|
+
* IMPORTANT: This state persists across all ulid() calls in the module's lifetime.
|
|
45
|
+
* - In serverless/edge functions with warm starts, state persists between invocations.
|
|
46
|
+
* - For isolated state, pass explicit `msecs` and `random` via options.
|
|
47
|
+
* - Tests should mock Date.now() or provide explicit options for deterministic behavior.
|
|
48
|
+
*/
|
|
49
|
+
const state: UlidState = {
|
|
50
|
+
msecs: -Infinity,
|
|
51
|
+
lastRandom: new Uint8Array(10),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ulidBytes(time: number, random: Uint8Array, buf?: Uint8Array, offset = 0): Uint8Array {
|
|
55
|
+
if (!buf) {
|
|
56
|
+
buf = new Uint8Array(16)
|
|
57
|
+
offset = 0
|
|
58
|
+
} else if (offset < 0 || offset + 16 > buf.length) {
|
|
59
|
+
throw new BufferError(
|
|
60
|
+
'ULID_BUFFER_OUT_OF_BOUNDS',
|
|
61
|
+
`ULID byte range ${offset}:${offset + 15} is out of buffer bounds`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Timestamp (48-bit big-endian milliseconds since Unix epoch) -> bytes 0-5
|
|
66
|
+
writeTimestamp48(buf, offset, time)
|
|
67
|
+
|
|
68
|
+
// Random (80 bits) -> bytes 6-15
|
|
69
|
+
for (let i = 0; i < 10; i += 1) {
|
|
70
|
+
buf[offset + 6 + i] = random[i]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return buf
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Overload: no buffer => return a ULID string.
|
|
78
|
+
*/
|
|
79
|
+
function ulidFn(options?: UlidOptions, buf?: undefined, offset?: number): string
|
|
80
|
+
/*
|
|
81
|
+
* Overload: caller provides a buffer slice to fill with ULID bytes.
|
|
82
|
+
*/
|
|
83
|
+
function ulidFn<TBuf extends Uint8Array = Uint8Array>(
|
|
84
|
+
options: UlidOptions | undefined,
|
|
85
|
+
buf: TBuf,
|
|
86
|
+
offset?: number,
|
|
87
|
+
): TBuf
|
|
88
|
+
function ulidFn<TBuf extends Uint8Array = Uint8Array>(options?: UlidOptions, buf?: TBuf, offset = 0): string | TBuf {
|
|
89
|
+
let time: number
|
|
90
|
+
let random: Uint8Array
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Note: by default, Cloudflare Workers "freezes" time during request handling to prevent
|
|
94
|
+
* side-channel attacks. This means that Date.now() will return the same value for the entire
|
|
95
|
+
* duration of a request.
|
|
96
|
+
* Implications:
|
|
97
|
+
* - all ULIDs generated within a single request will have the same timestamp.
|
|
98
|
+
* - the monotonic ordering will rely entirely on incrementing the random portion.
|
|
99
|
+
*/
|
|
100
|
+
const defaultTime = Date.now()
|
|
101
|
+
|
|
102
|
+
if (options) {
|
|
103
|
+
// Explicit options provided - use them directly without monotonic state
|
|
104
|
+
time = options.msecs ?? defaultTime
|
|
105
|
+
if (options.random) {
|
|
106
|
+
if (options.random.length < 10) {
|
|
107
|
+
throw new InvalidInputError('ULID_RANDOM_BYTES_TOO_SHORT', 'Random bytes length must be >= 10 for ULID')
|
|
108
|
+
}
|
|
109
|
+
random = options.random
|
|
110
|
+
} else {
|
|
111
|
+
random = rng()
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
time = defaultTime
|
|
115
|
+
|
|
116
|
+
if (time > state.msecs) {
|
|
117
|
+
// New millisecond: generate fresh random
|
|
118
|
+
random = rng()
|
|
119
|
+
state.msecs = time
|
|
120
|
+
state.lastRandom.set(random.subarray(0, 10))
|
|
121
|
+
} else {
|
|
122
|
+
// Same millisecond or clock rollback: preserve last timestamp and increment random portion.
|
|
123
|
+
time = state.msecs
|
|
124
|
+
if (!incrementBytesInPlace(state.lastRandom)) {
|
|
125
|
+
state.lastRandom.fill(0xff)
|
|
126
|
+
throw new InvalidInputError(
|
|
127
|
+
'ULID_RANDOM_OVERFLOW',
|
|
128
|
+
'ULID random component overflowed while preserving monotonic order',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
random = state.lastRandom
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (buf) {
|
|
136
|
+
ulidBytes(time, random, buf, offset)
|
|
137
|
+
return buf
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// String mode: encode directly without buffer allocation
|
|
141
|
+
return encodeTime(time) + encodeRandom(random)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate a ULID string or write the bytes into a buffer.
|
|
146
|
+
*
|
|
147
|
+
* ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit
|
|
148
|
+
* identifier with millisecond timestamp precision and 80 bits of randomness.
|
|
149
|
+
* ULIDs are URL-safe, use Crockford's Base32 encoding, and sort lexicographically
|
|
150
|
+
* by creation time.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { ulid } from 'uniku/ulid'
|
|
155
|
+
*
|
|
156
|
+
* const id = ulid()
|
|
157
|
+
* // => "01HW9T2W9W9YJ3JZ1H4P4M2T8Q"
|
|
158
|
+
*
|
|
159
|
+
* // Extract timestamp
|
|
160
|
+
* const ts = ulid.timestamp(id)
|
|
161
|
+
* console.log(new Date(ts))
|
|
162
|
+
*
|
|
163
|
+
* // Validate
|
|
164
|
+
* ulid.isValid(id) // true
|
|
165
|
+
*
|
|
166
|
+
* // Convert to/from bytes (16 bytes)
|
|
167
|
+
* const bytes = ulid.toBytes(id)
|
|
168
|
+
* const restored = ulid.fromBytes(bytes)
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
function isValid(id: unknown): id is string {
|
|
172
|
+
return typeof id === 'string' && ULID_REGEX.test(id)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const ulid: Ulid = Object.assign(ulidFn, {
|
|
176
|
+
toBytes: (id: string) => decodeToBytes(id),
|
|
177
|
+
fromBytes: (bytes: Uint8Array) => bytesToUlid(bytes),
|
|
178
|
+
timestamp: (id: string) => decodeUlidTime(id),
|
|
179
|
+
isValid,
|
|
180
|
+
NIL: '00000000000000000000000000',
|
|
181
|
+
MAX: '7ZZZZZZZZZZZZZZZZZZZZZZZZZ',
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
export { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// ASCII code ranges for hex character validation
|
|
2
|
+
const ASCII_0 = 48 // '0'
|
|
3
|
+
const ASCII_9 = 57 // '9'
|
|
4
|
+
const ASCII_A = 65 // 'A'
|
|
5
|
+
const ASCII_F = 70 // 'F'
|
|
6
|
+
const ASCII_a = 97 // 'a'
|
|
7
|
+
const ASCII_f = 102 // 'f'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a hex character ASCII code to its numeric value (0-15).
|
|
11
|
+
* Returns -1 for invalid hex characters.
|
|
12
|
+
*/
|
|
13
|
+
export function hexValue(code: number): number {
|
|
14
|
+
if (code >= ASCII_0 && code <= ASCII_9) {
|
|
15
|
+
return code - ASCII_0
|
|
16
|
+
}
|
|
17
|
+
if (code >= ASCII_A && code <= ASCII_F) {
|
|
18
|
+
return code - ASCII_A + 10
|
|
19
|
+
}
|
|
20
|
+
if (code >= ASCII_a && code <= ASCII_f) {
|
|
21
|
+
return code - ASCII_a + 10
|
|
22
|
+
}
|
|
23
|
+
return -1 // Invalid hex character
|
|
24
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { ParseError } from '../../errors'
|
|
2
|
+
import { hexValue } from './hex'
|
|
3
|
+
|
|
4
|
+
const UUID_BYTE_LENGTH = 16
|
|
5
|
+
const UUID_STRING_LENGTH = 36
|
|
6
|
+
|
|
7
|
+
// Mapping from UUID string position to byte index.
|
|
8
|
+
// -1 indicates a dash position that should be skipped.
|
|
9
|
+
// This avoids intermediate string allocations during parsing.
|
|
10
|
+
const UUID_CHAR_TO_BYTE: number[] = [
|
|
11
|
+
0,
|
|
12
|
+
0,
|
|
13
|
+
1,
|
|
14
|
+
1,
|
|
15
|
+
2,
|
|
16
|
+
2,
|
|
17
|
+
3,
|
|
18
|
+
3, // chars 0-7 → bytes 0-3
|
|
19
|
+
-1, // char 8 is '-'
|
|
20
|
+
4,
|
|
21
|
+
4,
|
|
22
|
+
5,
|
|
23
|
+
5, // chars 9-12 → bytes 4-5
|
|
24
|
+
-1, // char 13 is '-'
|
|
25
|
+
6,
|
|
26
|
+
6,
|
|
27
|
+
7,
|
|
28
|
+
7, // chars 14-17 → bytes 6-7
|
|
29
|
+
-1, // char 18 is '-'
|
|
30
|
+
8,
|
|
31
|
+
8,
|
|
32
|
+
9,
|
|
33
|
+
9, // chars 19-22 → bytes 8-9
|
|
34
|
+
-1, // char 23 is '-'
|
|
35
|
+
10,
|
|
36
|
+
10,
|
|
37
|
+
11,
|
|
38
|
+
11,
|
|
39
|
+
12,
|
|
40
|
+
12,
|
|
41
|
+
13,
|
|
42
|
+
13,
|
|
43
|
+
14,
|
|
44
|
+
14,
|
|
45
|
+
15,
|
|
46
|
+
15, // chars 24-35 → bytes 10-15
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
// Whether each position is the high nibble (true) or low nibble (false)
|
|
50
|
+
const UUID_CHAR_IS_HIGH: boolean[] = [
|
|
51
|
+
true,
|
|
52
|
+
false,
|
|
53
|
+
true,
|
|
54
|
+
false,
|
|
55
|
+
true,
|
|
56
|
+
false,
|
|
57
|
+
true,
|
|
58
|
+
false, // chars 0-7
|
|
59
|
+
false, // dash (ignored)
|
|
60
|
+
true,
|
|
61
|
+
false,
|
|
62
|
+
true,
|
|
63
|
+
false, // chars 9-12
|
|
64
|
+
false, // dash
|
|
65
|
+
true,
|
|
66
|
+
false,
|
|
67
|
+
true,
|
|
68
|
+
false, // chars 14-17
|
|
69
|
+
false, // dash
|
|
70
|
+
true,
|
|
71
|
+
false,
|
|
72
|
+
true,
|
|
73
|
+
false, // chars 19-22
|
|
74
|
+
false, // dash
|
|
75
|
+
true,
|
|
76
|
+
false,
|
|
77
|
+
true,
|
|
78
|
+
false,
|
|
79
|
+
true,
|
|
80
|
+
false,
|
|
81
|
+
true,
|
|
82
|
+
false,
|
|
83
|
+
true,
|
|
84
|
+
false,
|
|
85
|
+
true,
|
|
86
|
+
false, // chars 24-35
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
// Pre-computed lookup table for byte-to-hex conversion (0x00 -> "00", 0xff -> "ff")
|
|
90
|
+
//
|
|
91
|
+
// Note: this table must remain defined in the same module as `formatUuid`, otherwise the v8 optimizer
|
|
92
|
+
// will cause a performance drop of ~36%.
|
|
93
|
+
const HEX_TABLE: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
|
|
94
|
+
|
|
95
|
+
export function formatUuid(bytes: Uint8Array): string {
|
|
96
|
+
// Direct string concatenation - optimized for V8's string builder.
|
|
97
|
+
// This approach avoids loop overhead and intermediate allocations.
|
|
98
|
+
// See: https://github.com/uuidjs/uuid/pull/434
|
|
99
|
+
return (
|
|
100
|
+
HEX_TABLE[bytes[0]] +
|
|
101
|
+
HEX_TABLE[bytes[1]] +
|
|
102
|
+
HEX_TABLE[bytes[2]] +
|
|
103
|
+
HEX_TABLE[bytes[3]] +
|
|
104
|
+
'-' +
|
|
105
|
+
HEX_TABLE[bytes[4]] +
|
|
106
|
+
HEX_TABLE[bytes[5]] +
|
|
107
|
+
'-' +
|
|
108
|
+
HEX_TABLE[bytes[6]] +
|
|
109
|
+
HEX_TABLE[bytes[7]] +
|
|
110
|
+
'-' +
|
|
111
|
+
HEX_TABLE[bytes[8]] +
|
|
112
|
+
HEX_TABLE[bytes[9]] +
|
|
113
|
+
'-' +
|
|
114
|
+
HEX_TABLE[bytes[10]] +
|
|
115
|
+
HEX_TABLE[bytes[11]] +
|
|
116
|
+
HEX_TABLE[bytes[12]] +
|
|
117
|
+
HEX_TABLE[bytes[13]] +
|
|
118
|
+
HEX_TABLE[bytes[14]] +
|
|
119
|
+
HEX_TABLE[bytes[15]]
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parseUuid(value: string): Uint8Array {
|
|
124
|
+
if (value.length !== UUID_STRING_LENGTH) {
|
|
125
|
+
throw new ParseError('UUID_INVALID_LENGTH', `UUID string must be 36 characters, got ${value.length}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Validate separator positions directly (more efficient than full loop)
|
|
129
|
+
if (value[8] !== '-' || value[13] !== '-' || value[18] !== '-' || value[23] !== '-') {
|
|
130
|
+
throw new ParseError(
|
|
131
|
+
'UUID_INVALID_SEPARATORS',
|
|
132
|
+
`UUID string has invalid separators at positions 8, 13, 18, 23. Received: "${value}"`,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Parse bytes directly from UUID string without intermediate string allocations.
|
|
137
|
+
// This avoids the 9 allocations (5 slices + 4 concatenations) of the naive approach.
|
|
138
|
+
const bytes = new Uint8Array(UUID_BYTE_LENGTH)
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < UUID_STRING_LENGTH; i += 1) {
|
|
141
|
+
const byteIdx = UUID_CHAR_TO_BYTE[i]
|
|
142
|
+
if (byteIdx === -1) continue // Skip dash positions
|
|
143
|
+
|
|
144
|
+
const nibble = hexValue(value.charCodeAt(i))
|
|
145
|
+
if (nibble === -1) {
|
|
146
|
+
throw new ParseError('UUID_INVALID_HEX_CHAR', `UUID string contains invalid hex character at position ${i}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (UUID_CHAR_IS_HIGH[i]) {
|
|
150
|
+
bytes[byteIdx] = nibble << 4
|
|
151
|
+
} else {
|
|
152
|
+
bytes[byteIdx] |= nibble
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return bytes
|
|
157
|
+
}
|
package/src/uuid/v4.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { rng } from '../common/random'
|
|
2
|
+
import { BufferError, InvalidInputError } from '../errors'
|
|
3
|
+
import { formatUuid, parseUuid } from './common/uuid'
|
|
4
|
+
|
|
5
|
+
const randomUUID = /*@__PURE__*/ globalThis.crypto.randomUUID.bind(globalThis.crypto)
|
|
6
|
+
|
|
7
|
+
export type UuidV4Options = {
|
|
8
|
+
/**
|
|
9
|
+
* 16 bytes of random data to use for UUID generation.
|
|
10
|
+
* Note: Bytes at index 6 and 8 will be modified in-place to set version/variant bits.
|
|
11
|
+
*/
|
|
12
|
+
random?: Uint8Array
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type UuidV4 = {
|
|
16
|
+
(): string
|
|
17
|
+
<TBuf extends Uint8Array = Uint8Array>(options: UuidV4Options | undefined, buf: TBuf, offset?: number): TBuf
|
|
18
|
+
(options?: UuidV4Options, buf?: undefined, offset?: number): string
|
|
19
|
+
toBytes(id: string): Uint8Array
|
|
20
|
+
fromBytes(bytes: Uint8Array): string
|
|
21
|
+
isValid(id: unknown): id is string
|
|
22
|
+
/** The nil UUID (all zeros) */
|
|
23
|
+
NIL: string
|
|
24
|
+
/** The max UUID (all ones) */
|
|
25
|
+
MAX: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
29
|
+
|
|
30
|
+
function v4Bytes(rnds: Uint8Array, buf?: Uint8Array, offset = 0): Uint8Array {
|
|
31
|
+
if (rnds.length < 16) {
|
|
32
|
+
throw new InvalidInputError('UUID_RANDOM_BYTES_TOO_SHORT', 'Random bytes length must be >= 16')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Set RFC 4122 version (4) and variant (10xx) bits.
|
|
36
|
+
// Note: This modifies the input array in-place.
|
|
37
|
+
rnds[6] = (rnds[6] & 0x0f) | 0x40
|
|
38
|
+
rnds[8] = (rnds[8] & 0x3f) | 0x80
|
|
39
|
+
|
|
40
|
+
if (!buf) {
|
|
41
|
+
// No output buffer provided - return the modified random bytes directly
|
|
42
|
+
return rnds
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (offset < 0 || offset + 16 > buf.length) {
|
|
46
|
+
throw new BufferError(
|
|
47
|
+
'UUID_BUFFER_OUT_OF_BOUNDS',
|
|
48
|
+
`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Copy 16 UUID bytes into the provided buffer slice.
|
|
53
|
+
for (let i = 0; i < 16; i += 1) {
|
|
54
|
+
buf[offset + i] = rnds[i]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return buf
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* Overload: no buffer => return a UUID string.
|
|
62
|
+
*/
|
|
63
|
+
function v4(options?: UuidV4Options, buf?: undefined, offset?: number): string
|
|
64
|
+
/*
|
|
65
|
+
* Overload: caller provides a buffer slice to fill with UUID bytes.
|
|
66
|
+
*/
|
|
67
|
+
function v4<TBuf extends Uint8Array = Uint8Array>(options: UuidV4Options | undefined, buf: TBuf, offset?: number): TBuf
|
|
68
|
+
function v4<TBuf extends Uint8Array = Uint8Array>(options?: UuidV4Options, buf?: TBuf, offset?: number): string | TBuf {
|
|
69
|
+
if (!buf && !options) {
|
|
70
|
+
return randomUUID()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return _v4(options, buf, offset)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _v4<TBuf extends Uint8Array = Uint8Array>(
|
|
77
|
+
options?: UuidV4Options,
|
|
78
|
+
buf?: TBuf,
|
|
79
|
+
offset?: number,
|
|
80
|
+
): string | TBuf {
|
|
81
|
+
const bytes = v4Bytes(options?.random ?? rng(), buf, offset)
|
|
82
|
+
return buf ?? formatUuid(bytes)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isValid(id: unknown): id is string {
|
|
86
|
+
return typeof id === 'string' && UUID_V4_REGEX.test(id)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generate a UUID v4 string or write the bytes into a buffer.
|
|
91
|
+
*
|
|
92
|
+
* UUID v4 is a purely random UUID with 122 bits of entropy. It's the most
|
|
93
|
+
* widely compatible UUID format, supported by virtually all databases and systems.
|
|
94
|
+
* Use when you need maximum compatibility and don't require time-ordering.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* import { uuidv4 } from 'uniku/uuid/v4'
|
|
99
|
+
*
|
|
100
|
+
* const id = uuidv4()
|
|
101
|
+
* // => "550e8400-e29b-41d4-a716-446655440000"
|
|
102
|
+
*
|
|
103
|
+
* // Validate
|
|
104
|
+
* uuidv4.isValid(id) // true
|
|
105
|
+
*
|
|
106
|
+
* // Convert to/from bytes (16 bytes)
|
|
107
|
+
* const bytes = uuidv4.toBytes(id)
|
|
108
|
+
* const restored = uuidv4.fromBytes(bytes)
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export const uuidv4: UuidV4 = Object.assign(v4, {
|
|
112
|
+
toBytes: parseUuid,
|
|
113
|
+
fromBytes: formatUuid,
|
|
114
|
+
isValid,
|
|
115
|
+
NIL: '00000000-0000-0000-0000-000000000000',
|
|
116
|
+
MAX: 'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'
|
package/src/uuid/v7.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { rng } from '../common/random'
|
|
2
|
+
import { BufferError, InvalidInputError } from '../errors'
|
|
3
|
+
import { formatUuid, parseUuid } from './common/uuid'
|
|
4
|
+
|
|
5
|
+
export type UuidV7Options = {
|
|
6
|
+
/**
|
|
7
|
+
* 16 bytes of random data to use for UUID generation.
|
|
8
|
+
* Note: Several bytes will be overwritten with timestamp, version, and variant data.
|
|
9
|
+
*/
|
|
10
|
+
random?: Uint8Array
|
|
11
|
+
msecs?: number
|
|
12
|
+
seq?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type UuidV7 = {
|
|
16
|
+
(): string
|
|
17
|
+
<TBuf extends Uint8Array = Uint8Array>(options: UuidV7Options | undefined, buf: TBuf, offset?: number): TBuf
|
|
18
|
+
(options?: UuidV7Options, buf?: undefined, offset?: number): string
|
|
19
|
+
toBytes(id: string): Uint8Array
|
|
20
|
+
fromBytes(bytes: Uint8Array): string
|
|
21
|
+
timestamp(id: string): number
|
|
22
|
+
isValid(id: unknown): id is string
|
|
23
|
+
/** The nil UUID (all zeros) */
|
|
24
|
+
NIL: string
|
|
25
|
+
/** The max UUID (all ones) */
|
|
26
|
+
MAX: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const UUID_V7_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
30
|
+
|
|
31
|
+
// Reusable buffer for string output path - avoids allocation per call.
|
|
32
|
+
// Safe because bytes are consumed synchronously by formatUuid().
|
|
33
|
+
const reusableBuf = new Uint8Array(16)
|
|
34
|
+
|
|
35
|
+
type V7State = {
|
|
36
|
+
msecs: number
|
|
37
|
+
seq: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Module-level state for maintaining monotonic ordering within the same millisecond.
|
|
42
|
+
*
|
|
43
|
+
* IMPORTANT: This state persists across all uuidv7() calls in the module's lifetime.
|
|
44
|
+
* - In serverless/edge functions with warm starts, state persists between invocations.
|
|
45
|
+
* - For isolated state, pass explicit `msecs` and `seq` via options.
|
|
46
|
+
* - Tests should mock Date.now() or provide explicit options for deterministic behavior.
|
|
47
|
+
*/
|
|
48
|
+
const state: V7State = { msecs: -Infinity, seq: 0 }
|
|
49
|
+
|
|
50
|
+
function v7Bytes(
|
|
51
|
+
rnds: Uint8Array,
|
|
52
|
+
msecs: number | undefined,
|
|
53
|
+
seq: number | undefined,
|
|
54
|
+
buf: Uint8Array,
|
|
55
|
+
offset = 0,
|
|
56
|
+
): Uint8Array {
|
|
57
|
+
if (rnds.length < 16) {
|
|
58
|
+
throw new InvalidInputError('UUID_RANDOM_BYTES_TOO_SHORT', 'Random bytes length must be >= 16')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (offset < 0 || offset + 16 > buf.length) {
|
|
62
|
+
throw new BufferError(
|
|
63
|
+
'UUID_BUFFER_OUT_OF_BOUNDS',
|
|
64
|
+
`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
msecs ??= Date.now()
|
|
69
|
+
// Derive a 31-bit sequence if not provided by the caller.
|
|
70
|
+
// Uses the same formula as the hot path for consistency.
|
|
71
|
+
seq ??= (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9]
|
|
72
|
+
|
|
73
|
+
// Keep this inline on the UUID v7 hot path. Using writeTimestamp48() here
|
|
74
|
+
// benchmarked slower than the direct byte writes.
|
|
75
|
+
buf[offset++] = (msecs / 0x10000000000) & 0xff
|
|
76
|
+
buf[offset++] = (msecs / 0x100000000) & 0xff
|
|
77
|
+
buf[offset++] = (msecs / 0x1000000) & 0xff
|
|
78
|
+
buf[offset++] = (msecs / 0x10000) & 0xff
|
|
79
|
+
buf[offset++] = (msecs / 0x100) & 0xff
|
|
80
|
+
buf[offset++] = msecs & 0xff
|
|
81
|
+
|
|
82
|
+
// Set version (7) and variant (10xx), then pack sequence and random tail bytes.
|
|
83
|
+
buf[offset++] = 0x70 | ((seq >>> 28) & 0x0f)
|
|
84
|
+
buf[offset++] = (seq >>> 20) & 0xff
|
|
85
|
+
buf[offset++] = 0x80 | ((seq >>> 14) & 0x3f)
|
|
86
|
+
buf[offset++] = (seq >>> 6) & 0xff
|
|
87
|
+
// Lower seq bits plus 2 random bits to complete the 128-bit payload.
|
|
88
|
+
buf[offset++] = ((seq << 2) & 0xff) | (rnds[10] & 0x03)
|
|
89
|
+
buf[offset++] = rnds[11]
|
|
90
|
+
buf[offset++] = rnds[12]
|
|
91
|
+
buf[offset++] = rnds[13]
|
|
92
|
+
buf[offset++] = rnds[14]
|
|
93
|
+
buf[offset++] = rnds[15]
|
|
94
|
+
|
|
95
|
+
return buf
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* Overload: no buffer => return a UUID string.
|
|
100
|
+
*/
|
|
101
|
+
function v7(options?: UuidV7Options, buf?: undefined, offset?: number): string
|
|
102
|
+
/*
|
|
103
|
+
* Overload: caller provides a buffer slice to fill with UUID bytes.
|
|
104
|
+
*/
|
|
105
|
+
function v7<TBuf extends Uint8Array = Uint8Array>(options: UuidV7Options | undefined, buf: TBuf, offset?: number): TBuf
|
|
106
|
+
function v7<TBuf extends Uint8Array = Uint8Array>(options?: UuidV7Options, buf?: TBuf, offset?: number): string | TBuf {
|
|
107
|
+
let bytes: Uint8Array
|
|
108
|
+
|
|
109
|
+
if (options) {
|
|
110
|
+
bytes = v7Bytes(options.random ?? rng(), options.msecs, options.seq, buf ?? reusableBuf, buf ? offset : 0)
|
|
111
|
+
} else {
|
|
112
|
+
// HOT PATH: Inline state management and byte generation for best performance
|
|
113
|
+
const now = Date.now()
|
|
114
|
+
const rnds = rng()
|
|
115
|
+
|
|
116
|
+
// Update state (inlined for performance)
|
|
117
|
+
if (now > state.msecs) {
|
|
118
|
+
state.seq = (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9]
|
|
119
|
+
state.msecs = now
|
|
120
|
+
} else {
|
|
121
|
+
state.seq = (state.seq + 1) | 0
|
|
122
|
+
if (state.seq < 0) {
|
|
123
|
+
state.seq = 0
|
|
124
|
+
state.msecs++
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
bytes = v7Bytes(rnds, state.msecs, state.seq, buf ?? reusableBuf, buf ? offset : 0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return buf ? (bytes as TBuf) : formatUuid(bytes)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function timestamp(id: string): number {
|
|
135
|
+
const bytes = parseUuid(id)
|
|
136
|
+
let msecs = 0
|
|
137
|
+
for (let i = 0; i < 6; i += 1) {
|
|
138
|
+
msecs = msecs * 256 + bytes[i]
|
|
139
|
+
}
|
|
140
|
+
return msecs
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isValid(id: unknown): id is string {
|
|
144
|
+
return typeof id === 'string' && UUID_V7_REGEX.test(id)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generate a UUID v7 string or write the bytes into a buffer.
|
|
149
|
+
*
|
|
150
|
+
* UUID v7 is a time-ordered UUID that embeds a Unix timestamp in milliseconds,
|
|
151
|
+
* making IDs naturally sortable by creation time. Ideal for database primary keys
|
|
152
|
+
* where chronological ordering improves index performance.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* import { uuidv7 } from 'uniku/uuid/v7'
|
|
157
|
+
*
|
|
158
|
+
* const id = uuidv7()
|
|
159
|
+
* // => "018e5e5c-7c8a-7000-8000-000000000000"
|
|
160
|
+
*
|
|
161
|
+
* // Extract timestamp
|
|
162
|
+
* const ts = uuidv7.timestamp(id)
|
|
163
|
+
* console.log(new Date(ts))
|
|
164
|
+
*
|
|
165
|
+
* // Validate
|
|
166
|
+
* uuidv7.isValid(id) // true
|
|
167
|
+
*
|
|
168
|
+
* // Convert to/from bytes
|
|
169
|
+
* const bytes = uuidv7.toBytes(id)
|
|
170
|
+
* const restored = uuidv7.fromBytes(bytes)
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export const uuidv7: UuidV7 = Object.assign(v7, {
|
|
174
|
+
toBytes: parseUuid,
|
|
175
|
+
fromBytes: formatUuid,
|
|
176
|
+
timestamp,
|
|
177
|
+
isValid,
|
|
178
|
+
NIL: '00000000-0000-0000-0000-000000000000',
|
|
179
|
+
MAX: 'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
export { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"random-DcKA4vF7.mjs","names":[],"sources":["../src/common/random.ts"],"sourcesContent":["const randomFill = /*@__PURE__*/ globalThis.crypto.getRandomValues.bind(globalThis.crypto)\n\n/**\n * Simple, fast random byte pool for ID generation.\n * Uses plain JavaScript for maximum performance - no Atomics overhead.\n * Thread safety is not needed for typical ID generation use cases.\n */\n\nconst POOL_SIZE = 256\nconst rnds8Pool = new Uint8Array(POOL_SIZE)\nlet poolPtr = POOL_SIZE // Start exhausted to trigger first fill\n\n/**\n * Generate 16 bytes of cryptographically strong random data.\n * Uses a pre-filled pool to minimize crypto API calls.\n */\nexport function rng(): Uint8Array {\n if (poolPtr > POOL_SIZE - 16) {\n randomFill(rnds8Pool)\n poolPtr = 0\n }\n // Return a view - consumed synchronously within the same call\n const start = poolPtr\n poolPtr += 16\n return rnds8Pool.subarray(start, poolPtr)\n}\n"],"mappings":"AAAA,MAAM,EAA2B,WAAW,OAAO,gBAAgB,KAAK,WAAW,OAAO,CASpF,EAAY,IAAI,WAAW,IAAU,CAC3C,IAAI,EAAU,IAMd,SAAgB,GAAkB,CAC5B,EAAU,MACZ,EAAW,EAAU,CACrB,EAAU,GAGZ,IAAM,EAAQ,EAEd,MADA,IAAW,GACJ,EAAU,SAAS,EAAO,EAAQ"}
|