Files
Snowflakey/generator.js
Wessel Damian Tip a7b6e89b0a Complete refractor
2019-01-15 19:49:56 +01:00

129 lines
4.8 KiB
JavaScript

const big = require( './fake_node_modules/bigInt' );
const sleep = ( time = 1 ) => { return new Promise( res => { setTimeout( res, time ); } ); };
const getBits = ( bits ) => { return ( 2 ** bits ) - 1; };
exports.lookup = ( flake = 0, epoch = 1420070400000 ) => { return new Date( ( flake / 4194304 ) + epoch ).toLocaleString(); };
exports.generator = class Snowflake {
constructor( options = {} ) {
this.options = Object.assign({
async : false,
epoch : 1420070400000,
workerId : 0,
processId : 0,
stringify : true,
workerBits : 5,
processBits : 5,
incrementBits: 12
}, options);
// an object containing mutable (unfrozen) properties
this.mutable = {
locks : [],
locked : false,
increment : big.zero.subtract( 1 ),
lastTimestamp: Date.now()
};
if ( this.options.incrementBits + this.options.processBits + this.options.workerBits !== 22) throw new Error( 'incrementBits, processBits, and workerBits must add up to 22.' );
// ensure that ids conform to the number of bits
this.options.workerId = this.options.workerId % ( 2 ** this.options.workerBits );
this.options.processId = this.options.processId % ( 2 ** this.options.processBits );
// check if NaN
if ( isNaN( this.options.workerId ) ) this.options.workerId = 0;
if ( isNaN( this.options.processId ) ) this.options.processId = 0;
// store the maximum increment bound
this.maxIncrement = 2 ** this.options.incrementBits;
// calculate the shifted worker/process ids for later reference
this.workerId = big( this.options.workerId ).shiftLeft( this.options.incrementBits + this.options.processBits );
this.processId = big( this.options.processId ).shiftLeft( this.options.incrementBits );
// freeze options and this object, to prevent tampering
Object.freeze( this.options );
Object.freeze( this );
}
get increment() { return this.mutable.increment = this.mutable.increment.next().mod( this.maxIncrement ); }
generate() {
if ( this.options.async ) return this._generateAsync();
else return this._generate();
}
_generate( date, increment = null ) {
// 0000000000000000000000000000000000000000000000000000000000000000
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000
let flake = big( date || Date.now() ).minus( this.options.epoch ).shiftLeft( 22 )
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbb00000000000000000
.add( this.workerId )
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbccccc000000000000
.add( this.processId )
// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbcccccdddddddddddd
.add( increment || this.increment );
if ( this.options.stringify ) flake = flake.toString();
return flake;
}
_lock() {
if ( this.mutable.locked ) return new Promise( res => this.mutable.locks.push( res ) );
else this.mutable.locked = true;
}
_unlock() {
if ( this.mutable.locks.length > 0 ) this.mutable.locks.shift()();
else this.mutable.locked = false;
}
async _generateAsync() {
let lock = this._lock();
if( lock ) await lock;
let now = Date.now();
// check if increment should be reset
if ( this.mutable.lastTimestamp !== now ) {
// last timestamp didnt match, reset increment
this.mutable.increment = big.zero;
this.mutable.lastTimestamp = now;
} else {
// last timestamp matched, increase increment
this.mutable.increment = this.mutable.increment.next();
// check if increment exceeds max bounds
if ( this.mutable.increment.greaterOrEquals( this.maxIncrement ) ) {
// sleep for 2ms - 1ms has a risk of timestamp not incrementing for some reason?
await sleep( 2 );
// reset increment
this.mutable.increment = big.zero;
now = this.mutable.lastTimestamp = Date.now();
}
}
// generate a snowflake with the new increment
let flake = this._generate( now, this.mutable.increment );
this._unlock();
return flake;
}
deconstruct( snowflake ) {
// turn snowflake into a bigint
let flake = big( snowflake );
// shift right, and add epoch to obtain timestamp
let timestamp = flake.shiftRight( 22 ).add( this.options.epoch );
//obtain workerId
let wBitShift = this.options.incrementBits + this.options.processBits;
let workerId = flake.and( big( getBits( this.options.workerBits ) ).shiftLeft( wBitShift ) ).shiftRight( wBitShift );
// obtain processId
let processId = flake.and( big( getBits( this.options.processBits ) ).shiftLeft( this.options.incrementBits ) ).shiftRight( this.options.incrementBits );
// obtain increment
let increment = flake.and(getBits(this.options.incrementBits));
return { timestamp, workerId, processId, increment };
}
};