UNPKG

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
// 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) } }