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