UNPKG

webworker-ng

Version:

The Web Worker implementation based on thread for Node.js

293 lines (277 loc) 6.99 kB
'use strict'; const v8 = require('v8'); const path = require('path'); const isPlainObject = require('is-plain-object'); const WebWorkerWrap = require('bindings')('webworker').WebWorkerWrap; const defaultOptions = { timeout: undefined, defines: [], }; const fakeGlobals = { fs: require('fs'), process, setTimeout, setInterval, clearTimeout, clearInterval, }; /** * @class WebWorker */ class WebWorker { /** * @method constructor * @param {Function|String} script - the path to a worker script or script function directly. */ constructor(script, options=defaultOptions) { const root = path.join(__dirname, '../'); this._handle = new WebWorkerWrap(root, script, this.onRequest.bind(this)); this._refs = {}; this._requests = []; // messaging objects this._postMessage = null; this._queuedMessages = []; // events this._onstdout = null; this._onstderr = null; this._onmessage = null; // TODO(Yorkie): current not supported this._onerror = null; // init this._terminateTimer = options.timeout ? setTimeout(this.terminate.bind(this), options.timeout) : null; const defines = (options.defines || []).map((val, idx) => { return this.normalize(`defines${idx}`, val); }); this._handle.start(v8.serialize(defines).buffer); } /** * @property {Function} onstdout */ get onstdout() { return (...args) => { if (typeof this._onstdout === 'function') this._onstdout(Array.from(args).join(' ')); else console.info.apply(console, args); }; } set onstdout(val) { this._onstdout = val; } /** * @property {Function} onstderr */ get onstderr() { return (...args) => { if (typeof this._onstderr === 'function') this._onstderr(Array.from(args).join(' ')); else console.error.apply(console, args); }; } set onstderr(val) { this._onstderr = val; } /** * @property {Function} onmessage */ get onmessage() { return (...args) => { if (typeof this._onmessage === 'function') this._onmessage.apply(this, args); }; } set onmessage(val) { this._onmessage = val; } /** * @property {Function} onerror */ set onerror(val) { this._onerror = val; } /** * @property {Boolean} destroyed */ get destroyed() { return this._handle.destroyed; } /** * @method postMessage * @param {Object} msg - message to pass */ postMessage(msg) { if (typeof this._postMessage === 'function') { this._postMessage(msg); } else { this._queuedMessages.push(msg); } } /** * @method terminate */ terminate() { clearTimeout(this._terminateTimer); // clear the requests resouce... this._requests.forEach((request) => { if (request.host === fakeGlobals) { if (request.name === 'setTimeout') clearTimeout(request.result); else if (request.name === 'setInterval') clearInterval(request.result); } }); if (typeof this.onterminating === 'function') { this.onterminating(this._requests); } this._handle.terminate(); setTimeout(() => { this._handle.forceTerminate(); }, 500); } /** * @method send * @param {String} id * @param {Array} args */ send(id, args) { this._handle.send(id, v8.serialize(args.map((val) => { return this.normalize(id, val); })).buffer ); } /** * @method prePostMessage * @param {Function} sender */ prePostMessage(sender) { for (let i = 0; i < this._queuedMessages.length; i++) { sender.call(this, this._queuedMessages[i]); } this._queuedMessages.length = 0; this._postMessage = sender.bind(this); } /** * @method normalize * @param {String} id * @param {Object} data * @returns the normalized object. */ normalize(id, data) { // FIXME(Yorkie): for buffer, we will use SharedArrayBuffer to share // buffer between stacks. if (Buffer.isBuffer(data)) { return data.buffer; } if (data instanceof Error) { return { type: 'Error', message: data.message, stack: data.stack, }; } if (typeof data === 'function') { const key = `method:${id}#root`; this._refs[key] = data; return key; } // FIXME(Yorkie): if data is not a plain object, return data... if (!isPlainObject(data)) { return data; } // start common normalizing progress. const normalized = {}; assign.call(this, normalized, data); function assign(host, val) { for (let name in val) { let prop = val[name]; if (typeof prop === 'function') { const key = `method:${id}#${name}`; host[name] = key; this._refs[key] = val; } else if (Array.isArray(prop)) { host[name] = []; assign.call(this, host[name], prop); } else if (isPlainObject(prop)) { host[name] = {}; assign.call(this, host[name], prop); } else if (typeof prop !== 'object') { host[name] = prop; } } } return normalized; } /** * @method onRequest * @param {String} name * @param {Array} args */ onRequest(name, args) { args = args.map((arg) => { if (/callback:/.test(arg)) { const id = arg.replace(/^callback:/, ''); return (...args) => this.send(id, args); } else if (/^error:/.test(arg)) { try { const data = JSON.parse(arg.replace(/^error:/, '')); const err = new Error(data.message); err.stack = data.stack; return err; } catch (err) { return arg; } } else { return arg; } }); let host = fakeGlobals; let fname; if (this._refs[name]) { host = this._refs[name]; // FIXME(Yorkie): for direct function. if (typeof host === 'function') { host = this._refs; fname = name; } else { fname = name.replace(/^method:[a-z0-9]*#/i, ''); } } else { let mpath = name.split('.'); if (mpath[0] === '$') { host = this; mpath = mpath.slice(1); } if (mpath.length === 1) { fname = mpath[0]; } else { while (true) { const symbol = mpath.shift(); host = host[symbol]; if (mpath.length === 1) { fname = mpath[0]; break; } } } } if (typeof host[fname] === 'function') { let result = host[fname].apply(host, args); this._requests.push({ name: fname, host, result, }); try { return v8.serialize(result).buffer; } catch (err) { return v8.serialize({}).buffer; } } else { throw new Error(`${name} is not a function`); } } } exports.WebWorker = WebWorker;