uniku 0.0.13 → 0.2.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/README.md +113 -14
- package/build/{bytes-NRtBOHK5.mjs → bytes-CbbFzz-v.mjs} +1 -1
- package/build/{bytes-NRtBOHK5.mjs.map → bytes-CbbFzz-v.mjs.map} +1 -1
- package/build/cuid2/cuid2.mjs +1 -1
- package/build/ksuid/ksuid.mjs +1 -1
- package/build/nanoid/nanoid.d.mts.map +1 -1
- package/build/nanoid/nanoid.mjs +1 -1
- package/build/nanoid/nanoid.mjs.map +1 -1
- package/build/objectid/objectid.d.mts +62 -0
- package/build/objectid/objectid.d.mts.map +1 -0
- package/build/objectid/objectid.mjs +2 -0
- package/build/objectid/objectid.mjs.map +1 -0
- package/build/random-C_nRoaUe.mjs +2 -0
- package/build/{random-CWCVvNLb.mjs.map → random-C_nRoaUe.mjs.map} +1 -1
- package/build/typeid/typeid.d.mts +38 -0
- package/build/typeid/typeid.d.mts.map +1 -0
- package/build/typeid/typeid.mjs +2 -0
- package/build/typeid/typeid.mjs.map +1 -0
- package/build/ulid/ulid.mjs +1 -1
- package/build/uuid/v4.mjs +1 -1
- package/build/uuid/v7.mjs +1 -1
- package/build/{uuid-QysI8pWE.mjs → uuid-C9VcXh5N.mjs} +1 -1
- package/build/{uuid-QysI8pWE.mjs.map → uuid-C9VcXh5N.mjs.map} +1 -1
- package/package.json +17 -2
- package/src/nanoid/nanoid.ts +16 -7
- package/src/objectid/hex.ts +89 -0
- package/src/objectid/objectid.ts +266 -0
- package/src/typeid/typeid.ts +283 -0
- package/build/random-CWCVvNLb.mjs +0 -2
package/build/uuid/v7.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{r as e}from"../random-C_nRoaUe.mjs";import{BufferError as t,InvalidInputError as n,ParseError as r,UniqueIdError as i}from"../errors.mjs";import{n as a,t as o}from"../uuid-C9VcXh5N.mjs";const s=/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,c=new Uint8Array(16),l={msecs:-1/0,seq:0};function u(e,r,i,a,o=0){if(e.length<16)throw new n(`UUID_RANDOM_BYTES_TOO_SHORT`,`Random bytes length must be >= 16`);if(o<0||o+16>a.length)throw new t(`UUID_BUFFER_OUT_OF_BOUNDS`,`UUID byte range ${o}:${o+15} is out of buffer bounds`);return r??=Date.now(),i??=e[6]<<23|e[7]<<16|e[8]<<8|e[9],a[o++]=r/1099511627776&255,a[o++]=r/4294967296&255,a[o++]=r/16777216&255,a[o++]=r/65536&255,a[o++]=r/256&255,a[o++]=r&255,a[o++]=112|i>>>28&15,a[o++]=i>>>20&255,a[o++]=128|i>>>14&63,a[o++]=i>>>6&255,a[o++]=i<<2&255|e[10]&3,a[o++]=e[11],a[o++]=e[12],a[o++]=e[13],a[o++]=e[14],a[o++]=e[15],a}function d(t,n,r){let i;if(t)i=u(t.random??e(),t.msecs,t.seq,n??c,n?r:0);else{let t=Date.now(),a=e();t>l.msecs?(l.seq=a[6]<<23|a[7]<<16|a[8]<<8|a[9],l.msecs=t):(l.seq=l.seq+1|0,l.seq<0&&(l.seq=0,l.msecs++)),i=u(a,l.msecs,l.seq,n??c,n?r:0)}return n?i:o(i)}function f(e){let t=a(e),n=0;for(let e=0;e<6;e+=1)n=n*256+t[e];return n}function p(e){return typeof e==`string`&&s.test(e)}const m=Object.assign(d,{toBytes:a,fromBytes:o,timestamp:f,isValid:p,NIL:`00000000-0000-0000-0000-000000000000`,MAX:`ffffffff-ffff-ffff-ffff-ffffffffffff`});export{t as BufferError,n as InvalidInputError,r as ParseError,i as UniqueIdError,m as uuidv7};
|
|
2
2
|
//# sourceMappingURL=v7.mjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{ParseError as e}from"./errors.mjs";function t(e){return e>=48&&e<=57?e-48:e>=65&&e<=70?e-65+10:e>=97&&e<=102?e-97+10:-1}const n=[0,0,1,1,2,2,3,3,-1,4,4,5,5,-1,6,6,7,7,-1,8,8,9,9,-1,10,10,11,11,12,12,13,13,14,14,15,15],r=[!0,!1,!0,!1,!0,!1,!0,!1,!1,!0,!1,!0,!1,!1,!0,!1,!0,!1,!1,!0,!1,!0,!1,!1,!0,!1,!0,!1,!0,!1,!0,!1,!0,!1,!0,!1],i=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,`0`));function a(e){return i[e[0]]+i[e[1]]+i[e[2]]+i[e[3]]+`-`+i[e[4]]+i[e[5]]+`-`+i[e[6]]+i[e[7]]+`-`+i[e[8]]+i[e[9]]+`-`+i[e[10]]+i[e[11]]+i[e[12]]+i[e[13]]+i[e[14]]+i[e[15]]}function o(i){if(i.length!==36)throw new e(`UUID_INVALID_LENGTH`,`UUID string must be 36 characters, got ${i.length}`);if(i[8]!==`-`||i[13]!==`-`||i[18]!==`-`||i[23]!==`-`)throw new e(`UUID_INVALID_SEPARATORS`,`UUID string has invalid separators at positions 8, 13, 18, 23. Received: "${i}"`);let a=new Uint8Array(16);for(let o=0;o<36;o+=1){let s=n[o];if(s===-1)continue;let c=t(i.charCodeAt(o));if(c===-1)throw new e(`UUID_INVALID_HEX_CHAR`,`UUID string contains invalid hex character at position ${o}`);r[o]?a[s]=c<<4:a[s]|=c}return a}export{o as n,a as t};
|
|
2
|
-
//# sourceMappingURL=uuid-
|
|
2
|
+
//# sourceMappingURL=uuid-C9VcXh5N.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uuid-
|
|
1
|
+
{"version":3,"file":"uuid-C9VcXh5N.mjs","names":["UUID_CHAR_TO_BYTE: number[]","UUID_CHAR_IS_HIGH: boolean[]","HEX_TABLE: string[]"],"sources":["../src/uuid/common/hex.ts","../src/uuid/common/uuid.ts"],"sourcesContent":["// ASCII code ranges for hex character validation\nconst ASCII_0 = 48 // '0'\nconst ASCII_9 = 57 // '9'\nconst ASCII_A = 65 // 'A'\nconst ASCII_F = 70 // 'F'\nconst ASCII_a = 97 // 'a'\nconst ASCII_f = 102 // 'f'\n\n/**\n * Convert a hex character ASCII code to its numeric value (0-15).\n * Returns -1 for invalid hex characters.\n */\nexport function hexValue(code: number): number {\n if (code >= ASCII_0 && code <= ASCII_9) {\n return code - ASCII_0\n }\n if (code >= ASCII_A && code <= ASCII_F) {\n return code - ASCII_A + 10\n }\n if (code >= ASCII_a && code <= ASCII_f) {\n return code - ASCII_a + 10\n }\n return -1 // Invalid hex character\n}\n","import { ParseError } from '../../errors'\nimport { hexValue } from './hex'\n\nconst UUID_BYTE_LENGTH = 16\nconst UUID_STRING_LENGTH = 36\n\n// Mapping from UUID string position to byte index.\n// -1 indicates a dash position that should be skipped.\n// This avoids intermediate string allocations during parsing.\nconst UUID_CHAR_TO_BYTE: number[] = [\n 0,\n 0,\n 1,\n 1,\n 2,\n 2,\n 3,\n 3, // chars 0-7 → bytes 0-3\n -1, // char 8 is '-'\n 4,\n 4,\n 5,\n 5, // chars 9-12 → bytes 4-5\n -1, // char 13 is '-'\n 6,\n 6,\n 7,\n 7, // chars 14-17 → bytes 6-7\n -1, // char 18 is '-'\n 8,\n 8,\n 9,\n 9, // chars 19-22 → bytes 8-9\n -1, // char 23 is '-'\n 10,\n 10,\n 11,\n 11,\n 12,\n 12,\n 13,\n 13,\n 14,\n 14,\n 15,\n 15, // chars 24-35 → bytes 10-15\n]\n\n// Whether each position is the high nibble (true) or low nibble (false)\nconst UUID_CHAR_IS_HIGH: boolean[] = [\n true,\n false,\n true,\n false,\n true,\n false,\n true,\n false, // chars 0-7\n false, // dash (ignored)\n true,\n false,\n true,\n false, // chars 9-12\n false, // dash\n true,\n false,\n true,\n false, // chars 14-17\n false, // dash\n true,\n false,\n true,\n false, // chars 19-22\n false, // dash\n true,\n false,\n true,\n false,\n true,\n false,\n true,\n false,\n true,\n false,\n true,\n false, // chars 24-35\n]\n\n// Pre-computed lookup table for byte-to-hex conversion (0x00 -> \"00\", 0xff -> \"ff\")\n//\n// Note: this table must remain defined in the same module as `formatUuid`, otherwise the v8 optimizer\n// will cause a performance drop of ~36%.\nconst HEX_TABLE: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'))\n\nexport function formatUuid(bytes: Uint8Array): string {\n // Direct string concatenation - optimized for V8's string builder.\n // This approach avoids loop overhead and intermediate allocations.\n // See: https://github.com/uuidjs/uuid/pull/434\n return (\n HEX_TABLE[bytes[0]] +\n HEX_TABLE[bytes[1]] +\n HEX_TABLE[bytes[2]] +\n HEX_TABLE[bytes[3]] +\n '-' +\n HEX_TABLE[bytes[4]] +\n HEX_TABLE[bytes[5]] +\n '-' +\n HEX_TABLE[bytes[6]] +\n HEX_TABLE[bytes[7]] +\n '-' +\n HEX_TABLE[bytes[8]] +\n HEX_TABLE[bytes[9]] +\n '-' +\n HEX_TABLE[bytes[10]] +\n HEX_TABLE[bytes[11]] +\n HEX_TABLE[bytes[12]] +\n HEX_TABLE[bytes[13]] +\n HEX_TABLE[bytes[14]] +\n HEX_TABLE[bytes[15]]\n )\n}\n\nexport function parseUuid(value: string): Uint8Array {\n if (value.length !== UUID_STRING_LENGTH) {\n throw new ParseError('UUID_INVALID_LENGTH', `UUID string must be 36 characters, got ${value.length}`)\n }\n\n // Validate separator positions directly (more efficient than full loop)\n if (value[8] !== '-' || value[13] !== '-' || value[18] !== '-' || value[23] !== '-') {\n throw new ParseError(\n 'UUID_INVALID_SEPARATORS',\n `UUID string has invalid separators at positions 8, 13, 18, 23. Received: \"${value}\"`,\n )\n }\n\n // Parse bytes directly from UUID string without intermediate string allocations.\n // This avoids the 9 allocations (5 slices + 4 concatenations) of the naive approach.\n const bytes = new Uint8Array(UUID_BYTE_LENGTH)\n\n for (let i = 0; i < UUID_STRING_LENGTH; i += 1) {\n const byteIdx = UUID_CHAR_TO_BYTE[i]\n if (byteIdx === -1) continue // Skip dash positions\n\n const nibble = hexValue(value.charCodeAt(i))\n if (nibble === -1) {\n throw new ParseError('UUID_INVALID_HEX_CHAR', `UUID string contains invalid hex character at position ${i}`)\n }\n\n if (UUID_CHAR_IS_HIGH[i]) {\n bytes[byteIdx] = nibble << 4\n } else {\n bytes[byteIdx] |= nibble\n }\n }\n\n return bytes\n}\n"],"mappings":"0CAYA,SAAgB,EAAS,EAAsB,CAU7C,OATI,GAAQ,IAAW,GAAQ,GACtB,EAAO,GAEZ,GAAQ,IAAW,GAAQ,GACtB,EAAO,GAAU,GAEtB,GAAQ,IAAW,GAAQ,IACtB,EAAO,GAAU,GAEnB,GCnBT,MAMMA,EAA8B,CAClC,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,GACA,EACA,EACA,EACA,EACA,GACA,EACA,EACA,EACA,EACA,GACA,EACA,EACA,EACA,EACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACD,CAGKC,EAA+B,CACnC,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GACD,CAMKC,EAAsB,MAAM,KAAK,CAAE,OAAQ,IAAK,EAAG,EAAG,IAAM,EAAE,SAAS,GAAG,CAAC,SAAS,EAAG,IAAI,CAAC,CAElG,SAAgB,EAAW,EAA2B,CAIpD,OACE,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,IACA,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,IACA,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,IACA,EAAU,EAAM,IAChB,EAAU,EAAM,IAChB,IACA,EAAU,EAAM,KAChB,EAAU,EAAM,KAChB,EAAU,EAAM,KAChB,EAAU,EAAM,KAChB,EAAU,EAAM,KAChB,EAAU,EAAM,KAIpB,SAAgB,EAAU,EAA2B,CACnD,GAAI,EAAM,SAAW,GACnB,MAAM,IAAI,EAAW,sBAAuB,0CAA0C,EAAM,SAAS,CAIvG,GAAI,EAAM,KAAO,KAAO,EAAM,MAAQ,KAAO,EAAM,MAAQ,KAAO,EAAM,MAAQ,IAC9E,MAAM,IAAI,EACR,0BACA,6EAA6E,EAAM,GACpF,CAKH,IAAM,EAAQ,IAAI,WAAW,GAAiB,CAE9C,IAAK,IAAI,EAAI,EAAG,EAAI,GAAoB,GAAK,EAAG,CAC9C,IAAM,EAAU,EAAkB,GAClC,GAAI,IAAY,GAAI,SAEpB,IAAM,EAAS,EAAS,EAAM,WAAW,EAAE,CAAC,CAC5C,GAAI,IAAW,GACb,MAAM,IAAI,EAAW,wBAAyB,0DAA0D,IAAI,CAG1G,EAAkB,GACpB,EAAM,GAAW,GAAU,EAE3B,EAAM,IAAY,EAItB,OAAO"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"private": false,
|
|
3
3
|
"name": "uniku",
|
|
4
|
-
"version": "0.0
|
|
5
|
-
"description": "Minimal, tree-shakeable
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Minimal, tree-shakeable unique ID generators for every JavaScript runtime",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alberto Schiabel",
|
|
8
8
|
"email": "alberto.schiabel@gmail.com",
|
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
"uuidv4",
|
|
24
24
|
"uuidv7",
|
|
25
25
|
"ulid",
|
|
26
|
+
"typeid",
|
|
26
27
|
"cuid2",
|
|
27
28
|
"nanoid",
|
|
28
29
|
"ksuid",
|
|
30
|
+
"objectid",
|
|
31
|
+
"mongodb",
|
|
29
32
|
"unique",
|
|
30
33
|
"id",
|
|
31
34
|
"identifier",
|
|
@@ -70,6 +73,11 @@
|
|
|
70
73
|
"import": "./build/ulid/ulid.mjs",
|
|
71
74
|
"default": "./build/ulid/ulid.mjs"
|
|
72
75
|
},
|
|
76
|
+
"./typeid": {
|
|
77
|
+
"types": "./build/typeid/typeid.d.mts",
|
|
78
|
+
"import": "./build/typeid/typeid.mjs",
|
|
79
|
+
"default": "./build/typeid/typeid.mjs"
|
|
80
|
+
},
|
|
73
81
|
"./cuid2": {
|
|
74
82
|
"types": "./build/cuid2/cuid2.d.mts",
|
|
75
83
|
"import": "./build/cuid2/cuid2.mjs",
|
|
@@ -85,6 +93,11 @@
|
|
|
85
93
|
"import": "./build/ksuid/ksuid.mjs",
|
|
86
94
|
"default": "./build/ksuid/ksuid.mjs"
|
|
87
95
|
},
|
|
96
|
+
"./objectid": {
|
|
97
|
+
"types": "./build/objectid/objectid.d.mts",
|
|
98
|
+
"import": "./build/objectid/objectid.mjs",
|
|
99
|
+
"default": "./build/objectid/objectid.mjs"
|
|
100
|
+
},
|
|
88
101
|
"./errors": {
|
|
89
102
|
"types": "./build/errors.d.mts",
|
|
90
103
|
"import": "./build/errors.mjs",
|
|
@@ -99,7 +112,9 @@
|
|
|
99
112
|
"@owpz/ksuid": "^25.7.20",
|
|
100
113
|
"@paralleldrive/cuid2": "^3.3.0",
|
|
101
114
|
"@types/bun": "^1.3.8",
|
|
115
|
+
"bson": "^7.3.1",
|
|
102
116
|
"nanoid": "^5.1.6",
|
|
117
|
+
"typeid-js": "1.2.0",
|
|
103
118
|
"ulid": "3.0.2",
|
|
104
119
|
"uuid": "13.0.0"
|
|
105
120
|
},
|
package/src/nanoid/nanoid.ts
CHANGED
|
@@ -10,11 +10,12 @@ const NANOID_REGEX = /^[A-Za-z0-9_-]+$/
|
|
|
10
10
|
// Keep Nanoid's private pool for its default hot path. It consumes `size`
|
|
11
11
|
// bytes directly without the shared helper's UUID/CUID2-oriented branches.
|
|
12
12
|
const POOL_SIZE_MULTIPLIER = 128
|
|
13
|
+
const MAX_POOL_SIZE = 65_536
|
|
13
14
|
let pool: Uint8Array | undefined
|
|
14
15
|
let poolOffset = 0
|
|
15
16
|
|
|
16
17
|
function fillPool(bytes: number): void {
|
|
17
|
-
const size = bytes * POOL_SIZE_MULTIPLIER
|
|
18
|
+
const size = Math.min(bytes * POOL_SIZE_MULTIPLIER, MAX_POOL_SIZE)
|
|
18
19
|
if (!pool || pool.length < size) {
|
|
19
20
|
pool = new Uint8Array(size)
|
|
20
21
|
crypto.getRandomValues(pool)
|
|
@@ -26,6 +27,15 @@ function fillPool(bytes: number): void {
|
|
|
26
27
|
poolOffset += bytes
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function defaultAlphabetFromPool(size: number): string {
|
|
31
|
+
fillPool(size)
|
|
32
|
+
let id = ''
|
|
33
|
+
for (let i = poolOffset - size; i < poolOffset; i++) {
|
|
34
|
+
id += URL_ALPHABET[pool![i] & 63]
|
|
35
|
+
}
|
|
36
|
+
return id
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export type NanoidOptions = {
|
|
30
40
|
/**
|
|
31
41
|
* Random bytes for deterministic output (testing).
|
|
@@ -104,12 +114,7 @@ function nanoidFn(sizeOrOptions?: number | NanoidOptions): string {
|
|
|
104
114
|
// ULTRA-FAST PATH: No arguments = default nanoid
|
|
105
115
|
// Uses simple pooled random bytes (npm nanoid style) for best performance
|
|
106
116
|
if (sizeOrOptions === undefined) {
|
|
107
|
-
|
|
108
|
-
let id = ''
|
|
109
|
-
for (let i = poolOffset - DEFAULT_SIZE; i < poolOffset; i++) {
|
|
110
|
-
id += URL_ALPHABET[pool![i] & 63]
|
|
111
|
-
}
|
|
112
|
-
return id
|
|
117
|
+
return defaultAlphabetFromPool(DEFAULT_SIZE)
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
let size = DEFAULT_SIZE
|
|
@@ -131,6 +136,10 @@ function nanoidFn(sizeOrOptions?: number | NanoidOptions): string {
|
|
|
131
136
|
|
|
132
137
|
if (size === 0) return ''
|
|
133
138
|
|
|
139
|
+
if (alphabet === URL_ALPHABET && randomBytes === undefined) {
|
|
140
|
+
return defaultAlphabetFromPool(size)
|
|
141
|
+
}
|
|
142
|
+
|
|
134
143
|
const alphabetLen = alphabet.length
|
|
135
144
|
|
|
136
145
|
// FAST PATH: Power-of-2 alphabet (includes default 64-char)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { BufferError, ParseError } from '../errors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hex encoding/decoding for ObjectID.
|
|
5
|
+
* Alphabet: 0-9a-f (lowercase on encode, case-insensitive on decode)
|
|
6
|
+
*
|
|
7
|
+
* ObjectID binary format: 12 bytes (96 bits)
|
|
8
|
+
* - 4 bytes: big-endian Unix timestamp in seconds
|
|
9
|
+
* - 5 bytes: per-process random value
|
|
10
|
+
* - 3 bytes: big-endian counter
|
|
11
|
+
*
|
|
12
|
+
* String format: 24 characters of hex
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const HEX_CHARS = '0123456789abcdef'
|
|
16
|
+
const OBJECTID_BYTES = 12
|
|
17
|
+
const OBJECTID_STRING_LEN = 24
|
|
18
|
+
|
|
19
|
+
// Pre-computed encode table: byte value (0-255) -> 2-char lowercase hex string.
|
|
20
|
+
const ENCODE_TABLE: string[] = Array.from(
|
|
21
|
+
{ length: 256 },
|
|
22
|
+
(_, byte) => HEX_CHARS[(byte >> 4) & 0xf] + HEX_CHARS[byte & 0xf],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// Pre-computed decode table covering every UTF-16 code unit charCodeAt can
|
|
26
|
+
// return, so lookups never go out of bounds and a single `=== 255` check
|
|
27
|
+
// rejects invalid input (including non-ASCII) without a per-character range
|
|
28
|
+
// check. Costs 128 KiB once at module load; valid inputs only touch the first
|
|
29
|
+
// 128 bytes, so cache behavior is unaffected.
|
|
30
|
+
// Note: unlike Base62, hex decoding is case-insensitive - 'a' and 'A' both decode to 10.
|
|
31
|
+
const DECODING = new Uint8Array(65536)
|
|
32
|
+
DECODING.fill(255) // 255 = invalid marker
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i <= 9; i += 1) {
|
|
35
|
+
DECODING['0'.charCodeAt(0) + i] = i
|
|
36
|
+
}
|
|
37
|
+
for (let i = 0; i < 6; i += 1) {
|
|
38
|
+
DECODING['a'.charCodeAt(0) + i] = 10 + i
|
|
39
|
+
DECODING['A'.charCodeAt(0) + i] = 10 + i
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Encode 12 ObjectID bytes to a 24-character lowercase hex string.
|
|
44
|
+
*/
|
|
45
|
+
export function encodeObjectIdHex(bytes: Uint8Array): string {
|
|
46
|
+
if (bytes.length < OBJECTID_BYTES) {
|
|
47
|
+
throw new BufferError(
|
|
48
|
+
'OBJECTID_BYTES_TOO_SHORT',
|
|
49
|
+
`ObjectID bytes must be at least ${OBJECTID_BYTES} bytes, got ${bytes.length}`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let encoded = ''
|
|
54
|
+
for (let i = 0; i < OBJECTID_BYTES; i += 1) {
|
|
55
|
+
encoded += ENCODE_TABLE[bytes[i]]
|
|
56
|
+
}
|
|
57
|
+
return encoded
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Decode a 24-character hex string to 12 ObjectID bytes.
|
|
62
|
+
*/
|
|
63
|
+
export function decodeObjectIdHex(str: string): Uint8Array {
|
|
64
|
+
if (str.length !== OBJECTID_STRING_LEN) {
|
|
65
|
+
throw new ParseError(
|
|
66
|
+
'OBJECTID_INVALID_LENGTH',
|
|
67
|
+
`ObjectID string must be ${OBJECTID_STRING_LEN} characters, got ${str.length}`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bytes = new Uint8Array(OBJECTID_BYTES)
|
|
72
|
+
for (let i = 0; i < OBJECTID_BYTES; i += 1) {
|
|
73
|
+
const hiIndex = i * 2
|
|
74
|
+
const loIndex = hiIndex + 1
|
|
75
|
+
const hi = DECODING[str.charCodeAt(hiIndex)]
|
|
76
|
+
const lo = DECODING[str.charCodeAt(loIndex)]
|
|
77
|
+
|
|
78
|
+
if (hi === 255) {
|
|
79
|
+
throw new ParseError('OBJECTID_INVALID_CHAR', `Invalid ObjectID character: ${str[hiIndex]}`)
|
|
80
|
+
}
|
|
81
|
+
if (lo === 255) {
|
|
82
|
+
throw new ParseError('OBJECTID_INVALID_CHAR', `Invalid ObjectID character: ${str[loIndex]}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
bytes[i] = (hi << 4) | lo
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return bytes
|
|
89
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { writeTimestamp32 } from '../common/bytes'
|
|
2
|
+
import { randomBytes, randomUint32 } from '../common/random'
|
|
3
|
+
import { BufferError, InvalidInputError } from '../errors'
|
|
4
|
+
import { decodeObjectIdHex, encodeObjectIdHex } from './hex'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MongoDB ObjectID
|
|
8
|
+
*
|
|
9
|
+
* A 96-bit (12-byte) identifier consisting of:
|
|
10
|
+
* - 4 bytes: big-endian Unix timestamp in seconds
|
|
11
|
+
* - 5 bytes: per-process random value
|
|
12
|
+
* - 3 bytes: big-endian counter, always incrementing (wraps at 0xFFFFFF back to 0)
|
|
13
|
+
*
|
|
14
|
+
* Encoded as a 24-character lowercase hex string.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const OBJECTID_BYTES = 12
|
|
18
|
+
const TIMESTAMP_BYTES = 4
|
|
19
|
+
const RANDOM_BYTES = 5
|
|
20
|
+
const COUNTER_BYTES = 3
|
|
21
|
+
const MAX_SECS = 0xffffffff
|
|
22
|
+
const MAX_COUNTER = 0xffffff
|
|
23
|
+
|
|
24
|
+
// Validation regex: case-insensitive per KTD7, matching bson's ObjectId.isValid()
|
|
25
|
+
const OBJECTID_REGEX = /^[0-9a-f]{24}$/i
|
|
26
|
+
|
|
27
|
+
export type ObjectIdOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* 5 bytes of random data to use for the ObjectID's random field.
|
|
30
|
+
*/
|
|
31
|
+
random?: Uint8Array
|
|
32
|
+
/**
|
|
33
|
+
* Timestamp in seconds since Unix epoch.
|
|
34
|
+
* Defaults to Math.floor(Date.now() / 1000).
|
|
35
|
+
*/
|
|
36
|
+
secs?: number
|
|
37
|
+
/**
|
|
38
|
+
* 24-bit counter value (0 to 0xFFFFFF).
|
|
39
|
+
*/
|
|
40
|
+
counter?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ObjectId = {
|
|
44
|
+
(): string
|
|
45
|
+
<TBuf extends Uint8Array = Uint8Array>(options: ObjectIdOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
46
|
+
(options?: ObjectIdOptions, buf?: undefined, offset?: number): string
|
|
47
|
+
toBytes(id: string): Uint8Array
|
|
48
|
+
fromBytes(bytes: Uint8Array): string
|
|
49
|
+
timestamp(id: string): number
|
|
50
|
+
isValid(id: unknown): id is string
|
|
51
|
+
/** The nil ObjectID (all zeros) */
|
|
52
|
+
NIL: string
|
|
53
|
+
/** The max ObjectID (all 0xff) */
|
|
54
|
+
MAX: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type ObjectIdState = {
|
|
58
|
+
random: Uint8Array | undefined
|
|
59
|
+
counter: number | undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Module-level state for the per-process random value and the always-incrementing counter.
|
|
64
|
+
*
|
|
65
|
+
* IMPORTANT: This state persists across all objectid() calls in the module's lifetime.
|
|
66
|
+
* - In serverless/edge functions with warm starts, state persists between invocations.
|
|
67
|
+
* Unlike ULID/UUIDv7's per-timestamp sequence reset, the counter continuing to climb
|
|
68
|
+
* across warm starts is exactly the anti-collision behavior the ObjectID spec intends.
|
|
69
|
+
* - For isolated state, pass explicit `random`, `secs`, or `counter` via options.
|
|
70
|
+
* - Tests should mock Date.now() or provide explicit options for deterministic behavior.
|
|
71
|
+
*/
|
|
72
|
+
const state: ObjectIdState = {
|
|
73
|
+
random: undefined,
|
|
74
|
+
counter: undefined,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Draw 5 fresh random bytes into a newly-owned buffer, copying immediately so the
|
|
79
|
+
* result is unaffected by any later pool refill triggered by other random draws
|
|
80
|
+
* (e.g. the counter's `randomUint32()` call) before these bytes are consumed.
|
|
81
|
+
*/
|
|
82
|
+
function freshRandom(): Uint8Array {
|
|
83
|
+
const bytes = new Uint8Array(RANDOM_BYTES)
|
|
84
|
+
bytes.set(randomBytes(RANDOM_BYTES))
|
|
85
|
+
return bytes
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function objectIdBytes(secs: number, random: Uint8Array, counter: number, buf?: Uint8Array, offset = 0): Uint8Array {
|
|
89
|
+
if (!buf) {
|
|
90
|
+
buf = new Uint8Array(OBJECTID_BYTES)
|
|
91
|
+
offset = 0
|
|
92
|
+
} else if (offset < 0 || offset + OBJECTID_BYTES > buf.length) {
|
|
93
|
+
throw new BufferError(
|
|
94
|
+
'OBJECTID_BUFFER_OUT_OF_BOUNDS',
|
|
95
|
+
`ObjectID byte range ${offset}:${offset + OBJECTID_BYTES - 1} is out of buffer bounds`,
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Timestamp (32-bit big-endian seconds since Unix epoch) -> bytes 0-3
|
|
100
|
+
writeTimestamp32(buf, offset, secs)
|
|
101
|
+
|
|
102
|
+
// Random (5 bytes) -> bytes 4-8
|
|
103
|
+
for (let i = 0; i < RANDOM_BYTES; i += 1) {
|
|
104
|
+
buf[offset + TIMESTAMP_BYTES + i] = random[i]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Counter (24-bit big-endian) -> bytes 9-11
|
|
108
|
+
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES] = (counter >>> 16) & 0xff
|
|
109
|
+
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES + 1] = (counter >>> 8) & 0xff
|
|
110
|
+
buf[offset + TIMESTAMP_BYTES + RANDOM_BYTES + COUNTER_BYTES - 1] = counter & 0xff
|
|
111
|
+
|
|
112
|
+
return buf
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* Overload: no buffer => return an ObjectID string.
|
|
117
|
+
*/
|
|
118
|
+
function objectIdFn(options?: ObjectIdOptions, buf?: undefined, offset?: number): string
|
|
119
|
+
/*
|
|
120
|
+
* Overload: caller provides a buffer slice to fill with ObjectID bytes.
|
|
121
|
+
*/
|
|
122
|
+
function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
|
|
123
|
+
options: ObjectIdOptions | undefined,
|
|
124
|
+
buf: TBuf,
|
|
125
|
+
offset?: number,
|
|
126
|
+
): TBuf
|
|
127
|
+
function objectIdFn<TBuf extends Uint8Array = Uint8Array>(
|
|
128
|
+
options?: ObjectIdOptions,
|
|
129
|
+
buf?: TBuf,
|
|
130
|
+
offset = 0,
|
|
131
|
+
): string | TBuf {
|
|
132
|
+
let secs: number
|
|
133
|
+
let random: Uint8Array
|
|
134
|
+
let counter: number
|
|
135
|
+
|
|
136
|
+
if (options) {
|
|
137
|
+
const optRandom = options.random
|
|
138
|
+
if (optRandom && optRandom.length < RANDOM_BYTES) {
|
|
139
|
+
throw new InvalidInputError(
|
|
140
|
+
'OBJECTID_RANDOM_BYTES_TOO_SHORT',
|
|
141
|
+
`Random bytes length must be >= ${RANDOM_BYTES} for ObjectID`,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const optSecs = options.secs
|
|
146
|
+
if (optSecs !== undefined && (optSecs < 0 || optSecs > MAX_SECS)) {
|
|
147
|
+
throw new InvalidInputError('OBJECTID_TIMESTAMP_OUT_OF_RANGE', `Timestamp must be between 0 and ${MAX_SECS}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const optCounter = options.counter
|
|
151
|
+
if (optCounter !== undefined && (optCounter < 0 || optCounter > MAX_COUNTER)) {
|
|
152
|
+
throw new InvalidInputError('OBJECTID_COUNTER_OUT_OF_RANGE', `Counter must be between 0 and ${MAX_COUNTER}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Options bypass persistent state entirely: every field is sourced fresh (given
|
|
156
|
+
// value, or an independently-defaulted one), never read from or written to
|
|
157
|
+
// `state` (see KTD2). This is what makes options-based generation deterministic
|
|
158
|
+
// when all three fields are supplied.
|
|
159
|
+
secs = optSecs ?? Math.floor(Date.now() / 1000)
|
|
160
|
+
random = optRandom ?? freshRandom()
|
|
161
|
+
counter = optCounter ?? randomUint32() & MAX_COUNTER
|
|
162
|
+
} else {
|
|
163
|
+
// Lazily initialize persistent state on first no-option call.
|
|
164
|
+
if (state.random === undefined) {
|
|
165
|
+
state.random = freshRandom()
|
|
166
|
+
}
|
|
167
|
+
if (state.counter === undefined) {
|
|
168
|
+
state.counter = randomUint32() & MAX_COUNTER
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Note: by default, Cloudflare Workers "freezes" time during request handling to prevent
|
|
173
|
+
* side-channel attacks. This means that Date.now() will return the same value for the entire
|
|
174
|
+
* duration of a request.
|
|
175
|
+
* Implications:
|
|
176
|
+
* - all ObjectIDs generated within a single request will share the same timestamp.
|
|
177
|
+
* - monotonic ordering relies entirely on the ever-incrementing counter.
|
|
178
|
+
*/
|
|
179
|
+
secs = Math.floor(Date.now() / 1000)
|
|
180
|
+
random = state.random
|
|
181
|
+
state.counter = (state.counter + 1) & MAX_COUNTER
|
|
182
|
+
counter = state.counter
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (buf) {
|
|
186
|
+
objectIdBytes(secs, random, counter, buf, offset)
|
|
187
|
+
return buf
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const bytes = objectIdBytes(secs, random, counter)
|
|
191
|
+
return encodeObjectIdHex(bytes)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Convert an ObjectID hex string to 12 bytes.
|
|
196
|
+
*/
|
|
197
|
+
function toBytes(id: string): Uint8Array {
|
|
198
|
+
return decodeObjectIdHex(id)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Convert 12 bytes to an ObjectID hex string.
|
|
203
|
+
*/
|
|
204
|
+
function fromBytes(bytes: Uint8Array): string {
|
|
205
|
+
return encodeObjectIdHex(bytes)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Extract the timestamp from an ObjectID string.
|
|
210
|
+
* Returns Unix timestamp in milliseconds for API consistency with ulid/uuidv7/ksuid.
|
|
211
|
+
* Note: ObjectID only has second precision, so the returned value will always end in 000.
|
|
212
|
+
*/
|
|
213
|
+
function timestamp(id: string): number {
|
|
214
|
+
const bytes = decodeObjectIdHex(id)
|
|
215
|
+
// First 4 bytes are big-endian timestamp (seconds since Unix epoch)
|
|
216
|
+
const secs = ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0
|
|
217
|
+
return secs * 1000
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validate an ObjectID string format.
|
|
222
|
+
* Case-insensitive (KTD7), matching bson's ObjectId.isValid() and this repo's existing
|
|
223
|
+
* convention (ulid, ksuid) of accepting mixed-case input for parsing, even though
|
|
224
|
+
* generation always emits lowercase (matching MongoDB driver output).
|
|
225
|
+
*/
|
|
226
|
+
function isValid(id: unknown): id is string {
|
|
227
|
+
return typeof id === 'string' && OBJECTID_REGEX.test(id)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate a MongoDB ObjectID string or write the bytes into a buffer.
|
|
232
|
+
*
|
|
233
|
+
* ObjectID is a 12-byte identifier consisting of a 4-byte timestamp, a 5-byte
|
|
234
|
+
* per-process random value, and a 3-byte always-incrementing counter, encoded
|
|
235
|
+
* as a 24-character lowercase hex string. It is time-ordered and compatible
|
|
236
|
+
* with MongoDB's own ObjectID implementation.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```ts
|
|
240
|
+
* import { objectid } from 'uniku/objectid'
|
|
241
|
+
*
|
|
242
|
+
* const id = objectid()
|
|
243
|
+
* // => "667c3f2a1e2b3c4d5e6f7081"
|
|
244
|
+
*
|
|
245
|
+
* // Extract timestamp
|
|
246
|
+
* const ts = objectid.timestamp(id)
|
|
247
|
+
* console.log(new Date(ts))
|
|
248
|
+
*
|
|
249
|
+
* // Validate
|
|
250
|
+
* objectid.isValid(id) // true
|
|
251
|
+
*
|
|
252
|
+
* // Convert to/from bytes (12 bytes)
|
|
253
|
+
* const bytes = objectid.toBytes(id)
|
|
254
|
+
* const restored = objectid.fromBytes(bytes)
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export const objectid: ObjectId = Object.assign(objectIdFn, {
|
|
258
|
+
toBytes,
|
|
259
|
+
fromBytes,
|
|
260
|
+
timestamp,
|
|
261
|
+
isValid,
|
|
262
|
+
NIL: '0'.repeat(24),
|
|
263
|
+
MAX: 'f'.repeat(24),
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
export { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'
|