uniku 0.3.0 → 0.3.2
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/build/cuid2/cuid2.d.mts +5 -5
- 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 +6 -6
- 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 +2 -2
- package/build/nanoid/nanoid.mjs.map +1 -1
- package/build/objectid/objectid.d.mts +6 -6
- package/build/objectid/objectid.d.mts.map +1 -1
- package/build/objectid/objectid.mjs +1 -1
- package/build/objectid/objectid.mjs.map +1 -1
- package/build/tsid/tsid.d.mts +8 -8
- package/build/tsid/tsid.d.mts.map +1 -1
- package/build/tsid/tsid.mjs +1 -1
- package/build/tsid/tsid.mjs.map +1 -1
- package/build/typeid/typeid.d.mts +8 -8
- package/build/typeid/typeid.d.mts.map +1 -1
- package/build/typeid/typeid.mjs.map +1 -1
- package/build/ulid/ulid.d.mts +10 -6
- 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.d.mts +5 -6
- package/build/uuid/v4.d.mts.map +1 -1
- package/build/uuid/v4.mjs +1 -1
- package/build/uuid/v4.mjs.map +1 -1
- package/build/uuid/v7.d.mts +13 -6
- 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/build/uuid-CnQIYoQi.mjs +2 -0
- package/build/uuid-CnQIYoQi.mjs.map +1 -0
- package/build/validation-CTNpXm94.mjs +2 -0
- package/build/validation-CTNpXm94.mjs.map +1 -0
- package/package.json +1 -1
- package/src/common/validation.ts +7 -0
- package/src/cuid2/cuid2.ts +13 -7
- package/src/ksuid/ksuid.ts +26 -17
- package/src/nanoid/nanoid.ts +3 -3
- package/src/objectid/objectid.ts +32 -17
- package/src/tsid/tsid.ts +92 -53
- package/src/typeid/typeid.ts +9 -0
- package/src/ulid/crockford.ts +2 -2
- package/src/ulid/ulid.ts +46 -24
- package/src/uuid/common/uuid.ts +12 -1
- package/src/uuid/v4.ts +31 -28
- package/src/uuid/v7.ts +86 -47
- package/build/uuid-BBjwHv7J.mjs +0 -2
- package/build/uuid-BBjwHv7J.mjs.map +0 -1
package/src/ksuid/ksuid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeTimestamp32 } from '../common/bytes'
|
|
2
2
|
import { rng } from '../common/random'
|
|
3
|
+
import { isWritableRange } from '../common/validation'
|
|
3
4
|
import { BufferError, InvalidInputError } from '../errors'
|
|
4
5
|
import { decodeBase62, encodeBase62 } from './base62'
|
|
5
6
|
|
|
@@ -44,12 +45,19 @@ export type KsuidOptions = {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export type Ksuid = {
|
|
48
|
+
/** Generate a time-ordered KSUID string. */
|
|
47
49
|
(): string
|
|
50
|
+
/** Generate a KSUID with explicit options or write its 20 canonical bytes into a caller-owned buffer. */
|
|
48
51
|
<TBuf extends Uint8Array = Uint8Array>(options: KsuidOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
52
|
+
/** Generate a KSUID string with optional timestamp or random payload bytes. */
|
|
49
53
|
(options?: KsuidOptions, buf?: undefined, offset?: number): string
|
|
54
|
+
/** Convert a KSUID string to its canonical 20-byte representation. */
|
|
50
55
|
toBytes(id: string): Uint8Array
|
|
56
|
+
/** Convert 20 canonical KSUID bytes to a KSUID string. */
|
|
51
57
|
fromBytes(bytes: Uint8Array): string
|
|
58
|
+
/** Read the embedded Unix timestamp in milliseconds. */
|
|
52
59
|
timestamp(id: string): number
|
|
60
|
+
/** Return whether a value is a syntactically valid KSUID string. */
|
|
53
61
|
isValid(id: unknown): id is string
|
|
54
62
|
/** The nil KSUID (all zeros) */
|
|
55
63
|
NIL: string
|
|
@@ -58,19 +66,9 @@ export type Ksuid = {
|
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
/**
|
|
61
|
-
* Write KSUID
|
|
69
|
+
* Write already-validated KSUID fields to a buffer.
|
|
62
70
|
*/
|
|
63
|
-
function
|
|
64
|
-
if (!buf) {
|
|
65
|
-
buf = new Uint8Array(KSUID_BYTES)
|
|
66
|
-
offset = 0
|
|
67
|
-
} else if (offset < 0 || offset + KSUID_BYTES > buf.length) {
|
|
68
|
-
throw new BufferError(
|
|
69
|
-
'KSUID_BUFFER_OUT_OF_BOUNDS',
|
|
70
|
-
`KSUID byte range ${offset}:${offset + KSUID_BYTES - 1} is out of buffer bounds`,
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
71
|
+
function writeKsuidBytesUnchecked(timestamp: number, payload: Uint8Array, buf: Uint8Array, offset: number): void {
|
|
74
72
|
// Timestamp (32-bit big-endian seconds since KSUID epoch) -> bytes 0-3
|
|
75
73
|
writeTimestamp32(buf, offset, timestamp)
|
|
76
74
|
|
|
@@ -78,8 +76,6 @@ function ksuidBytes(timestamp: number, payload: Uint8Array, buf?: Uint8Array, of
|
|
|
78
76
|
for (let i = 0; i < PAYLOAD_BYTES; i += 1) {
|
|
79
77
|
buf[offset + TIMESTAMP_BYTES + i] = payload[i]
|
|
80
78
|
}
|
|
81
|
-
|
|
82
|
-
return buf
|
|
83
79
|
}
|
|
84
80
|
|
|
85
81
|
/*
|
|
@@ -103,7 +99,7 @@ function ksuidFn<TBuf extends Uint8Array = Uint8Array>(options?: KsuidOptions, b
|
|
|
103
99
|
let timestamp: number
|
|
104
100
|
const secs = options?.secs
|
|
105
101
|
if (secs !== undefined) {
|
|
106
|
-
if (secs < KSUID_EPOCH) {
|
|
102
|
+
if (!Number.isInteger(secs) || secs < KSUID_EPOCH) {
|
|
107
103
|
throw new InvalidInputError('KSUID_TIMESTAMP_TOO_LOW', 'Timestamp must be >= KSUID epoch')
|
|
108
104
|
}
|
|
109
105
|
|
|
@@ -126,11 +122,18 @@ function ksuidFn<TBuf extends Uint8Array = Uint8Array>(options?: KsuidOptions, b
|
|
|
126
122
|
const payload = random ?? rng()
|
|
127
123
|
|
|
128
124
|
if (buf) {
|
|
129
|
-
|
|
125
|
+
if (!isWritableRange(buf, offset, KSUID_BYTES)) {
|
|
126
|
+
throw new BufferError(
|
|
127
|
+
'KSUID_BUFFER_OUT_OF_BOUNDS',
|
|
128
|
+
`KSUID byte range ${offset}:${offset + KSUID_BYTES - 1} is out of buffer bounds`,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
writeKsuidBytesUnchecked(timestamp, payload, buf, offset)
|
|
130
132
|
return buf
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
const bytes =
|
|
135
|
+
const bytes = new Uint8Array(KSUID_BYTES)
|
|
136
|
+
writeKsuidBytesUnchecked(timestamp, payload, bytes, 0)
|
|
134
137
|
|
|
135
138
|
// String mode: create bytes then encode to Base62
|
|
136
139
|
return encodeBase62(bytes)
|
|
@@ -150,6 +153,12 @@ function toBytes(id: string): Uint8Array {
|
|
|
150
153
|
* Convert 20 bytes to a KSUID string.
|
|
151
154
|
*/
|
|
152
155
|
function fromBytes(bytes: Uint8Array): string {
|
|
156
|
+
if (bytes.length !== KSUID_BYTES) {
|
|
157
|
+
throw new BufferError(
|
|
158
|
+
'KSUID_BYTES_INVALID_LENGTH',
|
|
159
|
+
`KSUID bytes must be exactly ${KSUID_BYTES} bytes, got ${bytes.length}`,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
153
162
|
return encodeBase62(bytes)
|
|
154
163
|
}
|
|
155
164
|
|
package/src/nanoid/nanoid.ts
CHANGED
|
@@ -55,11 +55,11 @@ export type NanoidOptions = {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export type Nanoid = {
|
|
58
|
-
/** Generate
|
|
58
|
+
/** Generate a Nanoid with the default URL-safe alphabet and 21-character length. */
|
|
59
59
|
(): string
|
|
60
|
-
/** Generate
|
|
60
|
+
/** Generate a Nanoid with the default alphabet and a custom length. */
|
|
61
61
|
(size: number): string
|
|
62
|
-
/** Generate
|
|
62
|
+
/** Generate a Nanoid with a custom alphabet, length, or deterministic random bytes. */
|
|
63
63
|
(options: NanoidOptions): string
|
|
64
64
|
/**
|
|
65
65
|
* Validate a nanoid string against the default URL-safe alphabet.
|
package/src/objectid/objectid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeTimestamp32 } from '../common/bytes'
|
|
2
2
|
import { randomBytes, randomUint32 } from '../common/random'
|
|
3
|
+
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
3
4
|
import { BufferError, InvalidInputError } from '../errors'
|
|
4
5
|
import { decodeObjectIdHex, encodeObjectIdHex } from './hex'
|
|
5
6
|
|
|
@@ -41,12 +42,19 @@ export type ObjectIdOptions = {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export type ObjectId = {
|
|
45
|
+
/** Generate a MongoDB-compatible ObjectID string. */
|
|
44
46
|
(): string
|
|
47
|
+
/** Generate an ObjectID with explicit options or write its 12 canonical bytes into a caller-owned buffer. */
|
|
45
48
|
<TBuf extends Uint8Array = Uint8Array>(options: ObjectIdOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
49
|
+
/** Generate an ObjectID string with optional timestamp, random field, or counter. */
|
|
46
50
|
(options?: ObjectIdOptions, buf?: undefined, offset?: number): string
|
|
51
|
+
/** Convert an ObjectID string to its canonical 12-byte representation. */
|
|
47
52
|
toBytes(id: string): Uint8Array
|
|
53
|
+
/** Convert 12 canonical ObjectID bytes to an ObjectID string. */
|
|
48
54
|
fromBytes(bytes: Uint8Array): string
|
|
55
|
+
/** Read the embedded Unix timestamp in milliseconds. */
|
|
49
56
|
timestamp(id: string): number
|
|
57
|
+
/** Return whether a value is a syntactically valid ObjectID string. */
|
|
50
58
|
isValid(id: unknown): id is string
|
|
51
59
|
/** The nil ObjectID (all zeros) */
|
|
52
60
|
NIL: string
|
|
@@ -85,17 +93,13 @@ function freshRandom(): Uint8Array {
|
|
|
85
93
|
return bytes
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`ObjectID byte range ${offset}:${offset + OBJECTID_BYTES - 1} is out of buffer bounds`,
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
96
|
+
function writeObjectIdBytesUnchecked(
|
|
97
|
+
secs: number,
|
|
98
|
+
random: Uint8Array,
|
|
99
|
+
counter: number,
|
|
100
|
+
buf: Uint8Array,
|
|
101
|
+
offset: number,
|
|
102
|
+
): void {
|
|
99
103
|
// Timestamp (32-bit big-endian seconds since Unix epoch) -> bytes 0-3
|
|
100
104
|
writeTimestamp32(buf, offset, secs)
|
|
101
105
|
|
|
@@ -108,8 +112,6 @@ function objectIdBytes(secs: number, random: Uint8Array, counter: number, buf?:
|
|
|
108
112
|
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES] = (counter >>> 16) & 0xff
|
|
109
113
|
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES + 1] = (counter >>> 8) & 0xff
|
|
110
114
|
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES + COUNTER_BYTES - 1] = counter & 0xff
|
|
111
|
-
|
|
112
|
-
return buf
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/*
|
|
@@ -143,12 +145,12 @@ function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
const optSecs = options.secs
|
|
146
|
-
if (optSecs !== undefined && (optSecs
|
|
148
|
+
if (optSecs !== undefined && !isIntegerInRange(optSecs, 0, MAX_SECS)) {
|
|
147
149
|
throw new InvalidInputError('OBJECTID_TIMESTAMP_OUT_OF_RANGE', `Timestamp must be between 0 and ${MAX_SECS}`)
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
const optCounter = options.counter
|
|
151
|
-
if (optCounter !== undefined && (optCounter
|
|
153
|
+
if (optCounter !== undefined && !isIntegerInRange(optCounter, 0, MAX_COUNTER)) {
|
|
152
154
|
throw new InvalidInputError('OBJECTID_COUNTER_OUT_OF_RANGE', `Counter must be between 0 and ${MAX_COUNTER}`)
|
|
153
155
|
}
|
|
154
156
|
|
|
@@ -183,11 +185,18 @@ function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
|
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
if (buf) {
|
|
186
|
-
|
|
188
|
+
if (!isWritableRange(buf, offset, OBJECTID_BYTES)) {
|
|
189
|
+
throw new BufferError(
|
|
190
|
+
'OBJECTID_BUFFER_OUT_OF_BOUNDS',
|
|
191
|
+
`ObjectID byte range ${offset}:${offset + OBJECTID_BYTES - 1} is out of buffer bounds`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
writeObjectIdBytesUnchecked(secs, random, counter, buf, offset)
|
|
187
195
|
return buf
|
|
188
196
|
}
|
|
189
197
|
|
|
190
|
-
const bytes =
|
|
198
|
+
const bytes = new Uint8Array(OBJECTID_BYTES)
|
|
199
|
+
writeObjectIdBytesUnchecked(secs, random, counter, bytes, 0)
|
|
191
200
|
return encodeObjectIdHex(bytes)
|
|
192
201
|
}
|
|
193
202
|
|
|
@@ -202,6 +211,12 @@ function toBytes(id: string): Uint8Array {
|
|
|
202
211
|
* Convert 12 bytes to an ObjectID hex string.
|
|
203
212
|
*/
|
|
204
213
|
function fromBytes(bytes: Uint8Array): string {
|
|
214
|
+
if (bytes.length !== OBJECTID_BYTES) {
|
|
215
|
+
throw new BufferError(
|
|
216
|
+
'OBJECTID_BYTES_INVALID_LENGTH',
|
|
217
|
+
`ObjectID bytes must be exactly ${OBJECTID_BYTES} bytes, got ${bytes.length}`,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
205
220
|
return encodeObjectIdHex(bytes)
|
|
206
221
|
}
|
|
207
222
|
|
package/src/tsid/tsid.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUint32 } from '../common/random'
|
|
2
|
+
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
2
3
|
import { BufferError, InvalidInputError } from '../errors'
|
|
3
4
|
import { decodeTsidString, encodeTsidString } from './crockford64'
|
|
4
5
|
|
|
@@ -61,14 +62,23 @@ export type TsidOptions = {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export type Tsid = {
|
|
65
|
+
/** Generate a time-sorted TSID bigint. */
|
|
64
66
|
(): bigint
|
|
67
|
+
/** Generate a TSID with explicit options or write its 8 canonical bytes into a caller-owned buffer. */
|
|
65
68
|
<TBuf extends Uint8Array = Uint8Array>(options: TsidOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
69
|
+
/** Generate a TSID bigint with optional timestamp, epoch, node, or counter controls. */
|
|
66
70
|
(options?: TsidOptions, buf?: undefined, offset?: number): bigint
|
|
71
|
+
/** Convert a TSID bigint to its canonical 8-byte representation. */
|
|
67
72
|
toBytes(id: bigint): Uint8Array
|
|
73
|
+
/** Convert 8 canonical TSID bytes to a TSID bigint. */
|
|
68
74
|
fromBytes(bytes: Uint8Array): bigint
|
|
75
|
+
/** Convert a TSID bigint to its canonical 13-character string. */
|
|
69
76
|
toString(id: bigint): string
|
|
77
|
+
/** Convert a canonical TSID string to a bigint. */
|
|
70
78
|
fromString(str: string): bigint
|
|
79
|
+
/** Read a TSID timestamp in milliseconds, using the supplied or default epoch. */
|
|
71
80
|
timestamp(id: bigint, epoch?: number): number
|
|
81
|
+
/** Return whether a value is a valid 64-bit TSID bigint. */
|
|
72
82
|
isValid(id: unknown): id is bigint
|
|
73
83
|
/** The nil TSID (all zeros) */
|
|
74
84
|
NIL: bigint
|
|
@@ -102,22 +112,60 @@ function pack(msecsDiff: bigint, node: number, counterBits: number, counter: num
|
|
|
102
112
|
return (msecsDiff << BigInt(RANDOM_BITS)) | (BigInt(node) << BigInt(counterBits)) | BigInt(counter)
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
function
|
|
106
|
-
if (!buf) {
|
|
107
|
-
buf = new Uint8Array(TSID_BYTES)
|
|
108
|
-
offset = 0
|
|
109
|
-
} else if (offset < 0 || offset + TSID_BYTES > buf.length) {
|
|
110
|
-
throw new BufferError(
|
|
111
|
-
'TSID_BUFFER_OUT_OF_BOUNDS',
|
|
112
|
-
`TSID byte range ${offset}:${offset + TSID_BYTES - 1} is out of buffer bounds`,
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
115
|
+
function writeTsidBytesUnchecked(value: bigint, buf: Uint8Array, offset: number): void {
|
|
116
116
|
for (let i = 0; i < TSID_BYTES; i += 1) {
|
|
117
117
|
buf[offset + i] = Number((value >> BigInt((TSID_BYTES - 1 - i) * 8)) & 0xffn)
|
|
118
118
|
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type ResolvedTsidOptions = {
|
|
122
|
+
msecsDiff: bigint
|
|
123
|
+
node: number
|
|
124
|
+
counterBits: number
|
|
125
|
+
counter: number
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveTsidOptions(options: TsidOptions): ResolvedTsidOptions {
|
|
129
|
+
const nodeBits = options.nodeBits ?? DEFAULT_NODE_BITS
|
|
130
|
+
if (!isIntegerInRange(nodeBits, 0, MAX_NODE_BITS)) {
|
|
131
|
+
throw new InvalidInputError('TSID_NODE_BITS_OUT_OF_RANGE', `nodeBits must be between 0 and ${MAX_NODE_BITS}`)
|
|
132
|
+
}
|
|
119
133
|
|
|
120
|
-
|
|
134
|
+
const counterBits = RANDOM_BITS - nodeBits
|
|
135
|
+
const nodeMask = (1 << nodeBits) - 1
|
|
136
|
+
const counterMask = (1 << counterBits) - 1
|
|
137
|
+
const optNode = options.node
|
|
138
|
+
if (optNode !== undefined && !isIntegerInRange(optNode, 0, nodeMask)) {
|
|
139
|
+
throw new InvalidInputError('TSID_NODE_OUT_OF_RANGE', `node must be between 0 and ${nodeMask}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const optCounter = options.counter
|
|
143
|
+
if (optCounter !== undefined && !isIntegerInRange(optCounter, 0, counterMask)) {
|
|
144
|
+
throw new InvalidInputError('TSID_COUNTER_OUT_OF_RANGE', `counter must be between 0 and ${counterMask}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const epoch = options.epoch ?? TSID_EPOCH
|
|
148
|
+
if (!Number.isInteger(epoch)) {
|
|
149
|
+
throw new InvalidInputError('TSID_EPOCH_INVALID', 'epoch must be a finite integer')
|
|
150
|
+
}
|
|
151
|
+
const msecs = options.msecs ?? Date.now()
|
|
152
|
+
if (!Number.isInteger(msecs)) {
|
|
153
|
+
throw new InvalidInputError('TSID_TIMESTAMP_INVALID', 'msecs must be a finite integer')
|
|
154
|
+
}
|
|
155
|
+
const msecsDiff = BigInt(msecs) - BigInt(epoch)
|
|
156
|
+
if (msecsDiff < 0n || msecsDiff > MAX_TIMESTAMP_DIFF) {
|
|
157
|
+
throw new InvalidInputError(
|
|
158
|
+
'TSID_TIMESTAMP_OUT_OF_RANGE',
|
|
159
|
+
`msecs - epoch must be between 0 and ${MAX_TIMESTAMP_DIFF}`,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
msecsDiff,
|
|
165
|
+
node: optNode ?? randomUint32() & nodeMask,
|
|
166
|
+
counterBits,
|
|
167
|
+
counter: optCounter ?? randomUint32() & counterMask,
|
|
168
|
+
}
|
|
121
169
|
}
|
|
122
170
|
|
|
123
171
|
/*
|
|
@@ -139,44 +187,11 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
|
|
|
139
187
|
let counter: number
|
|
140
188
|
|
|
141
189
|
if (options) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const nodeMask = (1 << nodeBits) - 1
|
|
148
|
-
const counterMask = (1 << counterBits) - 1
|
|
149
|
-
|
|
150
|
-
const optNode = options.node
|
|
151
|
-
if (optNode !== undefined) {
|
|
152
|
-
if (optNode < 0 || optNode > nodeMask) {
|
|
153
|
-
throw new InvalidInputError('TSID_NODE_OUT_OF_RANGE', `node must be between 0 and ${nodeMask}`)
|
|
154
|
-
}
|
|
155
|
-
node = optNode
|
|
156
|
-
} else {
|
|
157
|
-
node = randomUint32() & nodeMask
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const optCounter = options.counter
|
|
161
|
-
if (optCounter !== undefined) {
|
|
162
|
-
if (optCounter < 0 || optCounter > counterMask) {
|
|
163
|
-
throw new InvalidInputError('TSID_COUNTER_OUT_OF_RANGE', `counter must be between 0 and ${counterMask}`)
|
|
164
|
-
}
|
|
165
|
-
counter = optCounter
|
|
166
|
-
} else {
|
|
167
|
-
counter = randomUint32() & counterMask
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const epoch = options.epoch ?? TSID_EPOCH
|
|
171
|
-
const msecs = options.msecs ?? Date.now()
|
|
172
|
-
const diff = BigInt(msecs) - BigInt(epoch)
|
|
173
|
-
if (diff < 0n || diff > MAX_TIMESTAMP_DIFF) {
|
|
174
|
-
throw new InvalidInputError(
|
|
175
|
-
'TSID_TIMESTAMP_OUT_OF_RANGE',
|
|
176
|
-
`msecs - epoch must be between 0 and ${MAX_TIMESTAMP_DIFF}`,
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
msecsDiff = diff
|
|
190
|
+
const resolved = resolveTsidOptions(options)
|
|
191
|
+
msecsDiff = resolved.msecsDiff
|
|
192
|
+
node = resolved.node
|
|
193
|
+
counterBits = resolved.counterBits
|
|
194
|
+
counter = resolved.counter
|
|
180
195
|
} else {
|
|
181
196
|
// Lazily initialize the persistent node ID on first no-option call.
|
|
182
197
|
if (state.node === undefined) {
|
|
@@ -212,7 +227,13 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
|
|
|
212
227
|
const packed = pack(msecsDiff, node, counterBits, counter)
|
|
213
228
|
|
|
214
229
|
if (buf) {
|
|
215
|
-
|
|
230
|
+
if (!isWritableRange(buf, offset, TSID_BYTES)) {
|
|
231
|
+
throw new BufferError(
|
|
232
|
+
'TSID_BUFFER_OUT_OF_BOUNDS',
|
|
233
|
+
`TSID byte range ${offset}:${offset + TSID_BYTES - 1} is out of buffer bounds`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
writeTsidBytesUnchecked(packed, buf, offset)
|
|
216
237
|
return buf
|
|
217
238
|
}
|
|
218
239
|
|
|
@@ -223,7 +244,10 @@ function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf
|
|
|
223
244
|
* Convert a TSID bigint to 8 bytes.
|
|
224
245
|
*/
|
|
225
246
|
function toBytes(id: bigint): Uint8Array {
|
|
226
|
-
|
|
247
|
+
assertValidTsid(id)
|
|
248
|
+
const bytes = new Uint8Array(TSID_BYTES)
|
|
249
|
+
writeTsidBytesUnchecked(id, bytes, 0)
|
|
250
|
+
return bytes
|
|
227
251
|
}
|
|
228
252
|
|
|
229
253
|
/**
|
|
@@ -250,6 +274,10 @@ function fromBytes(bytes: Uint8Array): bigint {
|
|
|
250
274
|
* `epoch` used at generation time if it was overridden from the default.
|
|
251
275
|
*/
|
|
252
276
|
function timestamp(id: bigint, epoch: number = TSID_EPOCH): number {
|
|
277
|
+
assertValidTsid(id)
|
|
278
|
+
if (!Number.isInteger(epoch)) {
|
|
279
|
+
throw new InvalidInputError('TSID_EPOCH_INVALID', 'epoch must be a finite integer')
|
|
280
|
+
}
|
|
253
281
|
return epoch + Number(id >> BigInt(RANDOM_BITS))
|
|
254
282
|
}
|
|
255
283
|
|
|
@@ -263,6 +291,17 @@ function isValid(id: unknown): id is bigint {
|
|
|
263
291
|
const NIL = 0n
|
|
264
292
|
const MAX = (1n << 64n) - 1n
|
|
265
293
|
|
|
294
|
+
function assertValidTsid(id: bigint): void {
|
|
295
|
+
if (id < NIL || id > MAX) {
|
|
296
|
+
throw new InvalidInputError('TSID_VALUE_OUT_OF_RANGE', `TSID value must be between ${NIL} and ${MAX}`)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function toString(id: bigint): string {
|
|
301
|
+
assertValidTsid(id)
|
|
302
|
+
return encodeTsidString(id)
|
|
303
|
+
}
|
|
304
|
+
|
|
266
305
|
/**
|
|
267
306
|
* Generate a TSID bigint or write the bytes into a buffer.
|
|
268
307
|
*
|
|
@@ -299,7 +338,7 @@ const MAX = (1n << 64n) - 1n
|
|
|
299
338
|
export const tsid: Tsid = Object.assign(tsidFn, {
|
|
300
339
|
toBytes,
|
|
301
340
|
fromBytes,
|
|
302
|
-
toString
|
|
341
|
+
toString,
|
|
303
342
|
fromString: decodeTsidString,
|
|
304
343
|
timestamp,
|
|
305
344
|
isValid,
|
package/src/typeid/typeid.ts
CHANGED
|
@@ -5,14 +5,23 @@ import { uuidv7 } from '../uuid/v7'
|
|
|
5
5
|
export type TypeidOptions = UuidV7Options
|
|
6
6
|
|
|
7
7
|
export type Typeid = {
|
|
8
|
+
/** Generate a TypeID from a lowercase entity prefix and a UUID v7 suffix. */
|
|
8
9
|
(prefix: string, options?: TypeidOptions): string
|
|
10
|
+
/** Convert a TypeID's UUID v7 suffix to its canonical 16-byte representation. */
|
|
9
11
|
toBytes(id: string): Uint8Array
|
|
12
|
+
/** Build a TypeID from a prefix and canonical UUID v7 bytes. */
|
|
10
13
|
fromBytes(prefix: string, bytes: Uint8Array): string
|
|
14
|
+
/** Convert a TypeID to its UUID v7 string. */
|
|
11
15
|
toUuid(id: string): string
|
|
16
|
+
/** Build a TypeID from a prefix and UUID v7 string. */
|
|
12
17
|
fromUuid(prefix: string, uuid: string): string
|
|
18
|
+
/** Read the UUID v7 timestamp embedded in a TypeID, in milliseconds. */
|
|
13
19
|
timestamp(id: string): number
|
|
20
|
+
/** Read a TypeID's entity prefix. */
|
|
14
21
|
prefix(id: string): string
|
|
22
|
+
/** Read a TypeID's UUID v7 suffix. */
|
|
15
23
|
suffix(id: string): string
|
|
24
|
+
/** Return whether a value is a syntactically valid TypeID. */
|
|
16
25
|
isValid(id: unknown): id is string
|
|
17
26
|
}
|
|
18
27
|
|
package/src/ulid/crockford.ts
CHANGED
|
@@ -231,8 +231,8 @@ export function decodeToBytes(str: string): Uint8Array {
|
|
|
231
231
|
* Encode 16 bytes to a 26-character ULID string.
|
|
232
232
|
*/
|
|
233
233
|
export function bytesToUlid(bytes: Uint8Array): string {
|
|
234
|
-
if (bytes.length
|
|
235
|
-
throw new BufferError('
|
|
234
|
+
if (bytes.length !== 16) {
|
|
235
|
+
throw new BufferError('ULID_BYTES_INVALID_LENGTH', `ULID bytes must be exactly 16 bytes, got ${bytes.length}`)
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
// Timestamp: bytes 0-5 -> 10 characters
|
package/src/ulid/ulid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { incrementBytesInPlace, writeTimestamp48 } from '../common/bytes'
|
|
2
2
|
import { rng } from '../common/random'
|
|
3
|
+
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
3
4
|
import { BufferError, InvalidInputError } from '../errors'
|
|
4
5
|
import { bytesToUlid, decodeToBytes, decodeUlidTime, encodeRandom, encodeTime } from './crockford'
|
|
5
6
|
|
|
@@ -17,12 +18,19 @@ export type UlidOptions = {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export type Ulid = {
|
|
21
|
+
/** Generate a time-ordered ULID string. */
|
|
20
22
|
(): string
|
|
23
|
+
/** Generate a ULID with explicit options or write its 16 canonical bytes into a caller-owned buffer. */
|
|
21
24
|
<TBuf extends Uint8Array = Uint8Array>(options: UlidOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
25
|
+
/** Generate a ULID string with optional timestamp or random bytes. */
|
|
22
26
|
(options?: UlidOptions, buf?: undefined, offset?: number): string
|
|
27
|
+
/** Convert a ULID string to its canonical 16-byte representation. */
|
|
23
28
|
toBytes(id: string): Uint8Array
|
|
29
|
+
/** Convert 16 canonical ULID bytes to a ULID string. */
|
|
24
30
|
fromBytes(bytes: Uint8Array): string
|
|
31
|
+
/** Read the embedded Unix timestamp in milliseconds. */
|
|
25
32
|
timestamp(id: string): number
|
|
33
|
+
/** Return whether a value is a syntactically valid ULID string. */
|
|
26
34
|
isValid(id: unknown): id is string
|
|
27
35
|
/** The nil ULID (all zeros) */
|
|
28
36
|
NIL: string
|
|
@@ -32,6 +40,9 @@ export type Ulid = {
|
|
|
32
40
|
|
|
33
41
|
// Validation regex: first char [0-7] to prevent overflow, rest from Crockford alphabet
|
|
34
42
|
const ULID_REGEX = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i
|
|
43
|
+
const ULID_BYTES = 16
|
|
44
|
+
const RANDOM_BYTES = 10
|
|
45
|
+
const MAX_MSECS = 0xffffffffffff
|
|
35
46
|
|
|
36
47
|
type UlidState = {
|
|
37
48
|
msecs: number
|
|
@@ -48,29 +59,27 @@ type UlidState = {
|
|
|
48
59
|
*/
|
|
49
60
|
const state: UlidState = {
|
|
50
61
|
msecs: -Infinity,
|
|
51
|
-
lastRandom: new Uint8Array(
|
|
62
|
+
lastRandom: new Uint8Array(RANDOM_BYTES),
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
function
|
|
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
|
+
function writeUlidBytesUnchecked(time: number, random: Uint8Array, buf: Uint8Array, offset: number): void {
|
|
65
66
|
// Timestamp (48-bit big-endian milliseconds since Unix epoch) -> bytes 0-5
|
|
66
67
|
writeTimestamp48(buf, offset, time)
|
|
67
68
|
|
|
68
69
|
// Random (80 bits) -> bytes 6-15
|
|
69
|
-
for (let i = 0; i <
|
|
70
|
+
for (let i = 0; i < RANDOM_BYTES; i += 1) {
|
|
70
71
|
buf[offset + 6 + i] = random[i]
|
|
71
72
|
}
|
|
73
|
+
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
function writeUlidBytes(time: number, random: Uint8Array, buf: Uint8Array, offset: number): void {
|
|
76
|
+
if (!isWritableRange(buf, offset, ULID_BYTES)) {
|
|
77
|
+
throw new BufferError(
|
|
78
|
+
'ULID_BUFFER_OUT_OF_BOUNDS',
|
|
79
|
+
`ULID byte range ${offset}:${offset + ULID_BYTES - 1} is out of buffer bounds`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
writeUlidBytesUnchecked(time, random, buf, offset)
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
/*
|
|
@@ -97,27 +106,36 @@ function ulidFn<TBuf extends Uint8Array = Uint8Array>(options?: UlidOptions, buf
|
|
|
97
106
|
* - all ULIDs generated within a single request will have the same timestamp.
|
|
98
107
|
* - the monotonic ordering will rely entirely on incrementing the random portion.
|
|
99
108
|
*/
|
|
100
|
-
const defaultTime = Date.now()
|
|
101
|
-
|
|
102
109
|
if (options) {
|
|
103
110
|
// Explicit options provided - use them directly without monotonic state
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
const optMsecs = options.msecs
|
|
112
|
+
if (optMsecs !== undefined && !isIntegerInRange(optMsecs, 0, MAX_MSECS)) {
|
|
113
|
+
throw new InvalidInputError(
|
|
114
|
+
'ULID_TIMESTAMP_OUT_OF_RANGE',
|
|
115
|
+
`Timestamp must be an integer between 0 and ${MAX_MSECS}`,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
time = optMsecs ?? Date.now()
|
|
119
|
+
const optRandom = options.random
|
|
120
|
+
if (optRandom) {
|
|
121
|
+
if (optRandom.length < RANDOM_BYTES) {
|
|
122
|
+
throw new InvalidInputError(
|
|
123
|
+
'ULID_RANDOM_BYTES_TOO_SHORT',
|
|
124
|
+
`Random bytes length must be >= ${RANDOM_BYTES} for ULID`,
|
|
125
|
+
)
|
|
108
126
|
}
|
|
109
|
-
random =
|
|
127
|
+
random = optRandom
|
|
110
128
|
} else {
|
|
111
129
|
random = rng()
|
|
112
130
|
}
|
|
113
131
|
} else {
|
|
114
|
-
time =
|
|
132
|
+
time = Date.now()
|
|
115
133
|
|
|
116
134
|
if (time > state.msecs) {
|
|
117
135
|
// New millisecond: generate fresh random
|
|
118
136
|
random = rng()
|
|
119
137
|
state.msecs = time
|
|
120
|
-
state.lastRandom.set(random.subarray(0,
|
|
138
|
+
state.lastRandom.set(random.subarray(0, RANDOM_BYTES))
|
|
121
139
|
} else {
|
|
122
140
|
// Same millisecond or clock rollback: preserve last timestamp and increment random portion.
|
|
123
141
|
time = state.msecs
|
|
@@ -133,7 +151,7 @@ function ulidFn<TBuf extends Uint8Array = Uint8Array>(options?: UlidOptions, buf
|
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
if (buf) {
|
|
136
|
-
|
|
154
|
+
writeUlidBytes(time, random, buf, offset)
|
|
137
155
|
return buf
|
|
138
156
|
}
|
|
139
157
|
|
|
@@ -172,6 +190,10 @@ function isValid(id: unknown): id is string {
|
|
|
172
190
|
return typeof id === 'string' && ULID_REGEX.test(id)
|
|
173
191
|
}
|
|
174
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Generate a ULID string or write its 16 canonical bytes into a buffer.
|
|
195
|
+
* ULIDs are URL-safe, carry a millisecond timestamp, and sort by creation time.
|
|
196
|
+
*/
|
|
175
197
|
export const ulid: Ulid = Object.assign(ulidFn, {
|
|
176
198
|
toBytes: (id: string) => decodeToBytes(id),
|
|
177
199
|
fromBytes: (bytes: Uint8Array) => bytesToUlid(bytes),
|
package/src/uuid/common/uuid.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParseError } from '../../errors'
|
|
1
|
+
import { BufferError, ParseError } from '../../errors'
|
|
2
2
|
import { hexValue } from './hex'
|
|
3
3
|
|
|
4
4
|
const UUID_BYTE_LENGTH = 16
|
|
@@ -93,6 +93,17 @@ const UUID_CHAR_IS_HIGH: boolean[] = [
|
|
|
93
93
|
const HEX_TABLE: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))
|
|
94
94
|
|
|
95
95
|
export function formatUuid(bytes: Uint8Array): string {
|
|
96
|
+
if (bytes.length !== UUID_BYTE_LENGTH) {
|
|
97
|
+
throw new BufferError(
|
|
98
|
+
'UUID_BYTES_INVALID_LENGTH',
|
|
99
|
+
`UUID bytes must be exactly ${UUID_BYTE_LENGTH} bytes, got ${bytes.length}`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return formatUuidUnchecked(bytes)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function formatUuidUnchecked(bytes: Uint8Array): string {
|
|
96
107
|
// Direct string concatenation - optimized for V8's string builder.
|
|
97
108
|
// This approach avoids loop overhead and intermediate allocations.
|
|
98
109
|
// See: https://github.com/uuidjs/uuid/pull/434
|