Files
oh-my-pi/packages/utils/src/snowflake.ts
T
can1357 0c2ba68641 fix(utils): fixed snowflake ID generation to avoid exceeding Number.MAX_SAFE_INTEGER
- Fixed snowflake ID generation to avoid exceeding Number.MAX_SAFE_INTEGER by splitting timestamp into high and low components before bit shifting.
2026-02-10 04:09:44 +01:00

137 lines
3.8 KiB
TypeScript

// 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 };