import { randomBytes } from "crypto" export type Prefix = "evt" | "ses" | "msg" | "per" | "que" | "prt" | "pty" | "tool" | "wrk" const LENGTH = 26 // State for monotonic ID generation let lastTimestamp = 0 let counter = 0 export function ascending(prefix: Prefix, given?: string) { return generateID(prefix, "ascending", given) } export function descending(prefix: Prefix, given?: string) { return generateID(prefix, "descending", given) } function generateID(prefix: Prefix, direction: "descending" | "ascending", given?: string): string { if (!given) { return create(prefix, direction) } if (!given.startsWith(prefix)) { throw new Error(`ID ${given} does not start with ${prefix}`) } return given } function randomBase62(length: number): string { const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" let result = "" const bytes = randomBytes(length) for (let i = 0; i < length; i++) { result += chars[bytes[i] % 62] } return result } export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string { const currentTimestamp = timestamp ?? Date.now() if (currentTimestamp !== lastTimestamp) { lastTimestamp = currentTimestamp counter = 0 } counter++ let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter) now = direction === "descending" ? ~now : now const timeBytes = Buffer.alloc(6) for (let i = 0; i < 6; i++) { timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) } return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12) } /** Extract timestamp from an ascending ID. Does not work with descending IDs. */ export function timestamp(id: string): number { const prefix = id.split("_")[0] const hex = id.slice(prefix.length + 1, prefix.length + 13) const encoded = BigInt("0x" + hex) return Number(encoded / BigInt(0x1000)) } export * as Identifier from "./id"