plugins/sequencer.js

'use strict';

/**
 * @class
 * @description Optional sequencer plugin. Manages creating sequence ids and registering callback handlers to them.
 */

class Sequencer {
	/**
	 * @constructor
	 * @param {Object} opts Sequencer id settings (range, start, etc.)
	 * @param {Number} opts.min The minimum sequence id
	 * @param {Number} opts.max The maximum sequence id
 	 * @param {String|Number} [opts.start] Can be set to 'random' or a number to start the sequence id with
	 * @param {Adapter} adapter The adapter object associated with this sequencer
	 *
	 */

	constructor(opts, adapter) {
		this._opts = opts;
		this._min = opts.min;
		this._max = opts.max;

		this._adapter = adapter;
		this._handlers = {};
		this._format = this._adapter.opts.format;

		// init seqId
		if (opts.start === 'random') {
			this._seqId = this.random();
		} else if (typeof opts.start === 'number') {
			if (opts.start < opts.min || opts.start > opts.max) {
				throw new Error('Sequencer start is out of range');
			}
			this._seqId = opts.start;
		} else {
			this._seqId = this._min;
		}
	}

	/**
	 * @description Get the next sequence id
	 * @param {Function} [callback] Optional callback handler. Will be called if transport receives data with the corresponding sequence id
	 * @param {Object} [scope] Scope to call callback with
	 * @returns {Number}
	 */

	next(callback, scope) {
		let num = this._seqId;

		if (typeof callback === 'function') {
			this.registerInternal(num, callback, scope);
		}

		if (this._seqId < this._max) {
			this._seqId++;
		} else {
			this._seqId = this._min;
		}

		return num;
	}

	/**
	 * @description Processed received data from transport
	 * @param {Buffer} data The data received
	 * @private
	 * @returns {Boolean} True if sequencer handled the data
	 */

	received(data) {
		this._adapter.bridge.k4.logging.debug('Sequencer received', data.toString(this._format));

		let num = this._adapter.getSequence(data);
		let handler = this._handlers[num];

		if (!handler) {
			num = `external_${num}`;
			handler = this._handlers[num];
		}

		if (handler) {
			this._adapter.k4.logging.debug(`Sequencer received seqId ${num}`);
			delete this._handlers[num];

			// NOTE: The reference to the callback is still accessible because
			// it was stored earlier, even though the handler has been deleted
			// from the map. This allows the callback to re-register a handler
			// to the sequencer for the same sequence id without that handler
			// being immediately deleted.
			handler.callback.apply(handler.scope, [data]);
			return true;
		}
	}

	/**
	 * @description Generates a random sequence id between min and max configs
	 * @private
	 * @returns {Number}
	 */

	random() {
		return Math.floor(Math.random() * (this._max - this._min + 1) + this._min);
	}

	/**
	 * @description Indicates if a particular sequencer id has already been
	 * registered and has neither been resolved nor cancelled
	 * @param {String/Number} id
	 * @returns {Array} First element is a Boolean indicating if the id has been
	 * registered with handler. The second element is an Array of types of seqId
	 * usages (internal, external) where the specified id is being used.
	 */
	checkIfHandlerForIdExists(id) {
		const placesWhereUsed = [];

		if (this._handlers[id]) {
			placesWhereUsed.push('internal');
		}

		if (this._handlers[`external_${id}`]) {
			placesWhereUsed.push('external');
		}

		if (placesWhereUsed.length > 0) {
			return [true, placesWhereUsed];
		}

		return [false, placesWhereUsed];
	}

	/**
	 * @description Registers a handler for the specified internal sequence id.
	 * Normally, next() would both obtain a sequence id and register a handler
	 * (and this method would not need to be called). However, this method
	 * allows next() to be called without a callback, and then a handler can be
	 * manually registered here corresponding to the sequence id returned by that next() call.
	 * @param {Number} num The sequence id
	 * @param {Function} callback Callback handler
	 * @param {Object} [scope] Scope to call callback with
	 */
	registerInternal(num, callback, scope) {
		if (typeof callback === 'function') {
			this._handlers[num] = {
				callback: callback,
				scope: scope
			};
		}
	}

	/**
	 * @description Register an external sequence id. External id numbers must
	 * not overlap internal id numbers.
	 * @param {String/Number} id The sequence id
	 * @param {Function} callback Callback handler
	 * @param {Object} [scope] Scope to call callback with
	 */
	registerExternal(id, callback, scope) {
		if (typeof callback === 'function') {
			this._handlers[`external_${id}`] = {
				callback: callback,
				scope: scope
			};
		}
	}

	/**
	* @description Cancel a pending internal sequence id
	* @param {Number} id The sequence id
	*/
	cancelInternal(id) {
		if (this._handlers[id]) {
			delete this._handlers[id];
		}
	}

	/**
	* @description Cancel a pending external sequence id
	* @param {String/Number} id The sequence id
	*/
	cancelExternal(id) {
		if (this._handlers[`external_${id}`]) {
			delete this._handlers[`external_${id}`];
		}
	}
};

module.exports = Sequencer;