143 lines
5.8 KiB
TypeScript
143 lines
5.8 KiB
TypeScript
const getSecureRandomInt = (max: number): number => {
|
|
const array = new Uint32Array(1);
|
|
crypto.getRandomValues(array);
|
|
// Using modulo bias is technically present, but negligible here because the space (56) is tiny compared to 2^32.
|
|
return array[0] % max;
|
|
}
|
|
|
|
|
|
/**
|
|
* Generates a random ID with characteristics similar to UUIDv7,
|
|
* incorporating a timestamp prefix for sortability and a random suffix,
|
|
* using a customizable length and a specific character set.
|
|
*
|
|
* This function aims for sortability by placing a time-based component at the beginning,
|
|
* similar to UUIDv7, while allowing a variable total length and using a character set
|
|
* designed to avoid visually similar characters.
|
|
*
|
|
* The character set includes uppercase and lowercase letters and numbers,
|
|
* excluding visually similar characters (0, O, I, l, 1).
|
|
*
|
|
* **Length Reference**:
|
|
*
|
|
* With a collision probability of **0.1%**,
|
|
* the maximum ID generation rate per millisecond for the following lengths is:
|
|
* - **10**: 1.8 IDs / ms or 1,844 QPS
|
|
* - **12**: 27 IDs / ms or 26,998 QPS
|
|
* - **16**: 5784 IDs / ms or 5,784,295 QPS
|
|
*
|
|
* With a collision probability of **0.001%**,
|
|
* the maximum ID generation rate per millisecond for the following lengths is:
|
|
* - **11**: 1.5 IDs / ms or 1,520 QPS
|
|
* - **14**: 85 IDs / ms or 85,124 QPS
|
|
* - **16**: 1246 IDs / ms or 1,245,983 QPS
|
|
*
|
|
* With a collision probability of **0.00001%**,
|
|
* the maximum ID generation rate per millisecond for the following lengths is:
|
|
* - **14**: 18 IDs / ms or 18,339 QPS
|
|
* - **15**: 70 IDs / ms or 70,164 QPS
|
|
* - **16**: 1246 IDs / ms or 268,438 QPS
|
|
*
|
|
* The formula: max_qps = 1000 * (2 * (56**(length - 8)) * -log(1 - prob))**(1/3)
|
|
*
|
|
* @param length The desired total length of the ID. Must be at least 8.
|
|
* @returns A sortable random ID string of the specified length.
|
|
* @throws Error if the requested length is less than the minimum required for the timestamp prefix (8).
|
|
*/
|
|
export function generateRandomId(length: number): string {
|
|
// Character set excluding 0, O, I, l, 1
|
|
const allowedChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 56 characters
|
|
const base = allowedChars.length; // 56
|
|
const TIMESTAMP_PREFIX_LENGTH = 8; // Fixed length for the timestamp part to ensure sortability
|
|
|
|
if (length < TIMESTAMP_PREFIX_LENGTH) {
|
|
throw new Error(`Length must be at least ${TIMESTAMP_PREFIX_LENGTH} to include the timestamp prefix.`);
|
|
}
|
|
|
|
// --- Generate Timestamp Prefix ---
|
|
const timestamp = Date.now(); // Milliseconds since epoch (Unix time)
|
|
let timestampBaseString = "";
|
|
let tempTimestamp = timestamp;
|
|
const firstChar = allowedChars[0]; // Character for padding ('a')
|
|
|
|
// Convert timestamp to a base-56 string
|
|
// We process from the least significant "digit" to the most significant
|
|
while (tempTimestamp > 0) {
|
|
timestampBaseString = allowedChars[tempTimestamp % base] + timestampBaseString;
|
|
tempTimestamp = Math.floor(tempTimestamp / base);
|
|
}
|
|
|
|
// Pad the timestamp string at the beginning to ensure a fixed length.
|
|
// This is crucial for chronological sortability of the generated IDs.
|
|
while (timestampBaseString.length < TIMESTAMP_PREFIX_LENGTH) {
|
|
timestampBaseString = firstChar + timestampBaseString;
|
|
}
|
|
|
|
// Although highly unlikely with an 8-character prefix using base 56 for current timestamps,
|
|
// this would truncate if the timestamp string somehow exceeded the prefix length.
|
|
if (timestampBaseString.length > TIMESTAMP_PREFIX_LENGTH) {
|
|
timestampBaseString = timestampBaseString.substring(timestampBaseString.length - TIMESTAMP_PREFIX_LENGTH);
|
|
}
|
|
|
|
// --- Generate Random Suffix ---
|
|
const randomLength = length - TIMESTAMP_PREFIX_LENGTH;
|
|
let randomSuffix = "";
|
|
const allowedCharsLength = allowedChars.length;
|
|
|
|
for (let i = 0; i < randomLength; i++) {
|
|
const randomIndex = getSecureRandomInt(allowedCharsLength);
|
|
randomSuffix += allowedChars[randomIndex];
|
|
}
|
|
|
|
// --- Concatenate and Return ---
|
|
return timestampBaseString + randomSuffix;
|
|
}
|
|
|
|
/**
|
|
* Decodes the timestamp (in milliseconds since epoch) from an ID
|
|
* generated by a function that uses a fixed-length timestamp prefix
|
|
* encoded in a specific base and character set (like the previous example).
|
|
*
|
|
* It extracts the timestamp prefix and converts it back from the
|
|
* custom base-56 encoding to a number.
|
|
*
|
|
* @param id The ID string containing the timestamp prefix.
|
|
* @returns The timestamp in milliseconds since epoch.
|
|
* @throws Error if the ID is too short or contains invalid characters in the timestamp prefix.
|
|
*/
|
|
export function decodeTimestampFromId(id: string): number {
|
|
// Character set must match the encoding function used to generate the ID
|
|
const allowedChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 56 characters
|
|
const base = allowedChars.length; // 56
|
|
const TIMESTAMP_PREFIX_LENGTH = 8; // Fixed length for the timestamp part
|
|
|
|
if (id.length < TIMESTAMP_PREFIX_LENGTH) {
|
|
throw new Error(
|
|
`ID must be at least ${TIMESTAMP_PREFIX_LENGTH} characters long to contain a timestamp prefix.`
|
|
);
|
|
}
|
|
|
|
// Extract the timestamp prefix from the beginning of the ID string
|
|
const timestampPrefix = id.substring(0, TIMESTAMP_PREFIX_LENGTH);
|
|
let timestamp = 0;
|
|
|
|
// Convert the base-56 timestamp string back to a number
|
|
// Iterate through the prefix characters from left to right (most significant 'digit')
|
|
for (let i = 0; i < timestampPrefix.length; i++) {
|
|
const char = timestampPrefix[i];
|
|
// Find the index (value) of the character in the allowed character set
|
|
const charIndex = allowedChars.indexOf(char);
|
|
|
|
if (charIndex === -1) {
|
|
// If a character is not in the allowed set, the ID is likely invalid
|
|
throw new Error(`Invalid character "${char}" found in timestamp prefix.`);
|
|
}
|
|
|
|
// Standard base conversion: accumulate the value
|
|
// For each digit, multiply the current total by the base and add the digit's value
|
|
timestamp = timestamp * base + charIndex;
|
|
}
|
|
|
|
return timestamp;
|
|
}
|