T-PWK/flake-idgen

View on GitHub
flake-id-gen.js

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * Flake ID generator yields k-ordered, conflict-free ids in a distributed environment.
 */
(function () {
    "use strict";

    /**
     * Represents an ID generator.
     * @exports FlakeId
     * @constructor
     * @param {object=} options - Generator options
     * @param {Number} options.id - Generator identifier. It can have values from 0 to 1023. It can be provided instead of <tt>datacenter</tt> and <tt>worker</tt> identifiers.
     * @param {Number} options.datacenter - Datacenter identifier. It can have values from 0 to 31.
     * @param {Number} options.worker - Worker identifier. It can have values from 0 to 31.
     * @param {Number} options.epoch - Number used to reduce value of a generated timestamp.
     * @param {Number} options.seqMask
     */
    var FlakeId = module.exports = function (options) {
        this.options = options || {};

        // Set generator id from 'id' option or combination of 'datacenter' and 'worker'
        if (typeof this.options.id !== 'undefined') {
            /*jslint bitwise: true */
            this.id = this.options.id & 0x3FF;
        } else {
            this.datacenter = (this.options.datacenter || 0) & 0x1F;
            this.worker = (this.options.worker || 0) & 0x1F;
            this.id = (this.datacenter) << 5 | this.worker;
        }
        this.genId = this.id;
        this.genId <<= 12;  // id generator identifier - will not change while generating ids
        this.epoch = +this.options.epoch || 0;
        this.seq = 0;
        this.lastTime = 0;
        this.overflow = false;
        this.seqMask = this.options.seqMask || 0xFFF;
    };

    FlakeId.POW10 = Math.pow(2, 10); // 2 ^ 10
    FlakeId.POW26 = Math.pow(2, 26); // 2 ^ 26

    FlakeId.prototype = {
      /**
       * Generates conflice-free id
       * @param {cb=} callback The callback that handles the response.
       * @returns {Buffer} Generated id if callback is not provided
       * @exception if a sequence exceeded its maximum value and a callback function is not provided
       */
        next: function (cb) {
          /**
           * This callback receives generated id
           * @callback callback
           * @param {Error} error - Error occurred during id generation
           * @param {Buffer} id - Generated id
           */

            var id;
            if (Buffer.alloc) {
                id = Buffer.alloc(8);
            } else {
                id = new Buffer(8);
                id.fill(0);
            }
            var time = Date.now() - this.epoch;

            // Generates id in the same millisecond as the previous id
            if (time < this.lastTime) {
                if (cb) {
                    setTimeout(self.next.bind(self, cb), this.lastTime - time);
                    return;
                }
                throw new Error(`Clock moved backwards. Refusing to generate id for ${this.lastTime - time} milliseconds`);
            }
            if (time === this.lastTime) {

                // If all sequence values (4096 unique values including 0) have been used
                // to generate ids in the current millisecond (overflow is true) wait till next millisecond
                if (this.overflow) {
                    overflowCond(this, cb);
                    return;
                }

                // Increase sequence counter
                /*jslint bitwise: true */
                this.seq = (this.seq + 1) & this.seqMask;

                // sequence counter exceeded its max value (4095)
                // - set overflow flag and wait till next millisecond
                if (this.seq === 0) {
                    this.overflow = true;
                    overflowCond(this, cb);
                    return;
                }
            } else {
                this.overflow = false;
                this.seq = 0;
            }
            this.lastTime = time;

            id.writeUInt32BE(((time & 0x3) << 22) | this.genId | this.seq, 4);
            id.writeUInt8(Math.floor(time / 4) & 0xFF, 4);
            id.writeUInt16BE(Math.floor(time / FlakeId.POW10) & 0xFFFF, 2);
            id.writeUInt16BE(Math.floor(time / FlakeId.POW26) & 0xFFFF, 0);

            if (cb) {
                process.nextTick(cb.bind(null, null, id));
            }
            else {
                return id;
            }
        }
    };

  function overflowCond(self, cb) {
    if (cb) {
        setTimeout(self.next.bind(self, cb), 1);
    }
    else {
        throw new Error('Sequence exceeded its maximum value. Provide callback function to handle sequence overflow');
    }
  }


}());