fast-ulid 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shaul Lavo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # fast-ulid
2
+
3
+ Fastest spec-compliant monotonic ULID generator for JavaScript. ~62ns per ID, zero dependencies.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install fast-ulid
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { ulid, createUlid, timestamp } from 'fast-ulid'
15
+
16
+ // Generate a ULID
17
+ const id = ulid()
18
+ // → "01HYX3QGZK4P8RJ5N0VWMT6B2A"
19
+
20
+ // Extract the timestamp
21
+ const ms = timestamp(id)
22
+ // → 1700000000000
23
+
24
+ // Create an isolated generator (useful for Workers)
25
+ const generate = createUlid()
26
+ generate()
27
+ generate()
28
+ ```
29
+
30
+ ## API
31
+
32
+ ### `ulid(): string`
33
+
34
+ Generate a ULID using the default shared generator. Returns a 26-character Crockford Base32 string.
35
+
36
+ ### `createUlid(): () => string`
37
+
38
+ Create an isolated generator with its own monotonic state. Use this when you need a dedicated generator per Worker thread to avoid contention.
39
+
40
+ ### `timestamp(id: string): number`
41
+
42
+ Extract the UNIX millisecond timestamp from a ULID string. Unrolled decode — ~5.8ns per call.
43
+
44
+ ## Benchmark
45
+
46
+ Measured on Apple M1, Bun 1.3.10:
47
+
48
+ | Operation | fast-ulid | crypto.randomUUID() |
49
+ |---|---|---|
50
+ | Single ID | **62 ns** | 44 ns |
51
+ | Batch 1000 | **84 µs** | 43 µs |
52
+
53
+ `crypto.randomUUID()` is a native C++ call that formats 128 random bits. `fast-ulid` does more work — timestamp encoding, monotonic ordering, Crockford Base32 — and is still within 1.5x.
54
+
55
+ ```bash
56
+ bun run bench
57
+ ```
58
+
59
+ ## What makes it fast
60
+
61
+ - **Batched randomness** — `crypto.getRandomValues` called once per 8,192 IDs, not every call
62
+ - **Pre-computed lookup table** — Uint8Array maps digit → charCode, no string indexing
63
+ - **Reused output buffer** — single `Uint8Array(26)` + `TextDecoder`, zero allocations in hot path
64
+ - **Monotonic increment** — same-ms IDs bump a counter instead of regenerating randomness
65
+ - **Bit masking** — `& 31` instead of modulo
66
+ - **Unrolled loops** — timestamp encode/decode fully unrolled, no loop overhead
67
+
68
+ ## Spec compliance
69
+
70
+ Fully compliant with the [ULID spec](https://github.com/ulid/spec). 26 tests verify:
71
+
72
+ | Requirement | |
73
+ |---|---|
74
+ | 26-char Crockford Base32 string | ✅ |
75
+ | 48-bit ms timestamp (10 chars) | ✅ |
76
+ | 80-bit cryptographic randomness (16 chars) | ✅ |
77
+ | Monotonic: same-ms IDs increment by 1 | ✅ |
78
+ | Overflow advances timestamp | ✅ |
79
+ | Lexicographic sort = chronological sort | ✅ |
80
+ | Clock rollback resilience | ✅ |
81
+ | 10,000+ unique IDs | ✅ |
82
+ | Encode/decode roundtrip | ✅ |
83
+
84
+ ```bash
85
+ bun test
86
+ ```
87
+
88
+ ## Runtime support
89
+
90
+ Works everywhere with `crypto.getRandomValues`, `Date.now`, and `TextDecoder`:
91
+
92
+ - Node.js 16+
93
+ - Bun
94
+ - Deno
95
+ - Modern browsers
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1,7 @@
1
+ /** Create an isolated ULID generator with its own monotonic state. Useful for Workers. */
2
+ export declare function createUlid(): () => string;
3
+ /** Default shared ULID generator. */
4
+ export declare const ulid: () => string;
5
+ /** Extract the UNIX-ms timestamp from a ULID string. */
6
+ export declare function timestamp(id: string): number;
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAsGA,0FAA0F;AAC1F,wBAAgB,UAAU,IAAI,MAAM,MAAM,CA+BzC;AAED,qCAAqC;AACrC,eAAO,MAAM,IAAI,QAlCmB,MAkCJ,CAAA;AAOhC,wDAAwD;AACxD,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAe5C"}
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ // fast-ulid — Monotonic ULID implementation
2
+ // - Lexicographically increasing even for multiple IDs in the same millisecond
3
+ // - Monotonic under clock rollback by pinning to the last emitted timestamp
4
+ // - `createUlid()` provides isolated state so callers can create one generator per Worker
5
+ // - Zero dependencies, works in Node, Bun, Deno, and browsers
6
+ const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
7
+ const RANDOM_DIGITS = 16;
8
+ const MAX_DIGIT = 31;
9
+ const BATCH = 8192;
10
+ const ENC = new Uint8Array(32);
11
+ for (let i = 0; i < 32; i++)
12
+ ENC[i] = ENCODING.charCodeAt(i);
13
+ function fillRandomPool(state) {
14
+ crypto.getRandomValues(state.randomPool);
15
+ state.randomPoolPos = 0;
16
+ }
17
+ function setRandomFromPool(state) {
18
+ if (state.randomPoolPos >= BATCH)
19
+ fillRandomPool(state);
20
+ const poolOffset = state.randomPoolPos * RANDOM_DIGITS;
21
+ state.randomPoolPos += 1;
22
+ for (let i = 0; i < RANDOM_DIGITS; i++) {
23
+ const digit = state.randomPool[poolOffset + i] & MAX_DIGIT;
24
+ state.lastRandom[i] = digit;
25
+ state.outBuf[10 + i] = ENC[digit];
26
+ }
27
+ }
28
+ function incrementRandom(state) {
29
+ const random = state.lastRandom;
30
+ const out = state.outBuf;
31
+ for (let i = RANDOM_DIGITS - 1; i >= 0; i--) {
32
+ const value = random[i];
33
+ if (value < MAX_DIGIT) {
34
+ const next = value + 1;
35
+ random[i] = next;
36
+ out[10 + i] = ENC[next];
37
+ return true;
38
+ }
39
+ random[i] = 0;
40
+ out[10 + i] = ENC[0];
41
+ }
42
+ return false;
43
+ }
44
+ function nextTimestamp(previousTimestamp) {
45
+ let timestamp = Date.now();
46
+ if (timestamp > previousTimestamp)
47
+ return timestamp;
48
+ while (timestamp <= previousTimestamp) {
49
+ timestamp = Date.now();
50
+ }
51
+ return timestamp;
52
+ }
53
+ function setTimestamp(out, timestamp) {
54
+ // 10 chars of timestamp (48 bits, 5 bits per char, Crockford base32, MSB first).
55
+ // Divisors are exact powers of 2, so Math.floor division is float-precise.
56
+ out[0] =
57
+ ENC[Math.floor(timestamp / 35184372088832) & MAX_DIGIT]; // 2^45
58
+ out[1] =
59
+ ENC[Math.floor(timestamp / 1099511627776) & MAX_DIGIT]; // 2^40
60
+ out[2] =
61
+ ENC[Math.floor(timestamp / 34359738368) & MAX_DIGIT]; // 2^35
62
+ out[3] =
63
+ ENC[Math.floor(timestamp / 1073741824) & MAX_DIGIT]; // 2^30
64
+ out[4] = ENC[Math.floor(timestamp / 33554432) & MAX_DIGIT]; // 2^25
65
+ out[5] = ENC[Math.floor(timestamp / 1048576) & MAX_DIGIT]; // 2^20
66
+ out[6] = ENC[Math.floor(timestamp / 32768) & MAX_DIGIT]; // 2^15
67
+ out[7] = ENC[Math.floor(timestamp / 1024) & MAX_DIGIT]; // 2^10
68
+ out[8] = ENC[Math.floor(timestamp / 32) & MAX_DIGIT]; // 2^5
69
+ out[9] = ENC[timestamp & MAX_DIGIT]; // 2^0
70
+ }
71
+ function nextMonotonicTimestamp(lastTimestamp, now) {
72
+ if (now > lastTimestamp)
73
+ return now;
74
+ return lastTimestamp;
75
+ }
76
+ /** Create an isolated ULID generator with its own monotonic state. Useful for Workers. */
77
+ export function createUlid() {
78
+ const state = {
79
+ lastTimestamp: -1,
80
+ lastRandom: new Uint8Array(RANDOM_DIGITS),
81
+ randomPool: new Uint8Array(BATCH * RANDOM_DIGITS),
82
+ randomPoolPos: BATCH,
83
+ outBuf: new Uint8Array(26),
84
+ decoder: new TextDecoder()
85
+ };
86
+ return function ulidFromState() {
87
+ const timestamp = nextMonotonicTimestamp(state.lastTimestamp, Date.now());
88
+ if (timestamp > state.lastTimestamp) {
89
+ state.lastTimestamp = timestamp;
90
+ setTimestamp(state.outBuf, state.lastTimestamp);
91
+ setRandomFromPool(state);
92
+ return state.decoder.decode(state.outBuf);
93
+ }
94
+ if (incrementRandom(state)) {
95
+ return state.decoder.decode(state.outBuf);
96
+ }
97
+ state.lastTimestamp = nextTimestamp(state.lastTimestamp);
98
+ setTimestamp(state.outBuf, state.lastTimestamp);
99
+ setRandomFromPool(state);
100
+ return state.decoder.decode(state.outBuf);
101
+ };
102
+ }
103
+ /** Default shared ULID generator. */
104
+ export const ulid = createUlid();
105
+ // Reverse lookup: charCode → base32 digit value (0-31), 0xFF = invalid.
106
+ // Covers ASCII 0–127. Built once at module load.
107
+ const DEC = new Uint8Array(128).fill(0xff);
108
+ for (let i = 0; i < 32; i++)
109
+ DEC[ENCODING.charCodeAt(i)] = i;
110
+ /** Extract the UNIX-ms timestamp from a ULID string. */
111
+ export function timestamp(id) {
112
+ // 10 Crockford base32 chars → 50 bits → fits in a JS number (53-bit mantissa).
113
+ // Unrolled to avoid loop overhead.
114
+ return (DEC[id.charCodeAt(0)] * 35184372088832 + // 2^45
115
+ DEC[id.charCodeAt(1)] * 1099511627776 + // 2^40
116
+ DEC[id.charCodeAt(2)] * 34359738368 + // 2^35
117
+ DEC[id.charCodeAt(3)] * 1073741824 + // 2^30
118
+ DEC[id.charCodeAt(4)] * 33554432 + // 2^25
119
+ DEC[id.charCodeAt(5)] * 1048576 + // 2^20
120
+ DEC[id.charCodeAt(6)] * 32768 + // 2^15
121
+ DEC[id.charCodeAt(7)] * 1024 + // 2^10
122
+ DEC[id.charCodeAt(8)] * 32 + // 2^5
123
+ DEC[id.charCodeAt(9)] // 2^0
124
+ );
125
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "fast-ulid",
3
+ "version": "1.0.0",
4
+ "description": "Fastest spec-compliant monotonic ULID generator. ~62ns/id, zero dependencies.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "test": "bun test",
22
+ "bench": "bun run test/bench.ts",
23
+ "prepublishOnly": "bun run build"
24
+ },
25
+ "keywords": [
26
+ "ulid",
27
+ "id",
28
+ "uuid",
29
+ "monotonic",
30
+ "sortable",
31
+ "unique",
32
+ "identifier",
33
+ "crockford",
34
+ "base32",
35
+ "fast"
36
+ ],
37
+ "author": "Shaul Lavo",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/ShaulLavo/fast-ulid.git"
42
+ },
43
+ "homepage": "https://github.com/ShaulLavo/fast-ulid",
44
+ "bugs": {
45
+ "url": "https://github.com/ShaulLavo/fast-ulid/issues"
46
+ },
47
+ "devDependencies": {
48
+ "mitata": "^1.0.34",
49
+ "typescript": "^5.7.0",
50
+ "bun-types": "latest"
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,161 @@
1
+ // fast-ulid — Monotonic ULID implementation
2
+ // - Lexicographically increasing even for multiple IDs in the same millisecond
3
+ // - Monotonic under clock rollback by pinning to the last emitted timestamp
4
+ // - `createUlid()` provides isolated state so callers can create one generator per Worker
5
+ // - Zero dependencies, works in Node, Bun, Deno, and browsers
6
+
7
+ const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
8
+ const RANDOM_DIGITS = 16
9
+ const MAX_DIGIT = 31
10
+ const BATCH = 8192
11
+
12
+ interface UlidState {
13
+ lastTimestamp: number
14
+ lastRandom: Uint8Array<ArrayBuffer>
15
+ randomPool: Uint8Array<ArrayBuffer>
16
+ randomPoolPos: number
17
+ outBuf: Uint8Array<ArrayBuffer>
18
+ decoder: TextDecoder
19
+ }
20
+
21
+ const ENC = new Uint8Array(32)
22
+ for (let i = 0; i < 32; i++) ENC[i] = ENCODING.charCodeAt(i)
23
+
24
+ function fillRandomPool(state: UlidState): void {
25
+ crypto.getRandomValues(state.randomPool)
26
+ state.randomPoolPos = 0
27
+ }
28
+
29
+ function setRandomFromPool(state: UlidState): void {
30
+ if (state.randomPoolPos >= BATCH) fillRandomPool(state)
31
+
32
+ const poolOffset = state.randomPoolPos * RANDOM_DIGITS
33
+ state.randomPoolPos += 1
34
+
35
+ for (let i = 0; i < RANDOM_DIGITS; i++) {
36
+ const digit =
37
+ state.randomPool[poolOffset + i] & MAX_DIGIT
38
+ state.lastRandom[i] = digit
39
+ state.outBuf[10 + i] = ENC[digit]
40
+ }
41
+ }
42
+
43
+ function incrementRandom(state: UlidState): boolean {
44
+ const random = state.lastRandom
45
+ const out = state.outBuf
46
+ for (let i = RANDOM_DIGITS - 1; i >= 0; i--) {
47
+ const value = random[i]
48
+ if (value < MAX_DIGIT) {
49
+ const next = value + 1
50
+ random[i] = next
51
+ out[10 + i] = ENC[next]
52
+ return true
53
+ }
54
+
55
+ random[i] = 0
56
+ out[10 + i] = ENC[0]
57
+ }
58
+
59
+ return false
60
+ }
61
+
62
+ function nextTimestamp(previousTimestamp: number): number {
63
+ let timestamp = Date.now()
64
+ if (timestamp > previousTimestamp) return timestamp
65
+
66
+ while (timestamp <= previousTimestamp) {
67
+ timestamp = Date.now()
68
+ }
69
+
70
+ return timestamp
71
+ }
72
+
73
+ function setTimestamp(
74
+ out: Uint8Array,
75
+ timestamp: number
76
+ ): void {
77
+ // 10 chars of timestamp (48 bits, 5 bits per char, Crockford base32, MSB first).
78
+ // Divisors are exact powers of 2, so Math.floor division is float-precise.
79
+ out[0] =
80
+ ENC[Math.floor(timestamp / 35184372088832) & MAX_DIGIT] // 2^45
81
+ out[1] =
82
+ ENC[Math.floor(timestamp / 1099511627776) & MAX_DIGIT] // 2^40
83
+ out[2] =
84
+ ENC[Math.floor(timestamp / 34359738368) & MAX_DIGIT] // 2^35
85
+ out[3] =
86
+ ENC[Math.floor(timestamp / 1073741824) & MAX_DIGIT] // 2^30
87
+ out[4] = ENC[Math.floor(timestamp / 33554432) & MAX_DIGIT] // 2^25
88
+ out[5] = ENC[Math.floor(timestamp / 1048576) & MAX_DIGIT] // 2^20
89
+ out[6] = ENC[Math.floor(timestamp / 32768) & MAX_DIGIT] // 2^15
90
+ out[7] = ENC[Math.floor(timestamp / 1024) & MAX_DIGIT] // 2^10
91
+ out[8] = ENC[Math.floor(timestamp / 32) & MAX_DIGIT] // 2^5
92
+ out[9] = ENC[timestamp & MAX_DIGIT] // 2^0
93
+ }
94
+
95
+ function nextMonotonicTimestamp(
96
+ lastTimestamp: number,
97
+ now: number
98
+ ): number {
99
+ if (now > lastTimestamp) return now
100
+ return lastTimestamp
101
+ }
102
+
103
+ /** Create an isolated ULID generator with its own monotonic state. Useful for Workers. */
104
+ export function createUlid(): () => string {
105
+ const state: UlidState = {
106
+ lastTimestamp: -1,
107
+ lastRandom: new Uint8Array(RANDOM_DIGITS),
108
+ randomPool: new Uint8Array(BATCH * RANDOM_DIGITS),
109
+ randomPoolPos: BATCH,
110
+ outBuf: new Uint8Array(26),
111
+ decoder: new TextDecoder()
112
+ }
113
+
114
+ return function ulidFromState(): string {
115
+ const timestamp = nextMonotonicTimestamp(
116
+ state.lastTimestamp,
117
+ Date.now()
118
+ )
119
+ if (timestamp > state.lastTimestamp) {
120
+ state.lastTimestamp = timestamp
121
+ setTimestamp(state.outBuf, state.lastTimestamp)
122
+ setRandomFromPool(state)
123
+ return state.decoder.decode(state.outBuf)
124
+ }
125
+
126
+ if (incrementRandom(state)) {
127
+ return state.decoder.decode(state.outBuf)
128
+ }
129
+
130
+ state.lastTimestamp = nextTimestamp(state.lastTimestamp)
131
+ setTimestamp(state.outBuf, state.lastTimestamp)
132
+ setRandomFromPool(state)
133
+ return state.decoder.decode(state.outBuf)
134
+ }
135
+ }
136
+
137
+ /** Default shared ULID generator. */
138
+ export const ulid = createUlid()
139
+
140
+ // Reverse lookup: charCode → base32 digit value (0-31), 0xFF = invalid.
141
+ // Covers ASCII 0–127. Built once at module load.
142
+ const DEC = new Uint8Array(128).fill(0xff)
143
+ for (let i = 0; i < 32; i++) DEC[ENCODING.charCodeAt(i)] = i
144
+
145
+ /** Extract the UNIX-ms timestamp from a ULID string. */
146
+ export function timestamp(id: string): number {
147
+ // 10 Crockford base32 chars → 50 bits → fits in a JS number (53-bit mantissa).
148
+ // Unrolled to avoid loop overhead.
149
+ return (
150
+ DEC[id.charCodeAt(0)] * 35184372088832 + // 2^45
151
+ DEC[id.charCodeAt(1)] * 1099511627776 + // 2^40
152
+ DEC[id.charCodeAt(2)] * 34359738368 + // 2^35
153
+ DEC[id.charCodeAt(3)] * 1073741824 + // 2^30
154
+ DEC[id.charCodeAt(4)] * 33554432 + // 2^25
155
+ DEC[id.charCodeAt(5)] * 1048576 + // 2^20
156
+ DEC[id.charCodeAt(6)] * 32768 + // 2^15
157
+ DEC[id.charCodeAt(7)] * 1024 + // 2^10
158
+ DEC[id.charCodeAt(8)] * 32 + // 2^5
159
+ DEC[id.charCodeAt(9)] // 2^0
160
+ )
161
+ }