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;