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 +21 -0
- package/README.md +99 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/package.json +52 -0
- package/src/index.ts +161 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|