| 1 | const ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz"; |
| 2 | |
| 3 | export type IdPrefix = |
| 4 | | "usr" |
| 5 | | "ses" |
| 6 | | "tok" |
| 7 | | "key" |
| 8 | | "rep" |
| 9 | | "int" |
| 10 | | "att" |
| 11 | | "evt" |
| 12 | | "dpl" |
| 13 | | "prj" |
| 14 | | "dom" |
| 15 | | "dep" |
| 16 | | "dst" |
| 17 | | "chn" |
| 18 | | "msg" |
| 19 | | "agt" |
| 20 | | "arp" |
| 21 | | "asn" |
| 22 | | "mem" |
| 23 | | "rtn" |
| 24 | | "drf" |
| 25 | | "spc" |
| 26 | | "pag" |
| 27 | | "ver" |
| 28 | | "thr" |
| 29 | | "cmt" |
| 30 | | "sug" |
| 31 | | "tpl" |
| 32 | | "fil" |
| 33 | | "rds" |
| 34 | | "fol" |
| 35 | | "prp" |
| 36 | | "ins" |
| 37 | | "skl" |
| 38 | | "ska" |
| 39 | | "abr" |
| 40 | | "mcp" |
| 41 | | "mkt" |
| 42 | | "plg" |
| 43 | | "con" |
| 44 | | "cgr" |
| 45 | | "civ" |
| 46 | | "crq"; |
| 47 | |
| 48 | let lastMs = 0; |
| 49 | let lastCounter = 0; |
| 50 | |
| 51 | /** |
| 52 | * A new id in TypeID format (https://github.com/jetify-com/typeid): a type |
| 53 | * prefix, then a UUIDv7 in lowercase Crockford base32, such as |
| 54 | * `att_01jb2k7x9hfq0b3zj0f5s2m8ra`. Sorting ids as strings sorts them by |
| 55 | * creation time. Must stay identical to `new_id` in crates/contracts. |
| 56 | */ |
| 57 | export function newId(prefix: IdPrefix, now: number = Date.now()): string { |
| 58 | const bytes = crypto.getRandomValues(new Uint8Array(16)); |
| 59 | |
| 60 | // Ids made in the same millisecond count up in the 12-bit rand_a field. |
| 61 | const counter = now === lastMs ? (lastCounter + 1) & 0x0fff : ((bytes[6] << 8) | bytes[7]) & 0x07ff; |
| 62 | lastMs = now; |
| 63 | lastCounter = counter; |
| 64 | |
| 65 | const time = BigInt(now); |
| 66 | for (let i = 0; i < 6; i++) { |
| 67 | bytes[i] = Number((time >> BigInt(40 - 8 * i)) & 0xffn); |
| 68 | } |
| 69 | bytes[6] = 0x70 | (counter >> 8); // version 7 |
| 70 | bytes[7] = counter & 0xff; |
| 71 | bytes[8] = 0x80 | (bytes[8] & 0x3f); // RFC 9562 variant |
| 72 | return encode(prefix, bytes); |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * The smallest id with `prefix` that could have been made at or after |
| 77 | * `now`: its time, and every other bit zero. Ids sort by time, so |
| 78 | * `id >= idFloor(p, from) AND id < idFloor(p, until)` picks the ids made in |
| 79 | * `[from, until)` as a range of any index that ends in the id, without a |
| 80 | * time column. No real id ever equals one: a real id's version bits are |
| 81 | * set. The same as `id_floor` in crates/contracts. |
| 82 | */ |
| 83 | export function idFloor(prefix: IdPrefix, now: number): string { |
| 84 | const bytes = new Uint8Array(16); |
| 85 | const time = BigInt(Math.max(0, Math.floor(now))); |
| 86 | for (let i = 0; i < 6; i++) { |
| 87 | bytes[i] = Number((time >> BigInt(40 - 8 * i)) & 0xffn); |
| 88 | } |
| 89 | return encode(prefix, bytes); |
| 90 | } |
| 91 | |
| 92 | function encode(prefix: string, bytes: Uint8Array): string { |
| 93 | let value = 0n; |
| 94 | for (const byte of bytes) value = (value << 8n) | BigInt(byte); |
| 95 | let suffix = ""; |
| 96 | // 128 bits in 26 characters of 5 bits; the first carries only 3. |
| 97 | for (let i = 0; i < 26; i++) { |
| 98 | suffix += ALPHABET[Number((value >> BigInt(125 - 5 * i)) & 31n)]; |
| 99 | } |
| 100 | return `${prefix}_${suffix}`; |
| 101 | } |