Convert UUID to class
This commit is contained in:
@@ -1,31 +1,171 @@
|
||||
export function generateUUIDV7(customTimestamp?: bigint) {
|
||||
// Step 1: Get the current timestamp in milliseconds
|
||||
const timestamp = customTimestamp ?? BigInt(Date.now()); // Use BigInt to handle 48-bit values
|
||||
|
||||
// Step 2: Convert the timestamp to a 48-bit hexadecimal string
|
||||
const timestampHex = timestamp.toString(16).padStart(12, '0'); // Ensure 12 hex characters
|
||||
|
||||
// Step 3: Generate 74 bits (19 hex characters) of random data
|
||||
const randomHex = crypto
|
||||
.getRandomValues(new Uint8Array(10))
|
||||
.map((b) => Number(b.toString(16).padStart(2, '0')))
|
||||
.join(''); // 10 bytes = 20 hex characters
|
||||
|
||||
// Step 4: Combine fields to form the UUID
|
||||
const version = '7'; // Version 7 (0111)
|
||||
const variant = '8'; // Variant 1 (10xx) is "8", "9", "a", or "b"
|
||||
|
||||
return (
|
||||
`${timestampHex.substring(0, 8)}-${timestampHex.substring(8, 12)}` + // Timestamp (48 bits)
|
||||
`-${version}${randomHex.substring(0, 3)}` + // Version (4 bits) + 12 random bits
|
||||
`-${variant}${randomHex.substring(3, 6)}` + // Variant (2 bits) + 14 random bits
|
||||
`-${randomHex.substring(6)}` // Remaining 48 random bits
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the timestamp from a UUID v7
|
||||
* A class for generating, parsing, and manipulating UUIDv7 identifiers.
|
||||
*/
|
||||
export function getTimestampFromUUID(uuid: string): bigint {
|
||||
return BigInt(parseInt(uuid.slice(0, 8), 16));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user