|
| 1 | +/** |
| 2 | + * Reserve 29 bits for the per-runtime counter, which allows more than |
| 3 | + * 500 million unique numeric IDs before exhaustion while keeping the final |
| 4 | + * value inside JavaScript's safe integer range once combined with the |
| 5 | + * runtime prefix. |
| 6 | + */ |
1 | 7 | const NUMERIC_ID_COUNTER_LIMIT = 2 ** 29 |
| 8 | + |
| 9 | +/** |
| 10 | + * Reserve the remaining 24 safe-integer bits for a cryptographically-random |
| 11 | + * runtime prefix so separate execution contexts are unlikely to share the same |
| 12 | + * numeric ID space. Together with the counter bits above, this keeps |
| 13 | + * `prefix * NUMERIC_ID_COUNTER_LIMIT + counter` below `Number.MAX_SAFE_INTEGER`. |
| 14 | + */ |
2 | 15 | const NUMERIC_ID_PREFIX_LIMIT = 2 ** 24 |
3 | 16 |
|
4 | 17 | let numericIdCounter = 0 |
5 | 18 | let numericIdPrefix: number | undefined |
6 | 19 |
|
7 | 20 | function getCryptoApi(): Crypto { |
8 | 21 | if (!('crypto' in globalThis)) { |
9 | | - throw new Error('Crypto API is required for ID generation') |
| 22 | + throw new Error( |
| 23 | + 'Crypto API is required for ID generation. Use a modern browser or Node.js runtime with Web Crypto support.', |
| 24 | + ) |
10 | 25 | } |
11 | 26 |
|
12 | 27 | return globalThis.crypto |
13 | 28 | } |
14 | 29 |
|
15 | 30 | function createNumericIdPrefix(): number { |
16 | 31 | const values = new Uint32Array(1) |
17 | | - getCryptoApi().getRandomValues(values) |
| 32 | + const maxUnbiasedValue = |
| 33 | + Math.floor(2 ** 32 / NUMERIC_ID_PREFIX_LIMIT) * NUMERIC_ID_PREFIX_LIMIT |
| 34 | + |
| 35 | + do { |
| 36 | + // Reject the top incomplete range so the modulo result stays uniform across |
| 37 | + // the full prefix space instead of favoring lower values. |
| 38 | + getCryptoApi().getRandomValues(values) |
| 39 | + } while (values[0]! >= maxUnbiasedValue) |
18 | 40 |
|
19 | 41 | return values[0]! % NUMERIC_ID_PREFIX_LIMIT |
20 | 42 | } |
@@ -55,7 +77,9 @@ export function generateUuid(): string { |
55 | 77 | */ |
56 | 78 | export function generateNumericId(): number { |
57 | 79 | if (numericIdCounter >= NUMERIC_ID_COUNTER_LIMIT) { |
58 | | - throw new Error('Numeric ID counter exhausted') |
| 80 | + throw new Error( |
| 81 | + 'Numeric ID counter exhausted after more than 500 million IDs in one runtime. Restart the application to reset the local counter.', |
| 82 | + ) |
59 | 83 | } |
60 | 84 |
|
61 | 85 | if (numericIdPrefix === undefined) { |
|
0 commit comments