172 lines
5.4 KiB
TypeScript
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;
|
|
}
|
|
}
|