flaky
Version:
Module for generating short, fixed-length, sequential UUIDs ideal for indexing in various tree based structures
126 lines (104 loc) • 4.29 kB
JavaScript
// Based on information about performance from:
// http://blog.mikemccandless.com/2014/05/choosing-fast-unique-identifier-uuid.html
let Decimal = require('decimal.js')
let charsets = {
base64: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/",
base64URL: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
}
let crypto = require('crypto'),
os = require('os')
// Distribution
// We need 42 bits for the timestamp. That leaves 22 bits - 14 for the sequence and 8 for the node id.
let nodeIdSize = 8
// Hash the hostname to hex, grab the first 8 bits of the hash (making the node id unique to the system)
// and XOR a random number between 0 and 8 to account for multiple instances on the same system.
// Note that we use XOR to make sure we only touch the last few (up to 4) bits and prevent changing the length
// of the binary string.
//
// Also, the random range is based on:
// https://stackoverflow.com/questions/1527803/generating-random-numbers-in-javascript-in-a-specific-range#1527820
let nodeIdBits = parseInt(crypto.createHash('md5').update(os.hostname()).digest('hex'), 16).toString(2).substr(0, nodeIdSize)
nodeIdBits = (parseInt(nodeIdBits, 2) ^ (Math.random() * (nodeIdSize + 1) | 0)).toString(2)
// Potential collisions:
//
// It is important to update the base time somewhat frequently to prevent a potential collision. If there are multiple server
// instances running and they each start at the same time then the node id may be the only thing preventing a collision.
// However, we have no idea what the actual binary distance between any two node ids is and it's fairly harmless to update
// the base timestamp every once in a while, effectively resetting the whole namespace.
// If there are more ids generated by a single instance than the distance between any two node ids before the base time is
// updated for two nodes with the same base time then there most likely would be a collision.
//
// To help mitigate that possibility somewhat, after each iteration of the 14 bit 16,384 number sequence, the base time will be
// bumped by a random, predetermined time step of between 1 and 9 (inclusive) milliseconds. Additionally, the base time will
// be completely reset after every 100,000 ids that are generated.
let baseTime, baseTimeBits,
baseTimeStep = Math.random() * (10 - 1) + 1 | 0,
totalSteps = 0,
seq = 0,
seqSize = 1 << 14,
maxGenPerTime = 100000,
genCount = 0
function zeroPadLeft(str, length) {
let padded = ((1 << length).toString(2) + str)
return padded.substr(padded.length - Math.max(length, str.length))
}
function toBase(binStr, base, charset) {
if (base > charset.length || base < 2) {
throw new RangeError(`Invalid base '${base}'`)
}
let num = new Decimal(`0b${binStr}`)
let encoded = ''
while (num > 0) {
let charpos
if (num <= Number.MAX_SAFE_INTEGER) {
let newnum = Math.floor(num / base)
charpos = num - (base * newnum)
num = newnum
} else {
let newnum = num.div(base).floor()
charpos = num.sub(newnum.mul(base))
num = newnum
}
encoded = charset[charpos] + encoded
}
return encoded
}
// Useful debug code to make sure base conversion works right
// let num = 1508677710190292
// console.log(num.toString(2))
// console.log(toBase(num.toString(2), 2, charsets.base64))
// process.exit()
function updateBaseTime() {
baseTime = Date.now()
// baseTime += totalSteps * baseTimeStep
baseTimeBits = zeroPadLeft(baseTime.toString(2), 42, '0')
totalSteps = 0
genCount = 0
seq = 0
}
function binaryId() {
++genCount
if (seq === seqSize) {
baseTime += baseTimeStep
totalSteps++
baseTimeBits = zeroPadLeft(baseTime.toString(2), 44, '0')
seq = 0
}
if (genCount === maxGenPerTime) {
updateBaseTime()
}
// The purpose of the bitwise OR followed by the substr() call is simply to provide easy left padding
let seqBits = (seqSize | seq++).toString(2).substr(1),
binStr = baseTimeBits + nodeIdBits + seqBits
return binStr
}
updateBaseTime()
let self = module.exports = {
id(base, charset) {
let chars = charsets.base64
if (charset) {
chars = charsets[charset] || charset
}
return toBase(binaryId(), base || 64, chars)
}
}