Files
xo-stack/packages/stack/src/utils/uuid.ts
T
2025-04-11 00:25:48 +00:00

172 lines
5.4 KiB
TypeScript

/**
* A class for generating, parsing, and manipulating UUIDv7 identifiers.
*/
export class UUIDV7 {
/**
* Create a UUIDV7 instance from an existing UUID string
*/
static from(uuid: string): UUIDV7 {
// Check if the input is a valid UUID string with version 7
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const uuidStr = uuid.toLowerCase();
if (!uuidRegex.test(uuidStr)) {
throw new Error('Invalid UUID format or not a UUIDv7');
}
// Create a new instance with the provided UUID
return new UUIDV7(uuidStr);
}
/**
* Generate a UUIDv7 with a custom timestamp
*/
static fromTimestamp(timestamp: bigint): UUIDV7 {
// Validate that timestamp is non-negative and fits within 48 bits
if (timestamp < 0n || timestamp > 0xffffffffffffn) {
throw new Error(
'Timestamp must be a non-negative BigInt that fits within 48 bits',
);
}
// Convert timestamp to hexadecimal and pad to 12 characters (48 bits)
const timestampHex = timestamp.toString(16).padStart(12, '0');
// Generate random bytes for the remaining portions of the UUID
const randomBytes = new Uint8Array(10); // We need 10 bytes of randomness
crypto.getRandomValues(randomBytes);
// Convert random bytes to hex
const randomHex = Array.from(randomBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
// Construct UUID parts
const timeLow = timestampHex.substring(0, 8);
const timeMid = timestampHex.substring(8, 12);
// Version 7 goes in the first nibble of time_high_and_version
const timeHighAndVersion = '7' + randomHex.substring(0, 3);
// Set variant bits (high 2 bits of clock_seq_high_and_variant to 10)
const variantByte = parseInt(randomHex.substring(3, 5), 16);
const variantValue = (variantByte & 0x3f) | 0x80; // Set the two most significant bits to 10
const clockSeqHighAndVariant = variantValue.toString(16).padStart(2, '0');
const clockSeqLow = randomHex.substring(5, 7);
// Node is the remaining random bits
const node = randomHex.substring(7, 19);
// Combine all parts to form the UUID string
const uuid = `${timeLow}-${timeMid}-${timeHighAndVersion}-${clockSeqHighAndVariant}${clockSeqLow}-${node}`;
return new UUIDV7(uuid);
}
/**
* Generate a random UUIDv7 with the current timestamp
*/
static generateRandom(): UUIDV7 {
// Get the current time in milliseconds since Unix epoch
const timestamp = BigInt(Date.now());
return UUIDV7.fromTimestamp(timestamp);
}
private readonly uuid: string;
private constructor(uuid: string) {
this.uuid = uuid;
}
/**
* Get the original UUID string
*/
toString(): string {
return this.uuid;
}
/**
* Get the timestamp component as a BigInt
*/
getTimestamp(): bigint {
// Extract the first 48 bits (12 hex chars) representing the timestamp
const uuidParts = this.uuid.split('-');
const timeLow = uuidParts[0];
const timeMid = uuidParts[1];
// Combine timeLow and timeMid to get the full 48-bit timestamp
const timestampHex = timeLow + timeMid;
return BigInt(`0x${timestampHex}`);
}
/**
* Get the timestamp as a JavaScript Date object
*/
getDate(): Date {
// Convert timestamp BigInt to number (milliseconds since Unix epoch)
return new Date(Number(this.getTimestamp()));
}
/**
* Get the version field (should always be 7 for UUIDv7)
*/
getVersion(): number {
// The version is the first hex character of the third group
return parseInt(this.uuid.split('-')[2][0], 16);
}
/**
* Get the variant field
*/
getVariant(): number {
// The variant is determined by the high bits of the first byte of the clock sequence
const clockSeqHighAndVariant = parseInt(
this.uuid.split('-')[3].substring(0, 2),
16,
);
// RFC 4122 defines variant based on the pattern of the most significant bits
if ((clockSeqHighAndVariant & 0x80) === 0) return 0; // 0xxx (NCS backward compatibility)
if ((clockSeqHighAndVariant & 0xc0) === 0x80) return 2; // 10xx (RFC 4122 / Standard)
if ((clockSeqHighAndVariant & 0xe0) === 0xc0) return 6; // 110x (Microsoft legacy)
return 7; // 111x (Reserved for future definition)
}
/**
* Get the random component as a hexadecimal string
*/
getRandomHex(): string {
// For UUIDv7, the random components are:
// - Lower 4 bits of byte 6 (after version)
// - All 8 bits of byte 7
// - Lower 6 bits of byte 8 (after variant)
// - All bits of bytes 9-15
// Parse the UUID without hyphens for easier manipulation
const uuidWithoutHyphens = this.uuid.replace(/-/g, '');
// Extract the random parts:
// Lower 4 bits of byte 6 (the byte containing the version)
const versionByte = parseInt(uuidWithoutHyphens.substring(12, 14), 16);
const versionByteRandom = (versionByte & 0x0f).toString(16);
// All of byte 7
const byte7 = uuidWithoutHyphens.substring(14, 16);
// Lower 6 bits of byte 8 (the byte containing the variant)
const variantByte = parseInt(uuidWithoutHyphens.substring(16, 18), 16);
const variantByteRandom = (variantByte & 0x3f)
.toString(16)
.padStart(2, '0');
// The remaining bytes (9-15)
const remainingBytes = uuidWithoutHyphens.substring(18);
// Combine all random parts
return versionByteRandom + byte7 + variantByteRandom + remainingBytes;
}
}