Source: index.js

const BigInt = require('big-integer');

/**
 * @module catflake
 */

// helper function to get the max value of a given number of bits
function getBits(bits) {
  return (2 ** bits) - 1;
}

// helper function to sleep for a specified duration
function sleep(time = 1) {
  return new Promise(res => {
    setTimeout(res, time);
  });
}

/**
 * A BigInteger object
 * @typedef {object} BigInteger
 * @see https://www.npmjs.com/package/big-integer
 */

/**
  * A container for a deconstructed snowflake
  * @typedef {object} DeconstructedSnowflake
  * @property {BigInteger} timestamp - The timestamp of a snowflake
  * @property {BigInteger} workerId - The worker ID associated to the snowflake
  * @property {BigInteger} processId - The process ID associated to the snowflake
  * @property {BigInteger} increment - The increment of the snowflake
  */

/**
 * Class representing a snowflake factory
 */
class Catflake {
  /**
   * Create a Catflake factory
   * @param {object} options - The options to configure Catflake with
   * @param {number} [options.epoch=1420070400000] - The epoch to subtract from the timestamp
   * @param {number} [options.incrementBits=12] - The max number of bits that the increment can span
   * @param {number} [options.workerBits=12] - The max number of bits that the workerId can span
   * @param {number} [options.processBits=12] - The max number of bits that the processId can span
   * @param {number} [options.processId=12] - The process ID to generate snowflakes for
   * @param {number} [options.workerBits=12] - The worker ID to generate snowflakes for
   * @param {boolean} [options.async=false] - When true, generate snowflakes asyncronously (eliminates duplicates, but slightly slower)
   * @param {boolean} [options.stringify=true] - When true, returns a string for snowflakes. Otherwise, returns a BigInteger object
   */
  constructor(options = {}) {
    this.options = {
      epoch: 1420070400000,
      incrementBits: 12,
      processBits: 5,
      workerBits: 5,
      processId: 0,
      workerId: 0,
      async: false,
      stringify: true,
      ...options
    };

    // an object containing mutable (unfrozen) properties
    this.mutable = {
      increment: BigInt.zero.subtract(1),
      lastTimestamp: Date.now(),
      locks: [],
      locked: false
    };

    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.processId = this.options.processId % (getBits(this.options.processBits));
    this.options.workerId = this.options.workerId % (getBits(this.options.workerBits));
    // store the maximum increment bound
    this.maxIncrement = 2 ** this.options.incrementBits;

    // calculate the shifted worker/process ids for later reference
    this.workerId = BigInt(this.options.workerId).shiftLeft(this.options.incrementBits + this.options.processBits);
    this.processId = BigInt(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);
  }

  /**
   * Generates a snowflake
   * @returns {(string|BigInteger|Promise<string|BigInteger>)}
   */
  generate() {
    if (this.options.async) {
      return this._generateAsync();
    } else return this._generate();
  }

  _generate(date, increment = null) {
    // 0000000000000000000000000000000000000000000000000000000000000000
    // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000
    let flake = BigInt(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.lastTimestamp = now;
      this.mutable.increment = BigInt.zero;
    } 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
        now = this.mutable.lastTimestamp = Date.now();
        this.mutable.increment = BigInt.zero;
      }
    }

    // generate a snowflake with the new increment
    let flake = this._generate(now, this.mutable.increment);
    this._unlock();
    return flake;
  }

  /**
   * Deconstructs a snowflake
   * @param {(string|number|BigInteger)} snowflake - The snowflake to deconstruct
   * @returns {DeconstructedSnowflake}
   */
  deconstruct(snowflake) {
    // turn snowflake into a bigint
    let flake = BigInt(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(
      BigInt(getBits(this.options.workerBits)).shiftLeft(wBitShift)
    ).shiftRight(wBitShift);

    // obtain processId
    let processId = flake.and(
      BigInt(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
    };
  }
}

module.exports = Catflake;