uniku 0.2.0 → 0.3.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 +87 -22
- package/build/{bytes-CbbFzz-v.mjs → bytes-xqWxFYsM.mjs} +1 -1
- package/build/{bytes-CbbFzz-v.mjs.map → bytes-xqWxFYsM.mjs.map} +1 -1
- package/build/cuid/v2.d.mts +2 -0
- package/build/cuid/v2.mjs +1 -0
- package/build/cuid2/cuid2.d.mts +5 -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/errors-Dgoyi-ci.d.mts +38 -0
- package/build/errors-Dgoyi-ci.d.mts.map +1 -0
- package/build/errors.d.mts +2 -38
- package/build/errors.mjs +1 -1
- package/build/errors.mjs.map +1 -1
- package/build/generators.d.mts +17 -0
- package/build/generators.d.mts.map +1 -0
- package/build/generators.mjs +2 -0
- package/build/generators.mjs.map +1 -0
- package/build/ksuid/ksuid.d.mts +3 -5
- 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 +3 -6
- 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 +3 -5
- 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/{random-C_nRoaUe.mjs → random-Chp-Nkzi.mjs} +1 -1
- package/build/{random-C_nRoaUe.mjs.map → random-Chp-Nkzi.mjs.map} +1 -1
- package/build/tsid/tsid.d.mts +82 -0
- package/build/tsid/tsid.d.mts.map +1 -0
- package/build/tsid/tsid.mjs +2 -0
- package/build/tsid/tsid.mjs.map +1 -0
- package/build/typeid/typeid.d.mts +1 -1
- package/build/typeid/typeid.d.mts.map +1 -1
- package/build/typeid/typeid.mjs.map +1 -1
- package/build/ulid/ulid.d.mts +3 -5
- 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 +3 -5
- 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 +3 -5
- 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-C9VcXh5N.mjs → uuid-BBjwHv7J.mjs} +1 -1
- package/build/uuid-BBjwHv7J.mjs.map +1 -0
- package/package.json +27 -5
- package/src/cuid/v2.ts +19 -0
- package/src/cuid2/cuid2.ts +4 -0
- package/src/generators.ts +17 -0
- package/src/tsid/crockford64.ts +75 -0
- package/src/tsid/tsid.ts +310 -0
- package/build/errors.d.mts.map +0 -1
- package/build/uuid-C9VcXh5N.mjs.map +0 -1
package/build/uuid/v4.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"../random-
|
|
1
|
+
import{r as e}from"../random-Chp-Nkzi.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-BBjwHv7J.mjs";const s=globalThis.crypto.randomUUID.bind(globalThis.crypto),c=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;function l(e,r,i=0){if(e.length<16)throw new n(`UUID_RANDOM_BYTES_TOO_SHORT`,`Random bytes length must be >= 16`);if(e[6]=e[6]&15|64,e[8]=e[8]&63|128,!r)return e;if(i<0||i+16>r.length)throw new t(`UUID_BUFFER_OUT_OF_BOUNDS`,`UUID byte range ${i}:${i+15} is out of buffer bounds`);for(let t=0;t<16;t+=1)r[i+t]=e[t];return r}function u(e,t,n){return!t&&!e?s():d(e,t,n)}function d(t,n,r){let i=l(t?.random??e(),n,r);return n??o(i)}function f(e){return typeof e==`string`&&c.test(e)}const p=Object.assign(u,{toBytes:a,fromBytes:o,isValid:f,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,p as uuidv4};
|
|
2
2
|
//# sourceMappingURL=v4.mjs.map
|
package/build/uuid/v4.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v4.mjs","names":[
|
|
1
|
+
{"version":3,"file":"v4.mjs","names":[],"sources":["../../src/uuid/v4.ts"],"sourcesContent":["import { rng } from '../common/random'\nimport { BufferError, InvalidInputError } from '../errors'\nimport { formatUuid, parseUuid } from './common/uuid'\n\nconst randomUUID = /*@__PURE__*/ globalThis.crypto.randomUUID.bind(globalThis.crypto)\n\nexport type UuidV4Options = {\n /**\n * 16 bytes of random data to use for UUID generation.\n * Note: Bytes at index 6 and 8 will be modified in-place to set version/variant bits.\n */\n random?: Uint8Array\n}\n\nexport type UuidV4 = {\n (): string\n <TBuf extends Uint8Array = Uint8Array>(options: UuidV4Options | undefined, buf: TBuf, offset?: number): TBuf\n (options?: UuidV4Options, buf?: undefined, offset?: number): string\n toBytes(id: string): Uint8Array\n fromBytes(bytes: Uint8Array): string\n isValid(id: unknown): id is string\n /** The nil UUID (all zeros) */\n NIL: string\n /** The max UUID (all ones) */\n MAX: string\n}\n\nconst 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\n\nfunction v4Bytes(rnds: Uint8Array, buf?: Uint8Array, offset = 0): Uint8Array {\n if (rnds.length < 16) {\n throw new InvalidInputError('UUID_RANDOM_BYTES_TOO_SHORT', 'Random bytes length must be >= 16')\n }\n\n // Set RFC 4122 version (4) and variant (10xx) bits.\n // Note: This modifies the input array in-place.\n rnds[6] = (rnds[6] & 0x0f) | 0x40\n rnds[8] = (rnds[8] & 0x3f) | 0x80\n\n if (!buf) {\n // No output buffer provided - return the modified random bytes directly\n return rnds\n }\n\n if (offset < 0 || offset + 16 > buf.length) {\n throw new BufferError(\n 'UUID_BUFFER_OUT_OF_BOUNDS',\n `UUID byte range ${offset}:${offset + 15} is out of buffer bounds`,\n )\n }\n\n // Copy 16 UUID bytes into the provided buffer slice.\n for (let i = 0; i < 16; i += 1) {\n buf[offset + i] = rnds[i]\n }\n\n return buf\n}\n\n/*\n * Overload: no buffer => return a UUID string.\n */\nfunction v4(options?: UuidV4Options, buf?: undefined, offset?: number): string\n/*\n * Overload: caller provides a buffer slice to fill with UUID bytes.\n */\nfunction v4<TBuf extends Uint8Array = Uint8Array>(options: UuidV4Options | undefined, buf: TBuf, offset?: number): TBuf\nfunction v4<TBuf extends Uint8Array = Uint8Array>(options?: UuidV4Options, buf?: TBuf, offset?: number): string | TBuf {\n if (!buf && !options) {\n return randomUUID()\n }\n\n return _v4(options, buf, offset)\n}\n\nfunction _v4<TBuf extends Uint8Array = Uint8Array>(\n options?: UuidV4Options,\n buf?: TBuf,\n offset?: number,\n): string | TBuf {\n const bytes = v4Bytes(options?.random ?? rng(), buf, offset)\n return buf ?? formatUuid(bytes)\n}\n\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && UUID_V4_REGEX.test(id)\n}\n\n/**\n * Generate a UUID v4 string or write the bytes into a buffer.\n *\n * UUID v4 is a purely random UUID with 122 bits of entropy. It's the most\n * widely compatible UUID format, supported by virtually all databases and systems.\n * Use when you need maximum compatibility and don't require time-ordering.\n *\n * @example\n * ```ts\n * import { uuidv4 } from 'uniku/uuid/v4'\n *\n * const id = uuidv4()\n * // => \"550e8400-e29b-41d4-a716-446655440000\"\n *\n * // Validate\n * uuidv4.isValid(id) // true\n *\n * // Convert to/from bytes (16 bytes)\n * const bytes = uuidv4.toBytes(id)\n * const restored = uuidv4.fromBytes(bytes)\n * ```\n */\nexport const uuidv4: UuidV4 = Object.assign(v4, {\n toBytes: parseUuid,\n fromBytes: formatUuid,\n isValid,\n NIL: '00000000-0000-0000-0000-000000000000',\n MAX: 'ffffffff-ffff-ffff-ffff-ffffffffffff',\n})\n\nexport { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'\n"],"mappings":"iMAIA,MAAM,EAA2B,WAAW,OAAO,WAAW,KAAK,WAAW,MAAM,EAuB9E,EAAgB,yEAEtB,SAAS,EAAQ,EAAkB,EAAkB,EAAS,EAAe,CAC3E,GAAI,EAAK,OAAS,GAChB,MAAM,IAAI,EAAkB,8BAA+B,mCAAmC,EAQhG,GAHA,EAAK,GAAM,EAAK,GAAK,GAAQ,GAC7B,EAAK,GAAM,EAAK,GAAK,GAAQ,IAEzB,CAAC,EAEH,OAAO,EAGT,GAAI,EAAS,GAAK,EAAS,GAAK,EAAI,OAClC,MAAM,IAAI,EACR,4BACA,mBAAmB,EAAO,GAAG,EAAS,GAAG,yBAC3C,EAIF,IAAK,IAAI,EAAI,EAAG,EAAI,GAAI,GAAK,EAC3B,EAAI,EAAS,GAAK,EAAK,GAGzB,OAAO,CACT,CAUA,SAAS,EAAyC,EAAyB,EAAY,EAAgC,CAKrH,MAJI,CAAC,GAAO,CAAC,EACJ,EAAW,EAGb,EAAI,EAAS,EAAK,CAAM,CACjC,CAEA,SAAS,EACP,EACA,EACA,EACe,CACf,IAAM,EAAQ,EAAQ,GAAS,QAAU,EAAI,EAAG,EAAK,CAAM,EAC3D,OAAO,GAAO,EAAW,CAAK,CAChC,CAEA,SAAS,EAAQ,EAA2B,CAC1C,OAAO,OAAO,GAAO,UAAY,EAAc,KAAK,CAAE,CACxD,CAwBA,MAAa,EAAiB,OAAO,OAAO,EAAI,CAC9C,QAAS,EACT,UAAW,EACX,UACA,IAAK,uCACL,IAAK,sCACP,CAAC"}
|
package/build/uuid/v7.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as UniqueIdError, n as InvalidInputError, r as ParseError, t as BufferError } from "../errors-Dgoyi-ci.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/uuid/v7.d.ts
|
|
4
4
|
type UuidV7Options = {
|
|
@@ -17,10 +17,8 @@ type UuidV7 = {
|
|
|
17
17
|
toBytes(id: string): Uint8Array;
|
|
18
18
|
fromBytes(bytes: Uint8Array): string;
|
|
19
19
|
timestamp(id: string): number;
|
|
20
|
-
isValid(id: unknown): id is string;
|
|
21
|
-
/** The
|
|
22
|
-
NIL: string;
|
|
23
|
-
/** The max UUID (all ones) */
|
|
20
|
+
isValid(id: unknown): id is string; /** The nil UUID (all zeros) */
|
|
21
|
+
NIL: string; /** The max UUID (all ones) */
|
|
24
22
|
MAX: string;
|
|
25
23
|
};
|
|
26
24
|
/**
|
package/build/uuid/v7.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v7.d.mts","names":[
|
|
1
|
+
{"version":3,"file":"v7.d.mts","names":[],"sources":["../../src/uuid/v7.ts"],"mappings":";;;KAIY,aAAA;;;AAAZ;;EAKE,MAAA,GAAS,UAAA;EACT,KAAA;EACA,GAAA;AAAA;AAAA,KAGU,MAAA;EAAA;EAAA,cAEI,UAAA,GAAa,UAAA,EAAY,OAAA,EAAS,aAAA,cAA2B,GAAA,EAAK,IAAA,EAAM,MAAA,YAAkB,IAAA;EAAA,CACvG,OAAA,GAAU,aAAA,EAAe,GAAA,cAAiB,MAAA;EAC3C,OAAA,CAAQ,EAAA,WAAa,UAAA;EACrB,SAAA,CAAU,KAAA,EAAO,UAAA;EACjB,SAAA,CAAU,EAAA;EACV,OAAA,CAAQ,EAAA,YAAc,EAAA;EAEtB,GAAA;EAEA,GAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmJW,MAAA,EAAQ,MAAA"}
|
package/build/uuid/v7.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as e}from"../random-
|
|
1
|
+
import{r as e}from"../random-Chp-Nkzi.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-BBjwHv7J.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
|
package/build/uuid/v7.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v7.mjs","names":[
|
|
1
|
+
{"version":3,"file":"v7.mjs","names":[],"sources":["../../src/uuid/v7.ts"],"sourcesContent":["import { rng } from '../common/random'\nimport { BufferError, InvalidInputError } from '../errors'\nimport { formatUuid, parseUuid } from './common/uuid'\n\nexport type UuidV7Options = {\n /**\n * 16 bytes of random data to use for UUID generation.\n * Note: Several bytes will be overwritten with timestamp, version, and variant data.\n */\n random?: Uint8Array\n msecs?: number\n seq?: number\n}\n\nexport type UuidV7 = {\n (): string\n <TBuf extends Uint8Array = Uint8Array>(options: UuidV7Options | undefined, buf: TBuf, offset?: number): TBuf\n (options?: UuidV7Options, buf?: undefined, offset?: number): string\n toBytes(id: string): Uint8Array\n fromBytes(bytes: Uint8Array): string\n timestamp(id: string): number\n isValid(id: unknown): id is string\n /** The nil UUID (all zeros) */\n NIL: string\n /** The max UUID (all ones) */\n MAX: string\n}\n\nconst 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\n\n// Reusable buffer for string output path - avoids allocation per call.\n// Safe because bytes are consumed synchronously by formatUuid().\nconst reusableBuf = new Uint8Array(16)\n\ntype V7State = {\n msecs: number\n seq: number\n}\n\n/**\n * Module-level state for maintaining monotonic ordering within the same millisecond.\n *\n * IMPORTANT: This state persists across all uuidv7() calls in the module's lifetime.\n * - In serverless/edge functions with warm starts, state persists between invocations.\n * - For isolated state, pass explicit `msecs` and `seq` via options.\n * - Tests should mock Date.now() or provide explicit options for deterministic behavior.\n */\nconst state: V7State = { msecs: -Infinity, seq: 0 }\n\nfunction v7Bytes(\n rnds: Uint8Array,\n msecs: number | undefined,\n seq: number | undefined,\n buf: Uint8Array,\n offset = 0,\n): Uint8Array {\n if (rnds.length < 16) {\n throw new InvalidInputError('UUID_RANDOM_BYTES_TOO_SHORT', 'Random bytes length must be >= 16')\n }\n\n if (offset < 0 || offset + 16 > buf.length) {\n throw new BufferError(\n 'UUID_BUFFER_OUT_OF_BOUNDS',\n `UUID byte range ${offset}:${offset + 15} is out of buffer bounds`,\n )\n }\n\n msecs ??= Date.now()\n // Derive a 31-bit sequence if not provided by the caller.\n // Uses the same formula as the hot path for consistency.\n seq ??= (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9]\n\n // Keep this inline on the UUID v7 hot path. Using writeTimestamp48() here\n // benchmarked slower than the direct byte writes.\n buf[offset++] = (msecs / 0x10000000000) & 0xff\n buf[offset++] = (msecs / 0x100000000) & 0xff\n buf[offset++] = (msecs / 0x1000000) & 0xff\n buf[offset++] = (msecs / 0x10000) & 0xff\n buf[offset++] = (msecs / 0x100) & 0xff\n buf[offset++] = msecs & 0xff\n\n // Set version (7) and variant (10xx), then pack sequence and random tail bytes.\n buf[offset++] = 0x70 | ((seq >>> 28) & 0x0f)\n buf[offset++] = (seq >>> 20) & 0xff\n buf[offset++] = 0x80 | ((seq >>> 14) & 0x3f)\n buf[offset++] = (seq >>> 6) & 0xff\n // Lower seq bits plus 2 random bits to complete the 128-bit payload.\n buf[offset++] = ((seq << 2) & 0xff) | (rnds[10] & 0x03)\n buf[offset++] = rnds[11]\n buf[offset++] = rnds[12]\n buf[offset++] = rnds[13]\n buf[offset++] = rnds[14]\n buf[offset++] = rnds[15]\n\n return buf\n}\n\n/*\n * Overload: no buffer => return a UUID string.\n */\nfunction v7(options?: UuidV7Options, buf?: undefined, offset?: number): string\n/*\n * Overload: caller provides a buffer slice to fill with UUID bytes.\n */\nfunction v7<TBuf extends Uint8Array = Uint8Array>(options: UuidV7Options | undefined, buf: TBuf, offset?: number): TBuf\nfunction v7<TBuf extends Uint8Array = Uint8Array>(options?: UuidV7Options, buf?: TBuf, offset?: number): string | TBuf {\n let bytes: Uint8Array\n\n if (options) {\n bytes = v7Bytes(options.random ?? rng(), options.msecs, options.seq, buf ?? reusableBuf, buf ? offset : 0)\n } else {\n // HOT PATH: Inline state management and byte generation for best performance\n const now = Date.now()\n const rnds = rng()\n\n // Update state (inlined for performance)\n if (now > state.msecs) {\n state.seq = (rnds[6] << 23) | (rnds[7] << 16) | (rnds[8] << 8) | rnds[9]\n state.msecs = now\n } else {\n state.seq = (state.seq + 1) | 0\n if (state.seq < 0) {\n state.seq = 0\n state.msecs++\n }\n }\n\n bytes = v7Bytes(rnds, state.msecs, state.seq, buf ?? reusableBuf, buf ? offset : 0)\n }\n\n return buf ? (bytes as TBuf) : formatUuid(bytes)\n}\n\nfunction timestamp(id: string): number {\n const bytes = parseUuid(id)\n let msecs = 0\n for (let i = 0; i < 6; i += 1) {\n msecs = msecs * 256 + bytes[i]\n }\n return msecs\n}\n\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && UUID_V7_REGEX.test(id)\n}\n\n/**\n * Generate a UUID v7 string or write the bytes into a buffer.\n *\n * UUID v7 is a time-ordered UUID that embeds a Unix timestamp in milliseconds,\n * making IDs naturally sortable by creation time. Ideal for database primary keys\n * where chronological ordering improves index performance.\n *\n * @example\n * ```ts\n * import { uuidv7 } from 'uniku/uuid/v7'\n *\n * const id = uuidv7()\n * // => \"018e5e5c-7c8a-7000-8000-000000000000\"\n *\n * // Extract timestamp\n * const ts = uuidv7.timestamp(id)\n * console.log(new Date(ts))\n *\n * // Validate\n * uuidv7.isValid(id) // true\n *\n * // Convert to/from bytes\n * const bytes = uuidv7.toBytes(id)\n * const restored = uuidv7.fromBytes(bytes)\n * ```\n */\nexport const uuidv7: UuidV7 = Object.assign(v7, {\n toBytes: parseUuid,\n fromBytes: formatUuid,\n timestamp,\n isValid,\n NIL: '00000000-0000-0000-0000-000000000000',\n MAX: 'ffffffff-ffff-ffff-ffff-ffffffffffff',\n})\n\nexport { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'\n"],"mappings":"iMA4BA,MAAM,EAAgB,yEAIhB,EAAc,IAAI,WAAW,EAAE,EAe/B,EAAiB,CAAE,MAAO,KAAW,IAAK,CAAE,EAElD,SAAS,EACP,EACA,EACA,EACA,EACA,EAAS,EACG,CACZ,GAAI,EAAK,OAAS,GAChB,MAAM,IAAI,EAAkB,8BAA+B,mCAAmC,EAGhG,GAAI,EAAS,GAAK,EAAS,GAAK,EAAI,OAClC,MAAM,IAAI,EACR,4BACA,mBAAmB,EAAO,GAAG,EAAS,GAAG,yBAC3C,EA8BF,MA3BA,KAAU,KAAK,IAAI,EAGnB,IAAS,EAAK,IAAM,GAAO,EAAK,IAAM,GAAO,EAAK,IAAM,EAAK,EAAK,GAIlE,EAAI,KAAa,EAAQ,cAAiB,IAC1C,EAAI,KAAa,EAAQ,WAAe,IACxC,EAAI,KAAa,EAAQ,SAAa,IACtC,EAAI,KAAa,EAAQ,MAAW,IACpC,EAAI,KAAa,EAAQ,IAAS,IAClC,EAAI,KAAY,EAAQ,IAGxB,EAAI,KAAY,IAAS,IAAQ,GAAM,GACvC,EAAI,KAAa,IAAQ,GAAM,IAC/B,EAAI,KAAY,IAAS,IAAQ,GAAM,GACvC,EAAI,KAAa,IAAQ,EAAK,IAE9B,EAAI,KAAc,GAAO,EAAK,IAAS,EAAK,IAAM,EAClD,EAAI,KAAY,EAAK,IACrB,EAAI,KAAY,EAAK,IACrB,EAAI,KAAY,EAAK,IACrB,EAAI,KAAY,EAAK,IACrB,EAAI,KAAY,EAAK,IAEd,CACT,CAUA,SAAS,EAAyC,EAAyB,EAAY,EAAgC,CACrH,IAAI,EAEJ,GAAI,EACF,EAAQ,EAAQ,EAAQ,QAAU,EAAI,EAAG,EAAQ,MAAO,EAAQ,IAAK,GAAO,EAAa,EAAM,EAAS,CAAC,MACpG,CAEL,IAAM,EAAM,KAAK,IAAI,EACf,EAAO,EAAI,EAGb,EAAM,EAAM,OACd,EAAM,IAAO,EAAK,IAAM,GAAO,EAAK,IAAM,GAAO,EAAK,IAAM,EAAK,EAAK,GACtE,EAAM,MAAQ,IAEd,EAAM,IAAO,EAAM,IAAM,EAAK,EAC1B,EAAM,IAAM,IACd,EAAM,IAAM,EACZ,EAAM,UAIV,EAAQ,EAAQ,EAAM,EAAM,MAAO,EAAM,IAAK,GAAO,EAAa,EAAM,EAAS,CAAC,CACpF,CAEA,OAAO,EAAO,EAAiB,EAAW,CAAK,CACjD,CAEA,SAAS,EAAU,EAAoB,CACrC,IAAM,EAAQ,EAAU,CAAE,EACtB,EAAQ,EACZ,IAAK,IAAI,EAAI,EAAG,EAAI,EAAG,GAAK,EAC1B,EAAQ,EAAQ,IAAM,EAAM,GAE9B,OAAO,CACT,CAEA,SAAS,EAAQ,EAA2B,CAC1C,OAAO,OAAO,GAAO,UAAY,EAAc,KAAK,CAAE,CACxD,CA4BA,MAAa,EAAiB,OAAO,OAAO,EAAI,CAC9C,QAAS,EACT,UAAW,EACX,YACA,UACA,IAAK,uCACL,IAAK,sCACP,CAAC"}
|
|
@@ -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-BBjwHv7J.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uuid-BBjwHv7J.mjs","names":[],"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,EACT,CCpBA,MAMM,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,EACF,EAGM,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,EACF,EAMM,EAAsB,MAAM,KAAK,CAAE,OAAQ,GAAI,GAAI,EAAG,IAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,EAEjG,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,IAEpB,CAEA,SAAgB,EAAU,EAA2B,CACnD,GAAI,EAAM,SAAW,GACnB,MAAM,IAAI,EAAW,sBAAuB,0CAA0C,EAAM,QAAQ,EAItG,GAAI,EAAM,KAAO,KAAO,EAAM,MAAQ,KAAO,EAAM,MAAQ,KAAO,EAAM,MAAQ,IAC9E,MAAM,IAAI,EACR,0BACA,6EAA6E,EAAM,EACrF,EAKF,IAAM,EAAQ,IAAI,WAAW,EAAgB,EAE7C,IAAK,IAAI,EAAI,EAAG,EAAI,GAAoB,GAAK,EAAG,CAC9C,IAAM,EAAU,EAAkB,GAClC,GAAI,IAAY,GAAI,SAEpB,IAAM,EAAS,EAAS,EAAM,WAAW,CAAC,CAAC,EAC3C,GAAI,IAAW,GACb,MAAM,IAAI,EAAW,wBAAyB,0DAA0D,GAAG,EAGzG,EAAkB,GACpB,EAAM,GAAW,GAAU,EAE3B,EAAM,IAAY,CAEtB,CAEA,OAAO,CACT"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"private": false,
|
|
3
3
|
"name": "uniku",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Minimal, tree-shakeable unique ID generators for every JavaScript runtime",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alberto Schiabel",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"ksuid",
|
|
30
30
|
"objectid",
|
|
31
31
|
"mongodb",
|
|
32
|
+
"tsid",
|
|
33
|
+
"snowflake",
|
|
32
34
|
"unique",
|
|
33
35
|
"id",
|
|
34
36
|
"identifier",
|
|
@@ -83,6 +85,11 @@
|
|
|
83
85
|
"import": "./build/cuid2/cuid2.mjs",
|
|
84
86
|
"default": "./build/cuid2/cuid2.mjs"
|
|
85
87
|
},
|
|
88
|
+
"./cuid/v2": {
|
|
89
|
+
"types": "./build/cuid/v2.d.mts",
|
|
90
|
+
"import": "./build/cuid/v2.mjs",
|
|
91
|
+
"default": "./build/cuid/v2.mjs"
|
|
92
|
+
},
|
|
86
93
|
"./nanoid": {
|
|
87
94
|
"types": "./build/nanoid/nanoid.d.mts",
|
|
88
95
|
"import": "./build/nanoid/nanoid.mjs",
|
|
@@ -98,25 +105,38 @@
|
|
|
98
105
|
"import": "./build/objectid/objectid.mjs",
|
|
99
106
|
"default": "./build/objectid/objectid.mjs"
|
|
100
107
|
},
|
|
108
|
+
"./tsid": {
|
|
109
|
+
"types": "./build/tsid/tsid.d.mts",
|
|
110
|
+
"import": "./build/tsid/tsid.mjs",
|
|
111
|
+
"default": "./build/tsid/tsid.mjs"
|
|
112
|
+
},
|
|
101
113
|
"./errors": {
|
|
102
114
|
"types": "./build/errors.d.mts",
|
|
103
115
|
"import": "./build/errors.mjs",
|
|
104
116
|
"default": "./build/errors.mjs"
|
|
105
117
|
},
|
|
118
|
+
"./generators": {
|
|
119
|
+
"types": "./build/generators.d.mts",
|
|
120
|
+
"import": "./build/generators.mjs",
|
|
121
|
+
"default": "./build/generators.mjs"
|
|
122
|
+
},
|
|
106
123
|
"./package.json": "./package.json"
|
|
107
124
|
},
|
|
108
125
|
"dependencies": {
|
|
109
|
-
"@noble/hashes": "^2.0
|
|
126
|
+
"@noble/hashes": "^2.2.0"
|
|
110
127
|
},
|
|
111
128
|
"devDependencies": {
|
|
129
|
+
"@effect/platform-bun": "4.0.0-beta.93",
|
|
112
130
|
"@owpz/ksuid": "^25.7.20",
|
|
113
131
|
"@paralleldrive/cuid2": "^3.3.0",
|
|
114
|
-
"@types/bun": "^1.3.
|
|
132
|
+
"@types/bun": "^1.3.14",
|
|
133
|
+
"effect": "4.0.0-beta.93",
|
|
115
134
|
"bson": "^7.3.1",
|
|
116
|
-
"nanoid": "^5.1.
|
|
135
|
+
"nanoid": "^5.1.16",
|
|
136
|
+
"tsid-ts": "^0.0.9",
|
|
117
137
|
"typeid-js": "1.2.0",
|
|
118
138
|
"ulid": "3.0.2",
|
|
119
|
-
"uuid": "
|
|
139
|
+
"uuid": "14.0.1"
|
|
120
140
|
},
|
|
121
141
|
"scripts": {
|
|
122
142
|
"clean": "rimraf build .turbo",
|
|
@@ -125,9 +145,11 @@
|
|
|
125
145
|
"test": "vitest run",
|
|
126
146
|
"test:unit": "vitest --project unit run",
|
|
127
147
|
"test:integration": "vitest --project integration run",
|
|
148
|
+
"test:coverage": "vitest run --coverage",
|
|
128
149
|
"test:e2e": "pnpm --filter @uniku/e2e-cloudflare test:e2e",
|
|
129
150
|
"test:e2e:cloudflare": "pnpm --filter @uniku/e2e-cloudflare test:e2e:cloudflare",
|
|
130
151
|
"smoke:publish": "node scripts/publish-smoke.mjs",
|
|
152
|
+
"smoke:runtime:browser": "bun scripts/browser-smoke.ts",
|
|
131
153
|
"smoke:runtime:bun": "bun scripts/runtime-smoke.mjs",
|
|
132
154
|
"smoke:runtime:deno": "deno run --node-modules-dir=manual scripts/runtime-smoke.mjs",
|
|
133
155
|
"bench": "vitest bench --run --project unit",
|
package/src/cuid/v2.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uniku/cuid/v2` — the canonical, versioned entry point for the CUID2 generator.
|
|
3
|
+
*
|
|
4
|
+
* This mirrors the versioned-subpath convention used by `uniku/uuid/v4` and
|
|
5
|
+
* `uniku/uuid/v7`, and supersedes the now-`@deprecated` `uniku/cuid2` entry
|
|
6
|
+
* point. It re-exports the single existing implementation under the `cuidv2`
|
|
7
|
+
* name — there is no second implementation.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { cuidv2 } from 'uniku/cuid/v2'
|
|
12
|
+
*
|
|
13
|
+
* const id = cuidv2()
|
|
14
|
+
* // => "pfh0haxfpzowht3oi213cqos"
|
|
15
|
+
*
|
|
16
|
+
* cuidv2.isValid(id) // true
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export { type Cuid2 as CuidV2, type Cuid2Options as CuidV2Options, cuid2 as cuidv2 } from '../cuid2/cuid2'
|
package/src/cuid2/cuid2.ts
CHANGED
|
@@ -203,6 +203,10 @@ function isValid(id: unknown): id is string {
|
|
|
203
203
|
* console.log(maybeId.length) // TypeScript knows maybeId is string
|
|
204
204
|
* }
|
|
205
205
|
* ```
|
|
206
|
+
*
|
|
207
|
+
* @deprecated Use `cuidv2` from `uniku/cuid/v2` instead. This entry point keeps
|
|
208
|
+
* working unchanged, but `uniku/cuid/v2` is the canonical versioned subpath
|
|
209
|
+
* (mirroring `uniku/uuid/v4` / `uniku/uuid/v7`).
|
|
206
210
|
*/
|
|
207
211
|
export const cuid2: Cuid2 = Object.assign(cuid2Fn, {
|
|
208
212
|
isValid,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical, ordered list of ID generators supported by uniku.
|
|
3
|
+
*
|
|
4
|
+
* This is the single source of truth for the set of supported ID generators.
|
|
5
|
+
* Tools that need to enumerate or validate ID generators (for example,
|
|
6
|
+
* `@uniku/cli`'s `--type` flag) should derive from this array rather than
|
|
7
|
+
* hand-copying the union.
|
|
8
|
+
*/
|
|
9
|
+
// Plain `as const` (not `as const satisfies readonly string[]`): the `satisfies`
|
|
10
|
+
// wrapper defeats isolatedDeclarations' trivial declaration emit for this export.
|
|
11
|
+
// All members are string literals, so the shape guard adds nothing here anyway.
|
|
12
|
+
export const ID_GENERATORS = ['uuid', 'ulid', 'typeid', 'nanoid', 'cuid', 'ksuid', 'objectid', 'tsid'] as const
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The union of ID generators supported by uniku, derived from {@link ID_GENERATORS}.
|
|
16
|
+
*/
|
|
17
|
+
export type IdGenerator = (typeof ID_GENERATORS)[number]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ParseError } from '../errors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Crockford Base32 codec for TSID, operating directly on the primary `bigint`
|
|
5
|
+
* value rather than an intermediate byte array (unlike ulid/crockford.ts) -
|
|
6
|
+
* tsid's primary type never routes through a `Uint8Array` during string
|
|
7
|
+
* conversion.
|
|
8
|
+
*
|
|
9
|
+
* TSID binary format: 64 bits (8 bytes), encoded as 13 Crockford Base32 characters.
|
|
10
|
+
* 13 chars * 5 bits = 65 bits of capacity for a 64-bit value, leaving exactly 1 bit
|
|
11
|
+
* of headroom on the leading character. That headroom bit is always 0, so the
|
|
12
|
+
* leading character only carries 4 significant real bits (values 0-15, i.e. the
|
|
13
|
+
* alphabet's first 16 symbols "0"-"9"/"A"-"F") rather than the full 32-symbol
|
|
14
|
+
* range - the same overflow-prevention technique ULID uses for its own leading
|
|
15
|
+
* `[0-7]` restriction (2 headroom bits there vs 1 here). Confirmed against
|
|
16
|
+
* `tsid-ts`'s own `toCanonicalString`: `(number >> BigInt(60 - i * 5)) & 0x1f`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const TSID_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
|
20
|
+
const TSID_STRING_LEN = 13
|
|
21
|
+
const TSID_LEADING_MAX = 15 // 4 significant bits on the leading character (see above)
|
|
22
|
+
|
|
23
|
+
// Pre-computed decode table (case-insensitive) covering ASCII, so lookups never
|
|
24
|
+
// go out of bounds and a single `=== 255` check rejects invalid input.
|
|
25
|
+
const DECODING = new Uint8Array(128).fill(255)
|
|
26
|
+
for (let i = 0; i < TSID_ALPHABET.length; i += 1) {
|
|
27
|
+
const char = TSID_ALPHABET[i]
|
|
28
|
+
const upperCode = TSID_ALPHABET.charCodeAt(i)
|
|
29
|
+
DECODING[upperCode] = i
|
|
30
|
+
// Only letters have a lowercase counterpart - applying +32 to digit codes
|
|
31
|
+
// would land on unrelated uppercase letters (e.g. '5' + 32 -> 'U', which is
|
|
32
|
+
// excluded from this alphabet and would never get overwritten back to 255).
|
|
33
|
+
if (char >= 'A' && char <= 'Z') {
|
|
34
|
+
DECODING[upperCode + 32] = i
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Encode a TSID `bigint` value to its 13-character canonical Crockford Base32 string.
|
|
40
|
+
*/
|
|
41
|
+
export function encodeTsidString(value: bigint): string {
|
|
42
|
+
let result = ''
|
|
43
|
+
for (let i = 0; i < TSID_STRING_LEN; i += 1) {
|
|
44
|
+
const shift = BigInt(60 - i * 5)
|
|
45
|
+
const group = Number((value >> shift) & 0x1fn)
|
|
46
|
+
result += TSID_ALPHABET[group]
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decode a 13-character canonical Crockford Base32 string to a TSID `bigint` value.
|
|
53
|
+
*/
|
|
54
|
+
export function decodeTsidString(str: string): bigint {
|
|
55
|
+
if (str.length !== TSID_STRING_LEN) {
|
|
56
|
+
throw new ParseError('TSID_INVALID_LENGTH', `TSID string must be ${TSID_STRING_LEN} characters, got ${str.length}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let value = 0n
|
|
60
|
+
for (let i = 0; i < TSID_STRING_LEN; i += 1) {
|
|
61
|
+
const code = str.charCodeAt(i)
|
|
62
|
+
const group = code < 128 ? DECODING[code] : 255
|
|
63
|
+
if (group === 255) {
|
|
64
|
+
throw new ParseError('TSID_INVALID_CHAR', `Invalid TSID character: ${str[i]}`)
|
|
65
|
+
}
|
|
66
|
+
if (i === 0 && group > TSID_LEADING_MAX) {
|
|
67
|
+
throw new ParseError(
|
|
68
|
+
'TSID_LEADING_CHAR_OUT_OF_RANGE',
|
|
69
|
+
`TSID leading character must be one of 0-9, A-F, got: ${str[i]}`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
value = (value << 5n) | BigInt(group)
|
|
73
|
+
}
|
|
74
|
+
return value
|
|
75
|
+
}
|
package/src/tsid/tsid.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { randomUint32 } from '../common/random'
|
|
2
|
+
import { BufferError, InvalidInputError } from '../errors'
|
|
3
|
+
import { decodeTsidString, encodeTsidString } from './crockford64'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TSID (Time-Sorted Unique Identifier)
|
|
7
|
+
*
|
|
8
|
+
* A 64-bit Snowflake-style identifier consisting of:
|
|
9
|
+
* - 42 bits: millisecond timestamp relative to a custom epoch (default 2020-01-01T00:00:00.000Z)
|
|
10
|
+
* - 10 bits: node ID (default; configurable via `nodeBits`)
|
|
11
|
+
* - 12 bits: per-millisecond counter (default; `22 - nodeBits`)
|
|
12
|
+
*
|
|
13
|
+
* Unlike every other uniku generator, `tsid()` returns a `bigint` by default -
|
|
14
|
+
* this is a deliberate design decision reflecting that TSID's value proposition
|
|
15
|
+
* is native numeric storage (e.g. a database BIGINT primary key), not a
|
|
16
|
+
* string-first identifier that happens to have a byte encoding. `toString`/
|
|
17
|
+
* `fromString` are the boundary conversions to/from the 13-character canonical
|
|
18
|
+
* Crockford Base32 string.
|
|
19
|
+
*
|
|
20
|
+
* Encoded as 8 big-endian bytes in buffer mode.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const TSID_EPOCH = 1577836800000 // 2020-01-01T00:00:00.000Z
|
|
24
|
+
const TSID_BYTES = 8
|
|
25
|
+
const RANDOM_BITS = 22 // combined node + counter bits
|
|
26
|
+
const DEFAULT_NODE_BITS = 10
|
|
27
|
+
const DEFAULT_COUNTER_BITS = RANDOM_BITS - DEFAULT_NODE_BITS
|
|
28
|
+
const DEFAULT_NODE_MASK = (1 << DEFAULT_NODE_BITS) - 1
|
|
29
|
+
const DEFAULT_COUNTER_MASK = (1 << DEFAULT_COUNTER_BITS) - 1
|
|
30
|
+
const MAX_NODE_BITS = 20
|
|
31
|
+
const MAX_TIMESTAMP_DIFF = (1n << 42n) - 1n
|
|
32
|
+
|
|
33
|
+
export type TsidOptions = {
|
|
34
|
+
/**
|
|
35
|
+
* Timestamp in milliseconds since Unix epoch.
|
|
36
|
+
* Defaults to Date.now().
|
|
37
|
+
*/
|
|
38
|
+
msecs?: number
|
|
39
|
+
/**
|
|
40
|
+
* Custom epoch in milliseconds since Unix epoch.
|
|
41
|
+
* Defaults to 1577836800000 (2020-01-01T00:00:00.000Z).
|
|
42
|
+
*/
|
|
43
|
+
epoch?: number
|
|
44
|
+
/**
|
|
45
|
+
* Node ID (0 to 2^nodeBits - 1).
|
|
46
|
+
* Defaults to a lazily-initialized, persistent random value.
|
|
47
|
+
*/
|
|
48
|
+
node?: number
|
|
49
|
+
/**
|
|
50
|
+
* Number of bits allocated to the node ID (0-20).
|
|
51
|
+
* The remaining bits (22 - nodeBits) are allocated to the counter.
|
|
52
|
+
* Defaults to 10.
|
|
53
|
+
*/
|
|
54
|
+
nodeBits?: number
|
|
55
|
+
/**
|
|
56
|
+
* Per-millisecond counter (0 to 2^(22 - nodeBits) - 1).
|
|
57
|
+
* Defaults to a fresh random value on a new millisecond, or the previous
|
|
58
|
+
* value incremented by 1 within the same millisecond.
|
|
59
|
+
*/
|
|
60
|
+
counter?: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type Tsid = {
|
|
64
|
+
(): bigint
|
|
65
|
+
<TBuf extends Uint8Array = Uint8Array>(options: TsidOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
66
|
+
(options?: TsidOptions, buf?: undefined, offset?: number): bigint
|
|
67
|
+
toBytes(id: bigint): Uint8Array
|
|
68
|
+
fromBytes(bytes: Uint8Array): bigint
|
|
69
|
+
toString(id: bigint): string
|
|
70
|
+
fromString(str: string): bigint
|
|
71
|
+
timestamp(id: bigint, epoch?: number): number
|
|
72
|
+
isValid(id: unknown): id is bigint
|
|
73
|
+
/** The nil TSID (all zeros) */
|
|
74
|
+
NIL: bigint
|
|
75
|
+
/** The max TSID (maximum valid 64-bit value) */
|
|
76
|
+
MAX: bigint
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type TsidState = {
|
|
80
|
+
node: number | undefined
|
|
81
|
+
msecs: number
|
|
82
|
+
counter: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Module-level state for the lazily-initialized persistent node ID and the
|
|
87
|
+
* per-millisecond-reset counter.
|
|
88
|
+
*
|
|
89
|
+
* IMPORTANT: This state persists across all tsid() calls in the module's lifetime.
|
|
90
|
+
* - In serverless/edge functions with warm starts, state persists between invocations.
|
|
91
|
+
* - For isolated state, pass explicit `msecs`, `epoch`, `node`, `nodeBits`, or
|
|
92
|
+
* `counter` via options - doing so bypasses `state` entirely.
|
|
93
|
+
* - Tests should mock Date.now() or provide explicit options for deterministic behavior.
|
|
94
|
+
*/
|
|
95
|
+
const state: TsidState = {
|
|
96
|
+
node: undefined,
|
|
97
|
+
msecs: -Infinity,
|
|
98
|
+
counter: 0,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pack(msecsDiff: bigint, node: number, counterBits: number, counter: number): bigint {
|
|
102
|
+
return (msecsDiff << BigInt(RANDOM_BITS)) | (BigInt(node) << BigInt(counterBits)) | BigInt(counter)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function writeTsidBytes(value: bigint, buf?: Uint8Array, offset = 0): Uint8Array {
|
|
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
|
+
|
|
116
|
+
for (let i = 0; i < TSID_BYTES; i += 1) {
|
|
117
|
+
buf[offset + i] = Number((value >> BigInt((TSID_BYTES - 1 - i) * 8)) & 0xffn)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return buf
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* Overload: no buffer => return a TSID bigint.
|
|
125
|
+
*/
|
|
126
|
+
function tsidFn(options?: TsidOptions, buf?: undefined, offset?: number): bigint
|
|
127
|
+
/*
|
|
128
|
+
* Overload: caller provides a buffer slice to fill with TSID bytes.
|
|
129
|
+
*/
|
|
130
|
+
function tsidFn<TBuf extends Uint8Array = Uint8Array>(
|
|
131
|
+
options: TsidOptions | undefined,
|
|
132
|
+
buf: TBuf,
|
|
133
|
+
offset?: number,
|
|
134
|
+
): TBuf
|
|
135
|
+
function tsidFn<TBuf extends Uint8Array = Uint8Array>(options?: TsidOptions, buf?: TBuf, offset = 0): bigint | TBuf {
|
|
136
|
+
let msecsDiff: bigint
|
|
137
|
+
let node: number
|
|
138
|
+
let counterBits: number
|
|
139
|
+
let counter: number
|
|
140
|
+
|
|
141
|
+
if (options) {
|
|
142
|
+
const nodeBits = options.nodeBits ?? DEFAULT_NODE_BITS
|
|
143
|
+
if (nodeBits < 0 || nodeBits > MAX_NODE_BITS) {
|
|
144
|
+
throw new InvalidInputError('TSID_NODE_BITS_OUT_OF_RANGE', `nodeBits must be between 0 and ${MAX_NODE_BITS}`)
|
|
145
|
+
}
|
|
146
|
+
counterBits = RANDOM_BITS - nodeBits
|
|
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
|
|
180
|
+
} else {
|
|
181
|
+
// Lazily initialize the persistent node ID on first no-option call.
|
|
182
|
+
if (state.node === undefined) {
|
|
183
|
+
state.node = randomUint32() & DEFAULT_NODE_MASK
|
|
184
|
+
}
|
|
185
|
+
node = state.node
|
|
186
|
+
counterBits = DEFAULT_COUNTER_BITS
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Note: by default, Cloudflare Workers "freezes" time during request handling
|
|
190
|
+
* to prevent side-channel attacks, so Date.now() returns the same value for
|
|
191
|
+
* the entire duration of a request. Monotonic ordering within such a request
|
|
192
|
+
* relies entirely on the counter (and its clock-drift-ahead overflow below).
|
|
193
|
+
*/
|
|
194
|
+
const now = Date.now()
|
|
195
|
+
if (now > state.msecs) {
|
|
196
|
+
state.msecs = now
|
|
197
|
+
state.counter = randomUint32() & DEFAULT_COUNTER_MASK
|
|
198
|
+
} else {
|
|
199
|
+
state.counter += 1
|
|
200
|
+
if (state.counter > DEFAULT_COUNTER_MASK) {
|
|
201
|
+
// Counter overflowed within this real millisecond: advance the internal
|
|
202
|
+
// virtual timestamp ahead of wall-clock time rather than throwing. A
|
|
203
|
+
// 12-bit counter (4096 values/ms) is a realistic throughput ceiling.
|
|
204
|
+
state.msecs += 1
|
|
205
|
+
state.counter = 0
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
counter = state.counter
|
|
209
|
+
msecsDiff = BigInt(state.msecs) - BigInt(TSID_EPOCH)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const packed = pack(msecsDiff, node, counterBits, counter)
|
|
213
|
+
|
|
214
|
+
if (buf) {
|
|
215
|
+
writeTsidBytes(packed, buf, offset)
|
|
216
|
+
return buf
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return packed
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Convert a TSID bigint to 8 bytes.
|
|
224
|
+
*/
|
|
225
|
+
function toBytes(id: bigint): Uint8Array {
|
|
226
|
+
return writeTsidBytes(id)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Convert 8 bytes to a TSID bigint.
|
|
231
|
+
*/
|
|
232
|
+
function fromBytes(bytes: Uint8Array): bigint {
|
|
233
|
+
if (bytes.length !== TSID_BYTES) {
|
|
234
|
+
throw new BufferError(
|
|
235
|
+
'TSID_BYTES_INVALID_LENGTH',
|
|
236
|
+
`TSID bytes must be exactly ${TSID_BYTES} bytes, got ${bytes.length}`,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let value = 0n
|
|
241
|
+
for (let i = 0; i < TSID_BYTES; i += 1) {
|
|
242
|
+
value = (value << 8n) | BigInt(bytes[i])
|
|
243
|
+
}
|
|
244
|
+
return value
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Extract the timestamp (milliseconds since Unix epoch) from a TSID.
|
|
249
|
+
* The epoch is not self-describing in the packed value - pass the same
|
|
250
|
+
* `epoch` used at generation time if it was overridden from the default.
|
|
251
|
+
*/
|
|
252
|
+
function timestamp(id: bigint, epoch: number = TSID_EPOCH): number {
|
|
253
|
+
return epoch + Number(id >> BigInt(RANDOM_BITS))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validate that a value is a TSID bigint within the 64-bit range.
|
|
258
|
+
*/
|
|
259
|
+
function isValid(id: unknown): id is bigint {
|
|
260
|
+
return typeof id === 'bigint' && id >= 0n && id <= MAX
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const NIL = 0n
|
|
264
|
+
const MAX = (1n << 64n) - 1n
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Generate a TSID bigint or write the bytes into a buffer.
|
|
268
|
+
*
|
|
269
|
+
* TSID (Time-Sorted Unique Identifier) is a 64-bit Snowflake-style identifier
|
|
270
|
+
* combining a millisecond timestamp, a node ID, and a per-millisecond counter.
|
|
271
|
+
* Unlike every other uniku generator, it returns a `bigint` by default, since
|
|
272
|
+
* TSID's entire value proposition is native numeric storage (e.g. a database
|
|
273
|
+
* BIGINT primary key).
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* import { tsid } from 'uniku/tsid'
|
|
278
|
+
*
|
|
279
|
+
* const id = tsid()
|
|
280
|
+
* // => 862301223059968074n
|
|
281
|
+
*
|
|
282
|
+
* // Canonical string form
|
|
283
|
+
* const str = tsid.toString(id)
|
|
284
|
+
* // => "0QXW2CK4XZM2A"
|
|
285
|
+
* tsid.fromString(str) === id // true
|
|
286
|
+
*
|
|
287
|
+
* // Extract timestamp
|
|
288
|
+
* const ts = tsid.timestamp(id)
|
|
289
|
+
* console.log(new Date(ts))
|
|
290
|
+
*
|
|
291
|
+
* // Validate
|
|
292
|
+
* tsid.isValid(id) // true
|
|
293
|
+
*
|
|
294
|
+
* // Convert to/from bytes (8 bytes)
|
|
295
|
+
* const bytes = tsid.toBytes(id)
|
|
296
|
+
* const restored = tsid.fromBytes(bytes)
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export const tsid: Tsid = Object.assign(tsidFn, {
|
|
300
|
+
toBytes,
|
|
301
|
+
fromBytes,
|
|
302
|
+
toString: encodeTsidString,
|
|
303
|
+
fromString: decodeTsidString,
|
|
304
|
+
timestamp,
|
|
305
|
+
isValid,
|
|
306
|
+
NIL,
|
|
307
|
+
MAX,
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
export { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'
|