// 16-bit hex lookup table (65536 entries) for fast conversion const HEX4 = Array.from({ length: 65536 }, (_, i) => i.toString(16).padStart(4, "0")); function randu32() { return crypto.getRandomValues(new Uint32Array(1))[0]; } const EPOCH = 1420070400000; const MAX_SEQ = 0x3fffff; // Snowflake as a hex string (16 chars, zero-padded). // // Since this is not distributed (no machine ID needed), we use an extended // 22-bit sequence instead of the standard 10-bit machine ID + 12-bit sequence. // type Snowflake = string & { readonly __brand: unique symbol }; namespace Snowflake { // Hex string validation pattern (16 lowercase hex chars). // export const PATTERN = /^[0-9a-f]{16}$/; // Epoch timestamp. // export const EPOCH_TIMESTAMP = EPOCH; // Maximum sequence number. // export const MAX_SEQUENCE = MAX_SEQ; // Parses a hex string or bigint to bigint. // function toBigInt(value: Snowflake): bigint { const hi = Number.parseInt(value.substring(0, 8), 16); const lo = Number.parseInt(value.substring(8, 16), 16); return (BigInt(hi) << 32n) | BigInt(lo); } // Formats a sequence and timestamp into a snowflake hex string. // export function formatParts(dt: number, seq: number): Snowflake { // Split dt into hi/lo to avoid exceeding Number.MAX_SAFE_INTEGER. // dt is ~39 bits; dt<<22 would be ~61 bits, so we split at bit 10: // lo32 = (dtLo << 22) | seq (10+22 = 32 bits, no overlap) // hi32 = dtHi (~29 bits) const dtLo = dt % 1024; const hi = (dt - dtLo) / 1024; // dt >>> 10 const lo = ((dtLo << 22) | seq) >>> 0; const hi1 = (hi >>> 16) & 0xffff; const hi2 = hi & 0xffff; const lo1 = (lo >>> 16) & 0xffff; const lo2 = lo & 0xffff; return `${HEX4[hi1]}${HEX4[hi2]}${HEX4[lo1]}${HEX4[lo2]}` as Snowflake; } // Snowflake generator type. // export class Source { #seq = 0; constructor(sequence: number = randu32() & MAX_SEQ) { this.#seq = sequence & MAX_SEQ; } // Sequence number. // get sequence() { return this.#seq & MAX_SEQ; } set sequence(v: number) { this.#seq = v & MAX_SEQ; } reset() { this.#seq = 0; } // Generates the next value as a hex string. // generate(timestamp: number): Snowflake { const seq = (this.#seq + 1) & MAX_SEQ; const dt = timestamp - EPOCH; this.#seq = seq; return formatParts(dt, seq); } } // Gets the next snowflake given the timestamp. // const defaultSource = new Source(); export function next(timestamp = Date.now()): Snowflake { return defaultSource.generate(timestamp); } // Validates a snowflake hex string. // export function valid(value: string): value is Snowflake { return value.length === 16 && PATTERN.test(value); } // Returns the upper/lower boundaries for the given timestamp. // export function lowerbound(timelike: Date | number | Snowflake): Snowflake { switch (typeof timelike) { case "object": // Date return formatParts(timelike.getTime() - EPOCH, 0); case "number": return formatParts(timelike - EPOCH, 0); case "string": // Snowflake hex string return timelike; } } export function upperbound(timelike: Date | number | Snowflake): Snowflake { switch (typeof timelike) { case "object": // Date return formatParts(timelike.getTime() - EPOCH, MAX_SEQ); case "number": return formatParts(timelike - EPOCH, MAX_SEQ); case "string": // Snowflake hex string return timelike; } } // Returns the individual bits given the snowflake. // export function getSequence(value: Snowflake) { return Number.parseInt(value.substring(8, 16), 16) & MAX_SEQ; } export function getTimestamp(value: Snowflake) { const n = toBigInt(value) >> 22n; return Number(n + BigInt(EPOCH)); } export function getDate(value: Snowflake) { return new Date(getTimestamp(value)); } } export { Snowflake };