Convert UUID to class

This commit is contained in:
Jim Hamill
2025-04-11 00:25:48 +00:00
parent 7a03bc2c3d
commit 2fea14522f
3 changed files with 371 additions and 32 deletions
+168 -28
View File
@@ -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;
}
}
+4 -4
View File
@@ -3,7 +3,7 @@
// Jason mentions pruning, but I don't quite understand how that will be done yet.
// He also has the concept of "extensions" - but I'm not sure this has been fleshed out yet.
import { generateUUIDV7, getTimestampFromUUID } from '../utils';
import { UUIDV7 } from '../utils';
import { BaseStore } from '../storage';
// TODO: Think about different activities
@@ -200,7 +200,7 @@ export class Activities {
timestamp,
};
const key = generateUUIDV7(timestamp);
const key = UUIDV7.fromTimestamp(timestamp).toString();
await this.store.set(key, activityWithTimestamp);
// Note: We don't need to call processActivity here as the 'set' event will trigger it
}
@@ -221,8 +221,8 @@ export class Activities {
// Sort activities by timestamp using UUID v7's embedded timestamp
const activitiesSorted = filteredEntries.sort(([keyA], [keyB]) => {
const timestampA = getTimestampFromUUID(keyA);
const timestampB = getTimestampFromUUID(keyB);
const timestampA = UUIDV7.from(keyA).getTimestamp();
const timestampB = UUIDV7.from(keyB).getTimestamp();
return Number(timestampA - timestampB);
});