uniku 0.3.2 → 0.4.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 +32 -0
- package/build/generators.d.mts +1 -1
- package/build/generators.d.mts.map +1 -1
- package/build/generators.mjs +1 -1
- package/build/generators.mjs.map +1 -1
- package/build/xid/xid.d.mts +31 -0
- package/build/xid/xid.d.mts.map +1 -0
- package/build/xid/xid.mjs +2 -0
- package/build/xid/xid.mjs.map +1 -0
- package/package.json +11 -3
- package/src/generators.ts +1 -1
- package/src/xid/base32hex.ts +79 -0
- package/src/xid/xid.ts +158 -0
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ console.log(first < second && second < third) // true
|
|
|
35
35
|
| KSUID | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
|
36
36
|
| ObjectID | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
|
37
37
|
| TSID | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
38
|
+
| XID | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
38
39
|
| Tree-shakeable | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
39
40
|
| ESM-only | ✅ | ✅¹ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
40
41
|
| Edge/Workers | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
|
@@ -67,6 +68,7 @@ Uses `globalThis.crypto` (Web Crypto API) — no Node.js-specific APIs.
|
|
|
67
68
|
| KSUID | **1.5× faster** |
|
|
68
69
|
| ObjectID | **1.1× faster** |
|
|
69
70
|
| TSID | **1.7× faster** |
|
|
71
|
+
| XID | See the current benchmark summary |
|
|
70
72
|
| UUID v7 | **1.1× faster** |
|
|
71
73
|
| Nanoid | **~comparable speed** |
|
|
72
74
|
| Nanoid (10 chars) | npm is 1.1× faster |
|
|
@@ -88,6 +90,7 @@ Uses `globalThis.crypto` (Web Crypto API) — no Node.js-specific APIs.
|
|
|
88
90
|
| Very high-volume distributed systems | **KSUID** | Time-ordered with 128-bit entropy |
|
|
89
91
|
| MongoDB `_id` compatibility | **ObjectID** | Drop-in match for MongoDB's native document ID format |
|
|
90
92
|
| Native 64-bit sortable integer ID | **TSID** | Fits a database `BIGINT` primary key, no UUID-sized overhead |
|
|
93
|
+
| Go rs/xid compatibility | **XID** | Compact, time-ordered 12-byte identifier |
|
|
91
94
|
|
|
92
95
|
### Detailed Guide
|
|
93
96
|
|
|
@@ -222,6 +225,7 @@ deno install npm:uniku
|
|
|
222
225
|
| `uniku/ksuid` | ~1.3 KB |
|
|
223
226
|
| `uniku/objectid` | ~1.3 KB |
|
|
224
227
|
| `uniku/tsid` | ~1.4 KB |
|
|
228
|
+
| `uniku/xid` | ~1.7 KB |
|
|
225
229
|
| `uniku/generators` | ~98 B |
|
|
226
230
|
|
|
227
231
|
* The CUID2 entry point imports SHA3-512 from `@noble/hashes`; this table's entry-point size excludes that external dependency.
|
|
@@ -375,6 +379,21 @@ const bytes = tsid.toBytes(id)
|
|
|
375
379
|
const restored = tsid.fromBytes(bytes)
|
|
376
380
|
```
|
|
377
381
|
|
|
382
|
+
### XID (time-ordered, rs/xid-compatible)
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
import { xid } from 'uniku/xid'
|
|
386
|
+
|
|
387
|
+
const id = xid()
|
|
388
|
+
// => "9m4e2mr0ui3e8a215n4g"
|
|
389
|
+
|
|
390
|
+
const bytes = xid.toBytes(id)
|
|
391
|
+
const restored = xid.fromBytes(bytes)
|
|
392
|
+
const timestamp = xid.timestamp(id)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
XID caches a random identity per runtime and uses a shared, always-incrementing counter. Supply `machineId`, `processId`, `secs`, and `counter` for deterministic output.
|
|
396
|
+
|
|
378
397
|
## Migrating to uniku
|
|
379
398
|
|
|
380
399
|
### From `uuid`
|
|
@@ -553,6 +572,19 @@ tsid.MAX // 18446744073709551615n
|
|
|
553
572
|
|
|
554
573
|
> Unlike every other generator listed here, `tsid()` returns `bigint` by default, and `toBytes`/`fromBytes`/`timestamp`/`isValid` all operate on that `bigint`. `toString`/`fromString` are the boundary conversions to/from the 13-character canonical Crockford Base32 string.
|
|
555
574
|
|
|
575
|
+
### `xid` (from `uniku/xid`)
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
xid(options?: XidOptions): string
|
|
579
|
+
xid(options: XidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
|
|
580
|
+
xid.toBytes(id: string): Uint8Array
|
|
581
|
+
xid.fromBytes(bytes: Uint8Array): string
|
|
582
|
+
xid.timestamp(id: string): number
|
|
583
|
+
xid.isValid(id: unknown): id is string
|
|
584
|
+
xid.NIL // "00000000000000000000"
|
|
585
|
+
xid.MAX // "vvvvvvvvvvvvvvvvvvvg"
|
|
586
|
+
```
|
|
587
|
+
|
|
556
588
|
## Documentation
|
|
557
589
|
|
|
558
590
|
For advanced usage, examples, and contributing guidelines, see the [full documentation on GitHub](https://github.com/jkomyno/uniku).
|
package/build/generators.d.mts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `@uniku/cli`'s `--type` flag) should derive from this array rather than
|
|
8
8
|
* hand-copying the union.
|
|
9
9
|
*/
|
|
10
|
-
declare const ID_GENERATORS: readonly ["uuid", "ulid", "typeid", "nanoid", "cuid", "ksuid", "objectid", "tsid"];
|
|
10
|
+
declare const ID_GENERATORS: readonly ["uuid", "ulid", "typeid", "nanoid", "cuid", "ksuid", "objectid", "tsid", "xid"];
|
|
11
11
|
/**
|
|
12
12
|
* The union of ID generators supported by uniku, derived from {@link ID_GENERATORS}.
|
|
13
13
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generators.d.mts","names":[],"sources":["../src/generators.ts"],"mappings":";;AAWA;;;;
|
|
1
|
+
{"version":3,"file":"generators.d.mts","names":[],"sources":["../src/generators.ts"],"mappings":";;AAWA;;;;AAAuG;AAKvG;;cALa,aAAA;;AAKqB;;KAAtB,WAAA,WAAsB,aAAA"}
|
package/build/generators.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=[`uuid`,`ulid`,`typeid`,`nanoid`,`cuid`,`ksuid`,`objectid`,`tsid`];export{e as ID_GENERATORS};
|
|
1
|
+
const e=[`uuid`,`ulid`,`typeid`,`nanoid`,`cuid`,`ksuid`,`objectid`,`tsid`,`xid`];export{e as ID_GENERATORS};
|
|
2
2
|
//# sourceMappingURL=generators.mjs.map
|
package/build/generators.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generators.mjs","names":[],"sources":["../src/generators.ts"],"sourcesContent":["/**\n * The canonical, ordered list of ID generators supported by uniku.\n *\n * This is the single source of truth for the set of supported ID generators.\n * Tools that need to enumerate or validate ID generators (for example,\n * `@uniku/cli`'s `--type` flag) should derive from this array rather than\n * hand-copying the union.\n */\n// Plain `as const` (not `as const satisfies readonly string[]`): the `satisfies`\n// wrapper defeats isolatedDeclarations' trivial declaration emit for this export.\n// All members are string literals, so the shape guard adds nothing here anyway.\nexport const ID_GENERATORS = ['uuid', 'ulid', 'typeid', 'nanoid', 'cuid', 'ksuid', 'objectid', 'tsid'] as const\n\n/**\n * The union of ID generators supported by uniku, derived from {@link ID_GENERATORS}.\n */\nexport type IdGenerator = (typeof ID_GENERATORS)[number]\n"],"mappings":"AAWA,MAAa,EAAgB,CAAC,OAAQ,OAAQ,SAAU,SAAU,OAAQ,QAAS,WAAY,
|
|
1
|
+
{"version":3,"file":"generators.mjs","names":[],"sources":["../src/generators.ts"],"sourcesContent":["/**\n * The canonical, ordered list of ID generators supported by uniku.\n *\n * This is the single source of truth for the set of supported ID generators.\n * Tools that need to enumerate or validate ID generators (for example,\n * `@uniku/cli`'s `--type` flag) should derive from this array rather than\n * hand-copying the union.\n */\n// Plain `as const` (not `as const satisfies readonly string[]`): the `satisfies`\n// wrapper defeats isolatedDeclarations' trivial declaration emit for this export.\n// All members are string literals, so the shape guard adds nothing here anyway.\nexport const ID_GENERATORS = ['uuid', 'ulid', 'typeid', 'nanoid', 'cuid', 'ksuid', 'objectid', 'tsid', 'xid'] as const\n\n/**\n * The union of ID generators supported by uniku, derived from {@link ID_GENERATORS}.\n */\nexport type IdGenerator = (typeof ID_GENERATORS)[number]\n"],"mappings":"AAWA,MAAa,EAAgB,CAAC,OAAQ,OAAQ,SAAU,SAAU,OAAQ,QAAS,WAAY,OAAQ,KAAK"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { i as UniqueIdError, n as InvalidInputError, r as ParseError, t as BufferError } from "../errors-Dgoyi-ci.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/xid/xid.d.ts
|
|
4
|
+
type XidOptions = {
|
|
5
|
+
/** First three bytes used as the XID machine identity. */machineId?: Uint8Array; /** 16-bit process identity. */
|
|
6
|
+
processId?: number; /** Unix timestamp in seconds. Defaults to the current second. */
|
|
7
|
+
secs?: number; /** 24-bit counter. Explicit values do not consume shared state. */
|
|
8
|
+
counter?: number;
|
|
9
|
+
};
|
|
10
|
+
type Xid = {
|
|
11
|
+
(): string;
|
|
12
|
+
<TBuf extends Uint8Array = Uint8Array>(options: XidOptions | undefined, buf: TBuf, offset?: number): TBuf;
|
|
13
|
+
(options?: XidOptions, buf?: undefined, offset?: number): string;
|
|
14
|
+
toBytes(id: string): Uint8Array;
|
|
15
|
+
fromBytes(bytes: Uint8Array): string;
|
|
16
|
+
timestamp(id: string): number;
|
|
17
|
+
isValid(id: unknown): id is string;
|
|
18
|
+
NIL: string;
|
|
19
|
+
MAX: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Generate a 20-character rs/xid-compatible identifier.
|
|
23
|
+
*
|
|
24
|
+
* XID embeds seconds, a lazily-random per-runtime identity, and a shared
|
|
25
|
+
* always-incrementing counter. In Cloudflare Workers, time is frozen during a
|
|
26
|
+
* request, so the counter preserves ordering for IDs made in that request.
|
|
27
|
+
*/
|
|
28
|
+
declare const xid: Xid;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { BufferError, InvalidInputError, ParseError, UniqueIdError, Xid, XidOptions, xid };
|
|
31
|
+
//# sourceMappingURL=xid.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xid.d.mts","names":[],"sources":["../../src/xid/xid.ts"],"mappings":";;;KAaY,UAAA;4DAEV,SAAA,GAAY,UAAA,EAFd;EAIE,SAAA;EAEA,IAAA;EAEA,OAAA;AAAA;AAAA,KAGU,GAAA;EAAA;EAAA,cAEI,UAAA,GAAa,UAAA,EAAY,OAAA,EAAS,UAAA,cAAwB,GAAA,EAAK,IAAA,EAAM,MAAA,YAAkB,IAAA;EAAA,CACpG,OAAA,GAAU,UAAA,EAAY,GAAA,cAAiB,MAAA;EACxC,OAAA,CAAQ,EAAA,WAAa,UAAA;EACrB,SAAA,CAAU,KAAA,EAAO,UAAA;EACjB,SAAA,CAAU,EAAA;EACV,OAAA,CAAQ,EAAA,YAAc,EAAA;EACtB,GAAA;EACA,GAAA;AAAA;;;;;;;;cAmHW,GAAA,EAAK,GAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{n as e,t}from"../random-Chp-Nkzi.mjs";import{n,t as r}from"../validation-CTNpXm94.mjs";import{BufferError as i,InvalidInputError as a,ParseError as o,UniqueIdError as s}from"../errors.mjs";import{n as c}from"../bytes-xqWxFYsM.mjs";const l=`0123456789abcdefghijklmnopqrstuv`,u=new Uint8Array(65536);u.fill(255);for(let e=0;e<32;e+=1)u[l.charCodeAt(e)]=e;function d(e){if(e.length!==12)throw new i(`XID_BYTES_INVALID_LENGTH`,`XID bytes must be exactly 12 bytes, got ${e.length}`);return l[e[0]>>3]+l[(e[0]<<2|e[1]>>6)&31]+l[e[1]>>1&31]+l[(e[1]<<4|e[2]>>4)&31]+l[(e[2]<<1|e[3]>>7)&31]+l[e[3]>>2&31]+l[(e[3]<<3|e[4]>>5)&31]+l[e[4]&31]+l[e[5]>>3]+l[(e[5]<<2|e[6]>>6)&31]+l[e[6]>>1&31]+l[(e[6]<<4|e[7]>>4)&31]+l[(e[7]<<1|e[8]>>7)&31]+l[e[8]>>2&31]+l[(e[8]<<3|e[9]>>5)&31]+l[e[9]&31]+l[e[10]>>3]+l[(e[10]<<2|e[11]>>6)&31]+l[e[11]>>1&31]+l[e[11]<<4&31]}function f(e){if(e.length!==20)throw new o(`XID_INVALID_LENGTH`,`XID string must be 20 characters, got ${e.length}`);let t=new Uint8Array(20);for(let n=0;n<20;n+=1){let r=u[e.charCodeAt(n)];if(r===255)throw new o(`XID_INVALID_CHAR`,`Invalid XID character: ${e[n]}`);t[n]=r}if(t[19]!==0&&t[19]!==16)throw new o(`XID_NON_CANONICAL`,`XID trailing bits must be canonically encoded`);return new Uint8Array([t[0]<<3|t[1]>>2,t[1]<<6|t[2]<<1|t[3]>>4,t[3]<<4|t[4]>>1,t[4]<<7|t[5]<<2|t[6]>>3,t[6]<<5|t[7],t[8]<<3|t[9]>>2,t[9]<<6|t[10]<<1|t[11]>>4,t[11]<<4|t[12]>>1,t[12]<<7|t[13]<<2|t[14]>>3,t[14]<<5|t[15],t[16]<<3|t[17]>>2,t[17]<<6|t[18]<<1|t[19]>>4])}const p=4294967295,m=65535,h=16777215,g=/^[0-9a-v]{19}[0g]$/,_={machineId:void 0,processId:void 0,counter:void 0};function v(e){return t(e).slice()}function y(){_.machineId===void 0&&(_.machineId=v(3)),_.processId===void 0&&(_.processId=e()&m)}function b(){return _.counter===void 0&&(_.counter=e()&h),_.counter=_.counter+1&h,_.counter}function x(e,t,n,r,i,a){c(i,a,e),i.set(t.subarray(0,3),a+4),i[a+7]=n>>>8,i[a+8]=n&255,i[a+9]=r>>>16,i[a+10]=r>>>8&255,i[a+11]=r&255}function S(e){if(e.machineId!==void 0&&e.machineId.length<3)throw new a(`XID_MACHINE_ID_BYTES_TOO_SHORT`,`Machine ID bytes length must be >= 3 for XID`);if(e.processId!==void 0&&!r(e.processId,0,m))throw new a(`XID_PROCESS_ID_OUT_OF_RANGE`,`Process ID must be between 0 and ${m}`);if(e.secs!==void 0&&!r(e.secs,0,p))throw new a(`XID_TIMESTAMP_OUT_OF_RANGE`,`Timestamp must be between 0 and ${p}`);if(e.counter!==void 0&&!r(e.counter,0,h))throw new a(`XID_COUNTER_OUT_OF_RANGE`,`Counter must be between 0 and ${h}`)}function C(e,t,r=0){e!==void 0&&S(e);let a=e?.secs??Math.floor(Date.now()/1e3);(e?.machineId===void 0||e.processId===void 0)&&y();let o=e?.machineId??_.machineId,s=e?.processId??_.processId,c=e?.counter??b();if(t!==void 0){if(!n(t,r,12))throw new i(`XID_BUFFER_OUT_OF_BOUNDS`,`XID byte range ${r}:${r+11} is out of buffer bounds`);return x(a,o,s,c,t,r),t}let l=new Uint8Array(12);return x(a,o,s,c,l,0),d(l)}function w(e){return f(e)}function T(e){return d(e)}function E(e){let t=f(e);return((t[0]<<24|t[1]<<16|t[2]<<8|t[3])>>>0)*1e3}function D(e){return typeof e==`string`&&g.test(e)}const O=Object.assign(C,{toBytes:w,fromBytes:T,timestamp:E,isValid:D,NIL:`0`.repeat(20),MAX:d(new Uint8Array(12).fill(255))});export{i as BufferError,a as InvalidInputError,o as ParseError,s as UniqueIdError,O as xid};
|
|
2
|
+
//# sourceMappingURL=xid.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xid.mjs","names":["XID_BYTES"],"sources":["../../src/xid/base32hex.ts","../../src/xid/xid.ts"],"sourcesContent":["import { BufferError, ParseError } from '../errors'\n\nconst ENCODING = '0123456789abcdefghijklmnopqrstuv'\nconst XID_BYTES = 12\nconst XID_LENGTH = 20\n\nconst DECODING = new Uint8Array(65536)\nDECODING.fill(255)\nfor (let i = 0; i < ENCODING.length; i += 1) {\n DECODING[ENCODING.charCodeAt(i)] = i\n}\n\n/** Encode the 12 canonical XID bytes as lowercase base32hex. */\nexport function encodeBase32Hex(bytes: Uint8Array): string {\n if (bytes.length !== XID_BYTES) {\n throw new BufferError(\n 'XID_BYTES_INVALID_LENGTH',\n `XID bytes must be exactly ${XID_BYTES} bytes, got ${bytes.length}`,\n )\n }\n\n return (\n ENCODING[bytes[0] >> 3] +\n ENCODING[((bytes[0] << 2) | (bytes[1] >> 6)) & 0x1f] +\n ENCODING[(bytes[1] >> 1) & 0x1f] +\n ENCODING[((bytes[1] << 4) | (bytes[2] >> 4)) & 0x1f] +\n ENCODING[((bytes[2] << 1) | (bytes[3] >> 7)) & 0x1f] +\n ENCODING[(bytes[3] >> 2) & 0x1f] +\n ENCODING[((bytes[3] << 3) | (bytes[4] >> 5)) & 0x1f] +\n ENCODING[bytes[4] & 0x1f] +\n ENCODING[bytes[5] >> 3] +\n ENCODING[((bytes[5] << 2) | (bytes[6] >> 6)) & 0x1f] +\n ENCODING[(bytes[6] >> 1) & 0x1f] +\n ENCODING[((bytes[6] << 4) | (bytes[7] >> 4)) & 0x1f] +\n ENCODING[((bytes[7] << 1) | (bytes[8] >> 7)) & 0x1f] +\n ENCODING[(bytes[8] >> 2) & 0x1f] +\n ENCODING[((bytes[8] << 3) | (bytes[9] >> 5)) & 0x1f] +\n ENCODING[bytes[9] & 0x1f] +\n ENCODING[bytes[10] >> 3] +\n ENCODING[((bytes[10] << 2) | (bytes[11] >> 6)) & 0x1f] +\n ENCODING[(bytes[11] >> 1) & 0x1f] +\n ENCODING[(bytes[11] << 4) & 0x1f]\n )\n}\n\n/** Decode a canonical lowercase base32hex XID string to its 12 bytes. */\nexport function decodeBase32Hex(id: string): Uint8Array {\n if (id.length !== XID_LENGTH) {\n throw new ParseError('XID_INVALID_LENGTH', `XID string must be ${XID_LENGTH} characters, got ${id.length}`)\n }\n\n const values = new Uint8Array(XID_LENGTH)\n for (let i = 0; i < XID_LENGTH; i += 1) {\n const value = DECODING[id.charCodeAt(i)]\n if (value === 255) {\n throw new ParseError('XID_INVALID_CHAR', `Invalid XID character: ${id[i]}`)\n }\n values[i] = value\n }\n\n if (values[19] !== 0 && values[19] !== 16) {\n throw new ParseError('XID_NON_CANONICAL', 'XID trailing bits must be canonically encoded')\n }\n\n return new Uint8Array([\n (values[0] << 3) | (values[1] >> 2),\n (values[1] << 6) | (values[2] << 1) | (values[3] >> 4),\n (values[3] << 4) | (values[4] >> 1),\n (values[4] << 7) | (values[5] << 2) | (values[6] >> 3),\n (values[6] << 5) | values[7],\n (values[8] << 3) | (values[9] >> 2),\n (values[9] << 6) | (values[10] << 1) | (values[11] >> 4),\n (values[11] << 4) | (values[12] >> 1),\n (values[12] << 7) | (values[13] << 2) | (values[14] >> 3),\n (values[14] << 5) | values[15],\n (values[16] << 3) | (values[17] >> 2),\n (values[17] << 6) | (values[18] << 1) | (values[19] >> 4),\n ])\n}\n","import { writeTimestamp32 } from '../common/bytes'\nimport { randomBytes, randomUint32 } from '../common/random'\nimport { isIntegerInRange, isWritableRange } from '../common/validation'\nimport { BufferError, InvalidInputError } from '../errors'\nimport { decodeBase32Hex, encodeBase32Hex } from './base32hex'\n\nconst XID_BYTES = 12\nconst MACHINE_ID_BYTES = 3\nconst MAX_SECS = 0xffffffff\nconst MAX_PROCESS_ID = 0xffff\nconst MAX_COUNTER = 0xffffff\nconst XID_REGEX = /^[0-9a-v]{19}[0g]$/\n\nexport type XidOptions = {\n /** First three bytes used as the XID machine identity. */\n machineId?: Uint8Array\n /** 16-bit process identity. */\n processId?: number\n /** Unix timestamp in seconds. Defaults to the current second. */\n secs?: number\n /** 24-bit counter. Explicit values do not consume shared state. */\n counter?: number\n}\n\nexport type Xid = {\n (): string\n <TBuf extends Uint8Array = Uint8Array>(options: XidOptions | undefined, buf: TBuf, offset?: number): TBuf\n (options?: XidOptions, 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 NIL: string\n MAX: string\n}\n\ntype XidState = {\n machineId: Uint8Array | undefined\n processId: number | undefined\n counter: number | undefined\n}\n\nconst state: XidState = { machineId: undefined, processId: undefined, counter: undefined }\n\n/** Copy pooled bytes before another random draw can refill their backing pool. */\nfunction freshRandom(count: number): Uint8Array {\n return randomBytes(count).slice()\n}\n\nfunction initializeIdentity(): void {\n if (state.machineId === undefined) state.machineId = freshRandom(MACHINE_ID_BYTES)\n if (state.processId === undefined) state.processId = randomUint32() & MAX_PROCESS_ID\n}\n\nfunction nextCounter(): number {\n if (state.counter === undefined) state.counter = randomUint32() & MAX_COUNTER\n state.counter = (state.counter + 1) & MAX_COUNTER\n return state.counter\n}\n\nfunction writeXidBytesUnchecked(\n secs: number,\n machineId: Uint8Array,\n processId: number,\n counter: number,\n buf: Uint8Array,\n offset: number,\n): void {\n writeTimestamp32(buf, offset, secs)\n buf.set(machineId.subarray(0, MACHINE_ID_BYTES), offset + 4)\n buf[offset + 7] = processId >>> 8\n buf[offset + 8] = processId & 0xff\n buf[offset + 9] = counter >>> 16\n buf[offset + 10] = (counter >>> 8) & 0xff\n buf[offset + 11] = counter & 0xff\n}\n\nfunction validateOptions(options: XidOptions): void {\n if (options.machineId !== undefined && options.machineId.length < MACHINE_ID_BYTES) {\n throw new InvalidInputError(\n 'XID_MACHINE_ID_BYTES_TOO_SHORT',\n `Machine ID bytes length must be >= ${MACHINE_ID_BYTES} for XID`,\n )\n }\n if (options.processId !== undefined && !isIntegerInRange(options.processId, 0, MAX_PROCESS_ID)) {\n throw new InvalidInputError('XID_PROCESS_ID_OUT_OF_RANGE', `Process ID must be between 0 and ${MAX_PROCESS_ID}`)\n }\n if (options.secs !== undefined && !isIntegerInRange(options.secs, 0, MAX_SECS)) {\n throw new InvalidInputError('XID_TIMESTAMP_OUT_OF_RANGE', `Timestamp must be between 0 and ${MAX_SECS}`)\n }\n if (options.counter !== undefined && !isIntegerInRange(options.counter, 0, MAX_COUNTER)) {\n throw new InvalidInputError('XID_COUNTER_OUT_OF_RANGE', `Counter must be between 0 and ${MAX_COUNTER}`)\n }\n}\n\nfunction xidFn(options?: XidOptions, buf?: undefined, offset?: number): string\nfunction xidFn<TBuf extends Uint8Array = Uint8Array>(options: XidOptions | undefined, buf: TBuf, offset?: number): TBuf\nfunction xidFn<TBuf extends Uint8Array = Uint8Array>(options?: XidOptions, buf?: TBuf, offset = 0): string | TBuf {\n if (options !== undefined) validateOptions(options)\n\n const secs = options?.secs ?? Math.floor(Date.now() / 1000)\n if (options?.machineId === undefined || options.processId === undefined) {\n initializeIdentity()\n }\n const machineId = options?.machineId ?? state.machineId!\n const processId = options?.processId ?? state.processId!\n const counter = options?.counter ?? nextCounter()\n\n if (buf !== undefined) {\n if (!isWritableRange(buf, offset, XID_BYTES)) {\n throw new BufferError(\n 'XID_BUFFER_OUT_OF_BOUNDS',\n `XID byte range ${offset}:${offset + 11} is out of buffer bounds`,\n )\n }\n writeXidBytesUnchecked(secs, machineId, processId, counter, buf, offset)\n return buf\n }\n\n const bytes = new Uint8Array(XID_BYTES)\n writeXidBytesUnchecked(secs, machineId, processId, counter, bytes, 0)\n return encodeBase32Hex(bytes)\n}\n\nfunction toBytes(id: string): Uint8Array {\n return decodeBase32Hex(id)\n}\n\nfunction fromBytes(bytes: Uint8Array): string {\n return encodeBase32Hex(bytes)\n}\n\nfunction timestamp(id: string): number {\n const bytes = decodeBase32Hex(id)\n return (((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0) * 1000\n}\n\nfunction isValid(id: unknown): id is string {\n return typeof id === 'string' && XID_REGEX.test(id)\n}\n\n/**\n * Generate a 20-character rs/xid-compatible identifier.\n *\n * XID embeds seconds, a lazily-random per-runtime identity, and a shared\n * always-incrementing counter. In Cloudflare Workers, time is frozen during a\n * request, so the counter preserves ordering for IDs made in that request.\n */\nexport const xid: Xid = Object.assign(xidFn, {\n toBytes,\n fromBytes,\n timestamp,\n isValid,\n NIL: '0'.repeat(20),\n MAX: encodeBase32Hex(new Uint8Array(12).fill(0xff)),\n})\n\nexport { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"private": false,
|
|
3
3
|
"name": "uniku",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "Minimal, tree-shakeable unique ID generators for every JavaScript runtime",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alberto Schiabel",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"nanoid",
|
|
29
29
|
"ksuid",
|
|
30
30
|
"objectid",
|
|
31
|
+
"xid",
|
|
31
32
|
"mongodb",
|
|
32
33
|
"tsid",
|
|
33
34
|
"snowflake",
|
|
@@ -110,6 +111,11 @@
|
|
|
110
111
|
"import": "./build/tsid/tsid.mjs",
|
|
111
112
|
"default": "./build/tsid/tsid.mjs"
|
|
112
113
|
},
|
|
114
|
+
"./xid": {
|
|
115
|
+
"types": "./build/xid/xid.d.mts",
|
|
116
|
+
"import": "./build/xid/xid.mjs",
|
|
117
|
+
"default": "./build/xid/xid.mjs"
|
|
118
|
+
},
|
|
113
119
|
"./errors": {
|
|
114
120
|
"types": "./build/errors.d.mts",
|
|
115
121
|
"import": "./build/errors.mjs",
|
|
@@ -130,13 +136,15 @@
|
|
|
130
136
|
"@owpz/ksuid": "^25.7.20",
|
|
131
137
|
"@paralleldrive/cuid2": "^3.3.0",
|
|
132
138
|
"@types/bun": "^1.3.14",
|
|
133
|
-
"effect": "4.0.0-beta.93",
|
|
134
139
|
"bson": "^7.3.1",
|
|
140
|
+
"effect": "4.0.0-beta.93",
|
|
135
141
|
"nanoid": "^5.1.16",
|
|
142
|
+
"nexid": "^2.1.0",
|
|
136
143
|
"tsid-ts": "^0.0.9",
|
|
137
144
|
"typeid-js": "1.2.0",
|
|
138
145
|
"ulid": "3.0.2",
|
|
139
|
-
"uuid": "14.0.1"
|
|
146
|
+
"uuid": "14.0.1",
|
|
147
|
+
"xid-ts": "^1.3.0"
|
|
140
148
|
},
|
|
141
149
|
"scripts": {
|
|
142
150
|
"clean": "rimraf build .turbo",
|
package/src/generators.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Plain `as const` (not `as const satisfies readonly string[]`): the `satisfies`
|
|
10
10
|
// wrapper defeats isolatedDeclarations' trivial declaration emit for this export.
|
|
11
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
|
|
12
|
+
export const ID_GENERATORS = ['uuid', 'ulid', 'typeid', 'nanoid', 'cuid', 'ksuid', 'objectid', 'tsid', 'xid'] as const
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The union of ID generators supported by uniku, derived from {@link ID_GENERATORS}.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BufferError, ParseError } from '../errors'
|
|
2
|
+
|
|
3
|
+
const ENCODING = '0123456789abcdefghijklmnopqrstuv'
|
|
4
|
+
const XID_BYTES = 12
|
|
5
|
+
const XID_LENGTH = 20
|
|
6
|
+
|
|
7
|
+
const DECODING = new Uint8Array(65536)
|
|
8
|
+
DECODING.fill(255)
|
|
9
|
+
for (let i = 0; i < ENCODING.length; i += 1) {
|
|
10
|
+
DECODING[ENCODING.charCodeAt(i)] = i
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Encode the 12 canonical XID bytes as lowercase base32hex. */
|
|
14
|
+
export function encodeBase32Hex(bytes: Uint8Array): string {
|
|
15
|
+
if (bytes.length !== XID_BYTES) {
|
|
16
|
+
throw new BufferError(
|
|
17
|
+
'XID_BYTES_INVALID_LENGTH',
|
|
18
|
+
`XID bytes must be exactly ${XID_BYTES} bytes, got ${bytes.length}`,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
ENCODING[bytes[0] >> 3] +
|
|
24
|
+
ENCODING[((bytes[0] << 2) | (bytes[1] >> 6)) & 0x1f] +
|
|
25
|
+
ENCODING[(bytes[1] >> 1) & 0x1f] +
|
|
26
|
+
ENCODING[((bytes[1] << 4) | (bytes[2] >> 4)) & 0x1f] +
|
|
27
|
+
ENCODING[((bytes[2] << 1) | (bytes[3] >> 7)) & 0x1f] +
|
|
28
|
+
ENCODING[(bytes[3] >> 2) & 0x1f] +
|
|
29
|
+
ENCODING[((bytes[3] << 3) | (bytes[4] >> 5)) & 0x1f] +
|
|
30
|
+
ENCODING[bytes[4] & 0x1f] +
|
|
31
|
+
ENCODING[bytes[5] >> 3] +
|
|
32
|
+
ENCODING[((bytes[5] << 2) | (bytes[6] >> 6)) & 0x1f] +
|
|
33
|
+
ENCODING[(bytes[6] >> 1) & 0x1f] +
|
|
34
|
+
ENCODING[((bytes[6] << 4) | (bytes[7] >> 4)) & 0x1f] +
|
|
35
|
+
ENCODING[((bytes[7] << 1) | (bytes[8] >> 7)) & 0x1f] +
|
|
36
|
+
ENCODING[(bytes[8] >> 2) & 0x1f] +
|
|
37
|
+
ENCODING[((bytes[8] << 3) | (bytes[9] >> 5)) & 0x1f] +
|
|
38
|
+
ENCODING[bytes[9] & 0x1f] +
|
|
39
|
+
ENCODING[bytes[10] >> 3] +
|
|
40
|
+
ENCODING[((bytes[10] << 2) | (bytes[11] >> 6)) & 0x1f] +
|
|
41
|
+
ENCODING[(bytes[11] >> 1) & 0x1f] +
|
|
42
|
+
ENCODING[(bytes[11] << 4) & 0x1f]
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Decode a canonical lowercase base32hex XID string to its 12 bytes. */
|
|
47
|
+
export function decodeBase32Hex(id: string): Uint8Array {
|
|
48
|
+
if (id.length !== XID_LENGTH) {
|
|
49
|
+
throw new ParseError('XID_INVALID_LENGTH', `XID string must be ${XID_LENGTH} characters, got ${id.length}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const values = new Uint8Array(XID_LENGTH)
|
|
53
|
+
for (let i = 0; i < XID_LENGTH; i += 1) {
|
|
54
|
+
const value = DECODING[id.charCodeAt(i)]
|
|
55
|
+
if (value === 255) {
|
|
56
|
+
throw new ParseError('XID_INVALID_CHAR', `Invalid XID character: ${id[i]}`)
|
|
57
|
+
}
|
|
58
|
+
values[i] = value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (values[19] !== 0 && values[19] !== 16) {
|
|
62
|
+
throw new ParseError('XID_NON_CANONICAL', 'XID trailing bits must be canonically encoded')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new Uint8Array([
|
|
66
|
+
(values[0] << 3) | (values[1] >> 2),
|
|
67
|
+
(values[1] << 6) | (values[2] << 1) | (values[3] >> 4),
|
|
68
|
+
(values[3] << 4) | (values[4] >> 1),
|
|
69
|
+
(values[4] << 7) | (values[5] << 2) | (values[6] >> 3),
|
|
70
|
+
(values[6] << 5) | values[7],
|
|
71
|
+
(values[8] << 3) | (values[9] >> 2),
|
|
72
|
+
(values[9] << 6) | (values[10] << 1) | (values[11] >> 4),
|
|
73
|
+
(values[11] << 4) | (values[12] >> 1),
|
|
74
|
+
(values[12] << 7) | (values[13] << 2) | (values[14] >> 3),
|
|
75
|
+
(values[14] << 5) | values[15],
|
|
76
|
+
(values[16] << 3) | (values[17] >> 2),
|
|
77
|
+
(values[17] << 6) | (values[18] << 1) | (values[19] >> 4),
|
|
78
|
+
])
|
|
79
|
+
}
|
package/src/xid/xid.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { writeTimestamp32 } from '../common/bytes'
|
|
2
|
+
import { randomBytes, randomUint32 } from '../common/random'
|
|
3
|
+
import { isIntegerInRange, isWritableRange } from '../common/validation'
|
|
4
|
+
import { BufferError, InvalidInputError } from '../errors'
|
|
5
|
+
import { decodeBase32Hex, encodeBase32Hex } from './base32hex'
|
|
6
|
+
|
|
7
|
+
const XID_BYTES = 12
|
|
8
|
+
const MACHINE_ID_BYTES = 3
|
|
9
|
+
const MAX_SECS = 0xffffffff
|
|
10
|
+
const MAX_PROCESS_ID = 0xffff
|
|
11
|
+
const MAX_COUNTER = 0xffffff
|
|
12
|
+
const XID_REGEX = /^[0-9a-v]{19}[0g]$/
|
|
13
|
+
|
|
14
|
+
export type XidOptions = {
|
|
15
|
+
/** First three bytes used as the XID machine identity. */
|
|
16
|
+
machineId?: Uint8Array
|
|
17
|
+
/** 16-bit process identity. */
|
|
18
|
+
processId?: number
|
|
19
|
+
/** Unix timestamp in seconds. Defaults to the current second. */
|
|
20
|
+
secs?: number
|
|
21
|
+
/** 24-bit counter. Explicit values do not consume shared state. */
|
|
22
|
+
counter?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type Xid = {
|
|
26
|
+
(): string
|
|
27
|
+
<TBuf extends Uint8Array = Uint8Array>(options: XidOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
28
|
+
(options?: XidOptions, buf?: undefined, offset?: number): string
|
|
29
|
+
toBytes(id: string): Uint8Array
|
|
30
|
+
fromBytes(bytes: Uint8Array): string
|
|
31
|
+
timestamp(id: string): number
|
|
32
|
+
isValid(id: unknown): id is string
|
|
33
|
+
NIL: string
|
|
34
|
+
MAX: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type XidState = {
|
|
38
|
+
machineId: Uint8Array | undefined
|
|
39
|
+
processId: number | undefined
|
|
40
|
+
counter: number | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const state: XidState = { machineId: undefined, processId: undefined, counter: undefined }
|
|
44
|
+
|
|
45
|
+
/** Copy pooled bytes before another random draw can refill their backing pool. */
|
|
46
|
+
function freshRandom(count: number): Uint8Array {
|
|
47
|
+
return randomBytes(count).slice()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function initializeIdentity(): void {
|
|
51
|
+
if (state.machineId === undefined) state.machineId = freshRandom(MACHINE_ID_BYTES)
|
|
52
|
+
if (state.processId === undefined) state.processId = randomUint32() & MAX_PROCESS_ID
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function nextCounter(): number {
|
|
56
|
+
if (state.counter === undefined) state.counter = randomUint32() & MAX_COUNTER
|
|
57
|
+
state.counter = (state.counter + 1) & MAX_COUNTER
|
|
58
|
+
return state.counter
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeXidBytesUnchecked(
|
|
62
|
+
secs: number,
|
|
63
|
+
machineId: Uint8Array,
|
|
64
|
+
processId: number,
|
|
65
|
+
counter: number,
|
|
66
|
+
buf: Uint8Array,
|
|
67
|
+
offset: number,
|
|
68
|
+
): void {
|
|
69
|
+
writeTimestamp32(buf, offset, secs)
|
|
70
|
+
buf.set(machineId.subarray(0, MACHINE_ID_BYTES), offset + 4)
|
|
71
|
+
buf[offset + 7] = processId >>> 8
|
|
72
|
+
buf[offset + 8] = processId & 0xff
|
|
73
|
+
buf[offset + 9] = counter >>> 16
|
|
74
|
+
buf[offset + 10] = (counter >>> 8) & 0xff
|
|
75
|
+
buf[offset + 11] = counter & 0xff
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateOptions(options: XidOptions): void {
|
|
79
|
+
if (options.machineId !== undefined && options.machineId.length < MACHINE_ID_BYTES) {
|
|
80
|
+
throw new InvalidInputError(
|
|
81
|
+
'XID_MACHINE_ID_BYTES_TOO_SHORT',
|
|
82
|
+
`Machine ID bytes length must be >= ${MACHINE_ID_BYTES} for XID`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
if (options.processId !== undefined && !isIntegerInRange(options.processId, 0, MAX_PROCESS_ID)) {
|
|
86
|
+
throw new InvalidInputError('XID_PROCESS_ID_OUT_OF_RANGE', `Process ID must be between 0 and ${MAX_PROCESS_ID}`)
|
|
87
|
+
}
|
|
88
|
+
if (options.secs !== undefined && !isIntegerInRange(options.secs, 0, MAX_SECS)) {
|
|
89
|
+
throw new InvalidInputError('XID_TIMESTAMP_OUT_OF_RANGE', `Timestamp must be between 0 and ${MAX_SECS}`)
|
|
90
|
+
}
|
|
91
|
+
if (options.counter !== undefined && !isIntegerInRange(options.counter, 0, MAX_COUNTER)) {
|
|
92
|
+
throw new InvalidInputError('XID_COUNTER_OUT_OF_RANGE', `Counter must be between 0 and ${MAX_COUNTER}`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function xidFn(options?: XidOptions, buf?: undefined, offset?: number): string
|
|
97
|
+
function xidFn<TBuf extends Uint8Array = Uint8Array>(options: XidOptions | undefined, buf: TBuf, offset?: number): TBuf
|
|
98
|
+
function xidFn<TBuf extends Uint8Array = Uint8Array>(options?: XidOptions, buf?: TBuf, offset = 0): string | TBuf {
|
|
99
|
+
if (options !== undefined) validateOptions(options)
|
|
100
|
+
|
|
101
|
+
const secs = options?.secs ?? Math.floor(Date.now() / 1000)
|
|
102
|
+
if (options?.machineId === undefined || options.processId === undefined) {
|
|
103
|
+
initializeIdentity()
|
|
104
|
+
}
|
|
105
|
+
const machineId = options?.machineId ?? state.machineId!
|
|
106
|
+
const processId = options?.processId ?? state.processId!
|
|
107
|
+
const counter = options?.counter ?? nextCounter()
|
|
108
|
+
|
|
109
|
+
if (buf !== undefined) {
|
|
110
|
+
if (!isWritableRange(buf, offset, XID_BYTES)) {
|
|
111
|
+
throw new BufferError(
|
|
112
|
+
'XID_BUFFER_OUT_OF_BOUNDS',
|
|
113
|
+
`XID byte range ${offset}:${offset + 11} is out of buffer bounds`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
writeXidBytesUnchecked(secs, machineId, processId, counter, buf, offset)
|
|
117
|
+
return buf
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const bytes = new Uint8Array(XID_BYTES)
|
|
121
|
+
writeXidBytesUnchecked(secs, machineId, processId, counter, bytes, 0)
|
|
122
|
+
return encodeBase32Hex(bytes)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toBytes(id: string): Uint8Array {
|
|
126
|
+
return decodeBase32Hex(id)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function fromBytes(bytes: Uint8Array): string {
|
|
130
|
+
return encodeBase32Hex(bytes)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function timestamp(id: string): number {
|
|
134
|
+
const bytes = decodeBase32Hex(id)
|
|
135
|
+
return (((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0) * 1000
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isValid(id: unknown): id is string {
|
|
139
|
+
return typeof id === 'string' && XID_REGEX.test(id)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate a 20-character rs/xid-compatible identifier.
|
|
144
|
+
*
|
|
145
|
+
* XID embeds seconds, a lazily-random per-runtime identity, and a shared
|
|
146
|
+
* always-incrementing counter. In Cloudflare Workers, time is frozen during a
|
|
147
|
+
* request, so the counter preserves ordering for IDs made in that request.
|
|
148
|
+
*/
|
|
149
|
+
export const xid: Xid = Object.assign(xidFn, {
|
|
150
|
+
toBytes,
|
|
151
|
+
fromBytes,
|
|
152
|
+
timestamp,
|
|
153
|
+
isValid,
|
|
154
|
+
NIL: '0'.repeat(20),
|
|
155
|
+
MAX: encodeBase32Hex(new Uint8Array(12).fill(0xff)),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
export { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'
|