import big from './bigInt'; import { EventEmitter } from 'events'; import { sleep, getBits } from './util' import { Snowflake, SnowflakeConfig, SnowflakeMutable } from './types'; export default class SnowflakeWorker extends EventEmitter { public options: SnowflakeConfig; private _mutable: SnowflakeMutable; private _maxIncrement: Number; public workerId: number; public processId: number; constructor(options: SnowflakeConfig) { super(); // The default options for the generator this.setMaxListeners(100); this.options = { name: undefined, async: false, epoch: null, 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 immutable objects to prevent tampering Object.freeze(this.options); // Object.freeze(this); } get increment(): number { return this._mutable.increment = this._mutable.increment.next().mod(this._maxIncrement); } generate(): Snowflake | Promise { if (this.options.async) return this._generateAsync(); else return this._generate(); } _generate(date: number = Date.now(), increment: number = this.increment): Snowflake { // 0000000000000000000000000000000000000000000000000000000000000000 // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000 let flake: any = big(date).minus(this.options.epoch).shiftLeft(22) // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbb00000000000000000 .add(this.workerId) // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbccccc000000000000 .add(this.processId) // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbcccccdddddddddddd .add(increment); this.emit('newSnowflake', { worker: this, method: 'sync', snowflake: flake.toString(), }); 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(): void { if (this._mutable.locks.length > 0) this._mutable.locks.shift()(); else this._mutable.locked = false; } async _generateAsync(): Promise { 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 / 1000); // reset increment this._mutable.increment = big.zero; now = this._mutable.lastTimestamp = Date.now(); } } // generate a snowflake with the new increment let flake: Snowflake = this._generate(now, this._mutable.increment); this._unlock(); this.emit('newSnowflake', { worker: this, method: 'async', snowflake: flake.toString(), }); return flake; } deconstruct(snowflake: Snowflake): object { // 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)); this.emit('deconstructedFlake', { worker: this, method: 'sync', timestamp, workerId, processId, increment }); return { timestamp, workerId, processId, increment }; } };