/** * ULID 生成与解析(Crockford Base32,26 字符 = 48bit 时间 + 80bit 随机) * 主键统一由应用侧生成 char(26) ULID(工程规范 05)。 */ import { ULID_LENGTH } from './constants.js'; const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; const ENCODING_LEN = 32; export const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/; function randomBytes(length: number): Uint8Array { const bytes = new Uint8Array(length); globalThis.crypto.getRandomValues(bytes); return bytes; } /** 生成 ULID;同一毫秒内依靠 80bit 随机数避免碰撞 */ export function ulid(timestamp: number = Date.now()): string { if (!Number.isInteger(timestamp) || timestamp < 0 || timestamp > 0xffffffffffff) { throw new RangeError('ULID 时间戳必须是 48bit 非负整数'); } let time = timestamp; const timeChars: string[] = new Array(10); for (let i = 9; i >= 0; i -= 1) { timeChars[i] = ENCODING[time % ENCODING_LEN] ?? '0'; time = Math.floor(time / ENCODING_LEN); } const randomChars = Array.from(randomBytes(16), (byte) => ENCODING[byte & 31] ?? '0'); return `${timeChars.join('')}${randomChars.join('')}`; } export function isUlid(value: string): boolean { return value.length === ULID_LENGTH && ULID_PATTERN.test(value); } /** 解析 ULID 前 10 位得到毫秒时间戳 */ export function ulidToTimestamp(id: string): number { if (!isUlid(id)) { throw new TypeError(`不是合法 ULID:${id}`); } let timestamp = 0; for (let i = 0; i < 10; i += 1) { const char = id[i] ?? ''; const value = ENCODING.indexOf(char); if (value < 0) { throw new TypeError(`ULID 含非法字符:${char}`); } timestamp = timestamp * ENCODING_LEN + value; } return timestamp; } export function ulidToDate(id: string): Date { return new Date(ulidToTimestamp(id)); }