index.js

const EventEmitter = require('events'),

    bridge = require('./bridge'),
    { isFunction, isObject } = require('./utils'),

    /**
     * The time to wait for UVM boot to finish. In milliseconds.
     *
     * @private
     * @type {Number}
     */
    DEFAULT_BOOT_TIMEOUT = 30 * 1000,

    /**
     * The time to wait for UVM dispatch process to finish. In milliseconds.
     *
     * @private
     * @type {Number}
     */
    DEFAULT_DISPATCH_TIMEOUT = 30 * 1000,

    E = '',
    ERROR_EVENT = 'error',
    DISPATCH_QUEUE_EVENT = 'dispatchQueued';

/**
 * Configuration options for  UniversalVM connection.
 *
 * @typedef UniversalVM.connectOptions
 *
 * @property {Boolean} [bootCode] Code to be executed inside a VM on boot
 * @property {Boolean} [_sandbox] Custom sandbox instance
 * @property {Boolean} [debug] Inject global console object in Node.js VM
 * @property {Boolean} [bootTimeout=30 * 1000] The time (in milliseconds) to wait for UVM boot to finish
 * @property {Boolean} [dispatchTimeout=30 * 1000] The time (in milliseconds) to wait for UVM dispatch process to finish
 */

/**
 * Universal Virtual Machine for Node and Browser.
 */
class UniversalVM extends EventEmitter {
    constructor () {
        super();

        /**
         * Boolean representing the bridge connectivity state.
         *
         * @private
         * @type {Boolean}
         */
        this._bridgeConnected = false;

        /**
         * Stores the pending dispatch events until the context is ready for use.
         * Useful when not using the asynchronous construction.
         *
         * @private
         * @type {Array}
         */
        this._dispatchQueue = [];
    }

    /**
     * Creates a new instance of UniversalVM.
     * This is merely an alias of the construction creation without needing to
     * write the `new` keyword and creating explicit connection.
     *
     * @param {UniversalVM.connectOptions} [options] Options to configure the UVM
     * @param {Function(error, context)} callback Callback function
     * @returns {Object} UVM event emitter instance
     *
     * @example
     * const uvm = require('uvm');
     *
     * uvm.spawn({
     *     bootCode: `
     *         bridge.on('loopback', function (data) {
     *             bridge.dispatch('loopback', 'pong');
     *         });
     *     `
     * }, (err, context) => {
     *     context.on('loopback', function (data) {
     *         console.log(data); // pong
     *     });
     *
     *     context.dispatch('loopback', 'ping');
     * });
     */
    static spawn (options, callback) {
        const uvm = new UniversalVM(options, callback);

        // connect with the bridge
        uvm.connect(options, callback);

        // return event emitter for chaining
        return uvm;
    }

    /**
     * Establish connection with the communication bridge.
     *
     * @param {UniversalVM.connectOptions} [options] Options to configure the UVM
     * @param {Function(error, context)} callback Callback function
     */
    connect (options, callback) {
        // set defaults for parameters
        !isObject(options) && (options = {});

        /**
         * Wrap the callback for unified result and reduce chance of bug.
         * We also abandon all dispatch replay.
         *
         * @private
         * @param  {Error=} [err] -
         */
        const done = (err) => {
            if (err) {
                // on error during bridging, we simply abandon all dispatch replay
                this._dispatchQueue.length = 0;

                try { this.emit(ERROR_EVENT, err); }
                // nothing to do if listeners fail, we need to move on and execute callback!
                catch (e) { } // eslint-disable-line no-empty
            }

            isFunction(callback) && callback.call(this, err, this);
        };

        // bail out if bridge is connected
        if (this._bridgeConnected) {
            return done();
        }

        // start connection with the communication bridge
        this._bridgeConnected = true;

        // we bridge this event emitter with the context (bridge usually creates the context as well)
        bridge(this, Object.assign({ // eslint-disable-line prefer-object-spread
            bootCode: E,
            bootTimeout: DEFAULT_BOOT_TIMEOUT,
            dispatchTimeout: DEFAULT_DISPATCH_TIMEOUT
        }, options), (err) => {
            if (err) {
                return done(err);
            }

            let args;

            try {
                // we dispatch all pending messages provided nothing had errors
                while ((args = this._dispatchQueue.shift())) {
                    this.dispatch(...args);
                }
            }
            // since there us no further work after dispatching events, we re-use the err parameter.
            // at this point err variable is falsy since truthy case is already handled before
            catch (e) { /* istanbul ignore next */ err = e; }

            done(err);
        });
    }

    /**
     * Emit an event on the other end of bridge.
     * The parameters are same as `emit` function of the event emitter.
     */
    dispatch () {
        try { this._dispatch(...arguments); }
        catch (e) { /* istanbul ignore next */ this.emit(ERROR_EVENT, e); }
    }

    /**
     * Disconnect the bridge and release memory.
     */
    disconnect () {
        // reset the bridge connection state
        this._bridgeConnected = false;

        try { this._disconnect(...arguments); }
        catch (e) { this.emit(ERROR_EVENT, e); }
    }

    /**
     * Stub dispatch handler to queue dispatched messages until bridge is ready.
     *
     * @private
     * @param {String} name -
     */
    _dispatch (name) {
        this._dispatchQueue.push(arguments);
        this.emit(DISPATCH_QUEUE_EVENT, name);
    }

    /**
     * The bridge should be ready to disconnect when this is called. If not,
     * then this prototype stub would throw an error
     *
     * @private
     * @throws {Error} If bridge is not ready and this function is called
     */
    _disconnect () { // eslint-disable-line class-methods-use-this
        throw new Error('uvm: cannot disconnect, communication bridge is broken');
    }
}

module.exports = UniversalVM;