155 lines
5.4 KiB
JavaScript
155 lines
5.4 KiB
JavaScript
/* istanbul ignore file */
|
|
/*
|
|
* @note options.dispatchTimeout is not implemented in browser sandbox because
|
|
* there is no way to interrupt an infinite loop.
|
|
* Maybe terminate and restart the worker or execute in nested worker.
|
|
*/
|
|
const Flatted = require('flatted'),
|
|
{ randomNumber } = require('./utils'),
|
|
|
|
ERROR = 'error',
|
|
MESSAGE = 'message',
|
|
UVM_ID_ = '__id_uvm_',
|
|
|
|
// code for bridge
|
|
bridgeClientCode = require('./bridge-client'),
|
|
|
|
/**
|
|
* Returns the firmware code to be executed inside Web Worker.
|
|
*
|
|
* @private
|
|
* @param {String} code -
|
|
* @param {String} id -
|
|
* @return {String}
|
|
*/
|
|
sandboxFirmware = (code, id) => {
|
|
// @note self.postMessage and self.addEventListener methods are cached
|
|
// in variable or closure because bootCode might mutate the global scope
|
|
return `
|
|
__uvm_emit = function (postMessage, args) {
|
|
postMessage({__id_uvm: "${id}",__emit_uvm: args});
|
|
}.bind(null, self.postMessage);
|
|
__uvm_addEventListener = self.addEventListener;
|
|
try {${code}} catch (e) { setTimeout(function () { throw e; }, 0); }
|
|
(function (emit, id) {
|
|
__uvm_addEventListener("message", function (e) {
|
|
(e && e.data && (typeof e.data.__emit_uvm === 'string') && (e.data.__id_uvm === id)) &&
|
|
emit(e.data.__emit_uvm);
|
|
});
|
|
}(__uvm_dispatch, "${id}"));
|
|
__uvm_emit('${Flatted.stringify(['load.' + id])}');
|
|
__uvm_dispatch = null; __uvm_emit = null; __uvm_addEventListener = null;
|
|
delete __uvm_dispatch; delete __uvm_emit; delete __uvm_addEventListener;
|
|
`;
|
|
};
|
|
|
|
module.exports = function (bridge, options, callback) {
|
|
if (!(Blob && Worker && window && window.URL && window.URL.createObjectURL)) {
|
|
return callback(new Error('uvm: unable to setup communication bridge, missing required APIs'));
|
|
}
|
|
|
|
let worker,
|
|
bootTimer,
|
|
firmwareCode,
|
|
firmwareObjectURL;
|
|
|
|
const id = UVM_ID_ + randomNumber(),
|
|
|
|
// function to forward messages emitted
|
|
forwardEmits = (e) => {
|
|
if (!(e && e.data && (typeof e.data.__emit_uvm === 'string') && (e.data.__id_uvm === id))) { return; }
|
|
|
|
let args;
|
|
|
|
try { args = Flatted.parse(e.data.__emit_uvm); }
|
|
catch (err) { return bridge.emit(ERROR, err); }
|
|
bridge.emit(...args);
|
|
},
|
|
|
|
// function to forward errors emitted
|
|
forwardErrors = (e) => {
|
|
bridge.emit(ERROR, e);
|
|
},
|
|
|
|
// function to terminate worker
|
|
terminateWorker = function () {
|
|
if (!worker) { return; }
|
|
|
|
// remove event listeners for this sandbox
|
|
worker.removeEventListener(MESSAGE, forwardEmits);
|
|
worker.removeEventListener(ERROR, forwardErrors);
|
|
|
|
// do not terminate sandbox worker if not spawned for the bridge
|
|
if (!options._sandbox) {
|
|
worker.terminate();
|
|
|
|
// revoke after termination. otherwise, blob reference is retained until GC
|
|
// refer: "chrome://blob-internals"
|
|
window.URL.revokeObjectURL(firmwareObjectURL);
|
|
}
|
|
|
|
worker = null;
|
|
};
|
|
|
|
// on load attach the dispatcher
|
|
bridge.once('load.' + id, () => {
|
|
// stop boot timer first
|
|
clearTimeout(bootTimer);
|
|
|
|
bridge._dispatch = function () {
|
|
if (!worker) {
|
|
return bridge.emit(ERROR,
|
|
new Error('uvm: unable to dispatch "' + arguments[0] + '" post disconnection.'));
|
|
}
|
|
|
|
worker.postMessage({
|
|
__emit_uvm: Flatted.stringify(Array.prototype.slice.call(arguments)),
|
|
__id_uvm: id
|
|
});
|
|
};
|
|
|
|
callback(null, bridge);
|
|
});
|
|
|
|
// get firmware code string with boot code
|
|
firmwareCode = sandboxFirmware(bridgeClientCode(options.bootCode), id);
|
|
|
|
// start boot timer, stops once we get the load signal, terminate otherwise
|
|
bootTimer = setTimeout(() => {
|
|
terminateWorker();
|
|
callback(new Error(`uvm: boot timed out after ${options.bootTimeout}ms.`));
|
|
}, options.bootTimeout);
|
|
|
|
// if sandbox worker is provided, we simply need to init with firmware code
|
|
// @todo validate sandbox type or APIs
|
|
if (options._sandbox) {
|
|
worker = options._sandbox;
|
|
worker.postMessage({ __init_uvm: firmwareCode });
|
|
}
|
|
// else, spawn a new worker
|
|
else {
|
|
// convert the firmware code into a blob URL
|
|
firmwareObjectURL = window.URL.createObjectURL(new Blob([firmwareCode], { type: 'text/javascript' }));
|
|
|
|
// catch CSP:worker-src violations
|
|
try { worker = new Worker(firmwareObjectURL); }
|
|
catch (error) {
|
|
// clear blob reference
|
|
window.URL.revokeObjectURL(firmwareObjectURL);
|
|
|
|
return callback(new Error(`uvm: unable to spawn worker.\n${error.message || error}`));
|
|
}
|
|
}
|
|
|
|
// add event listener for receiving events from worker (is removed on disconnect)
|
|
// don't set `onmessage` and `onerror` as it might override external sandbox
|
|
worker.addEventListener(MESSAGE, forwardEmits);
|
|
worker.addEventListener(ERROR, forwardErrors);
|
|
|
|
// equip bridge to disconnect (i.e. terminate the worker)
|
|
bridge._disconnect = terminateWorker;
|
|
|
|
// help GC collect large variables
|
|
firmwareCode = null;
|
|
};
|