@balancer-team/rando
Version:
Generate identifiers with Rando.
166 lines (165 loc) • 7.56 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Rando = void 0;
const rng_1 = require("./rng");
const constants_1 = require("./constants");
class Rando {
// Properties
alphabet;
length;
randomLength;
base;
randomBits;
randomLimit;
sortable;
supportDate;
sortableLength;
sortableLimit;
// Save the last monotonic ID
lastMonotonic = null;
// Constructor
constructor({ alphabet = constants_1.BASE_50, length = 22, sortable = false, supportDate = new Date('4000'), } = {}) {
// Validation logic
if (typeof alphabet !== 'string' || alphabet.length < 2) {
throw new Error('alphabet must be at least two characters.');
}
const uniqueAlphabet = new Set(alphabet);
if (uniqueAlphabet.size !== alphabet.length) {
throw new Error('alphabet must have unique characters.');
}
if (typeof length !== 'number' || length <= 0) {
throw new Error('length must be greater than zero.');
}
if (typeof sortable !== 'boolean') {
throw new Error('sortable must be a boolean.');
}
if (!(supportDate instanceof Date)) {
throw new Error('supportDate must be a Date object.');
}
if (supportDate.getTime() < Date.now()) {
throw new Error('supportDate must be in the future.');
}
// Assign the options
this.alphabet = this.sortAlphabet(alphabet);
this.length = length;
this.base = this.alphabet.length;
this.sortable = sortable;
this.supportDate = supportDate;
// Length of the sortable segment needed to support the target date at maximum resolution
this.sortableLength = this.sortable ? Math.floor(Math.log(this.supportDate.getTime()) / Math.log(this.base)) + 1 : 0;
if (this.sortableLength > this.length) {
throw new Error('length insufficient for sortable segment.');
}
// Set the remaining properties
this.sortableLimit = new Date(Math.pow(this.base, this.sortableLength));
this.randomLength = this.length - this.sortableLength;
this.randomBits = Math.log2(Math.pow(this.base, this.randomLength));
this.randomLimit = Math.round(Math.pow(2, this.randomBits));
}
// Methods
generate({ date } = {}) {
let randomSegment = this.generateRandomSegment();
if (!this.sortable)
return randomSegment;
let sortableSegment = this.generateSortableSegment({ date });
if (date)
return sortableSegment + randomSegment;
// If no date is provided, ensure monotonic IDs
const lastSortableSegment = this.lastMonotonic ? this.getSortableSegment(this.lastMonotonic) : '';
const lastRandomSegment = this.lastMonotonic ? this.getRandomSegment(this.lastMonotonic) : '';
// If the new sortable segment is greater than the last one, nothing needs to be incremented
if (sortableSegment > lastSortableSegment) {
this.lastMonotonic = sortableSegment + randomSegment;
return this.lastMonotonic;
}
// Get the lexicographically maximum sortable segment between sortableSegment and lastSortableSegment
// This ensures that IDs are always increasing even if it has been incremented into the future
if (sortableSegment < lastSortableSegment)
sortableSegment = lastSortableSegment;
// Increment the sortable segment plus up to four characters of the last random segment
const monotonicSegment = this.increment(sortableSegment + lastRandomSegment.slice(0, 3));
const remainingRandomSegment = randomSegment.slice(3);
// Set the last monotonic ID
this.lastMonotonic = monotonicSegment + remainingRandomSegment;
return this.lastMonotonic;
}
// generateMonotonic(): string {
// if (!this.sortable) throw new Error('generateMonotonic requires sortable to be true.')
// let sortableSegment = this.generateSortableSegment()
// let randomSegment = this.generateRandomSegment()
// const lastSortableSegment = this.lastMonotonic ? this.getSortableSegment(this.lastMonotonic) : ''
// const lastRandomSegment = this.lastMonotonic ? this.getRandomSegment(this.lastMonotonic) : ''
// // If the new sortable segment is greater than the last one, nothing needs to be incremented
// if (sortableSegment > lastSortableSegment) return sortableSegment + randomSegment
// // Get the lexicographically maximum sortable segment between sortableSegment and lastSortableSegment
// if (sortableSegment < lastSortableSegment) sortableSegment = lastSortableSegment
// // Increment the sortable segment plus four characters of the random segment
// const monotonicSegment = this.increment(sortableSegment + lastRandomSegment.slice(0, 4))
// const remainingRandomSegment = randomSegment.slice(4)
// // Update the last monotonic ID
// this.lastMonotonic = monotonicSegment + remainingRandomSegment
// return this.lastMonotonic
// }
generateRandomSegment() {
return Array.from({ length: this.randomLength }, () => this.alphabet[(0, rng_1.rng)(this.base)]).join('');
}
generateSortableSegment({ date = new Date() } = {}) {
if (!this.sortable)
throw new Error('generateSortableSegment requires sortable.');
let sortableSegment = '';
let remaining = date.getTime();
while (remaining > 0 || sortableSegment.length < this.sortableLength) {
const i = remaining % this.base;
sortableSegment = this.alphabet[i] + sortableSegment;
remaining = Math.floor(remaining / this.base);
}
return sortableSegment;
}
increment(segment) {
if (segment.length === 0)
throw new Error('Cannot increment empty segment.');
let incremented = '';
let cursor = segment.length - 1;
while (cursor >= 0) {
const index = this.alphabet.indexOf(segment[cursor]);
if (index === -1)
throw new Error('Invalid character in segment.');
if (index + 1 === this.base) {
incremented = this.alphabet[0] + incremented;
cursor--;
}
else {
incremented = this.alphabet[index + 1] + incremented;
return segment.slice(0, cursor) + incremented;
}
}
throw new Error('Cannot increment beyond maximum value.');
}
getRandomSegment(id) {
return id.slice(this.sortableLength);
}
getSortableSegment(id) {
if (!this.sortable)
throw new Error('getSortableSegment requires including a timestamp.');
return id.slice(0, this.sortableLength);
}
sortAlphabet(alphabet) {
return alphabet.split('').sort().join('');
}
getDate(id) {
if (!this.sortable)
return null;
if (id.length < this.sortableLength)
return null;
let sortableSegment = this.getSortableSegment(id);
let decoded = 0;
for (let i = 0; i < sortableSegment.length; i++) {
const alphabetIndex = this.alphabet.indexOf(sortableSegment[i]);
if (alphabetIndex === -1)
return null;
decoded = decoded * this.base + alphabetIndex;
}
return new Date(decoded);
}
}
exports.Rando = Rando;