From ba2bc372401f44bf339caa2fa4db89b0089aba09 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 22 Dec 2019 17:16:11 -0500 Subject: [PATCH] initial commit --- .gitignore | 3 ++ browser.js | 1 + index.js | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 35 +++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 .gitignore create mode 100644 browser.js create mode 100644 index.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16acd49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +package-lock.json diff --git a/browser.js b/browser.js new file mode 100644 index 0000000..b10d2db --- /dev/null +++ b/browser.js @@ -0,0 +1 @@ +export default Worker; diff --git a/index.js b/index.js new file mode 100644 index 0000000..26aff12 --- /dev/null +++ b/index.js @@ -0,0 +1,141 @@ +const URL = require('url'); +const threads = require('worker_threads'); +const { EventEmitter } = require('events'); + +const WORKER = Symbol.for('worker'); + +// this module is used self-referentially on both sides of the +// thread boundary, but behaves differently in each context. +if (threads.isMainThread) mainThread(); +else workerThread(); + +function mainThread() { + /** + * A web-compatible Worker implementation atop Node's worker_threads. + * - uses DOM-style events (Event.data, Event.type, etc) + * - supports event handler properties (worker.onmessage) + * - Worker() constructor accepts a module URL + * - accepts the {type:'module'} option + * - emulates WorkerGlobalScope within the worker + * @param {string} url The URL or module specifier to load + * @param {object} [options] Worker construction options + * @param {string} [options.name] Available as `self.name` within the Worker + * @param {string} [options.type="classic"] Pass "module" to create a Module Worker. + */ + module.exports = class Worker { + constructor(url, options) { + const { name, type } = options || {}; + const mod = URL.fileURLToPath(url); + this[WORKER] = new threads.Worker( + __filename, + { workerData: { mod, name, type } } + ); + worker.on('message', data => { + this.dispatchEvent({ type: 'message', data }); + }); + worker.on('error', error => { + error.type = 'error'; + this.dispatchEvent(error); + }); + worker.on('exit', () => { + this.dispatchEvent({ type: 'close' }); + }); + } + postMessage(data, transferList) { + this[WORKER].postMessage(data, transferList); + } + dispatchEvent(e) { + dispatchEvent(this, e); + } + addEventListener(type, fn) { + this.on(type, fn); + } + removeEventListener(type, fn) { + this.removeListener(type, fn); + } + terminate() { + this[WORKER].terminate(); + } + }; + Worker.prototype.onmessage = Worker.prototype.onerror = Worker.prototype.onclose = null; +} + +function workerThread() { + const { mod, name, type } = threads.workerData; + + // turn global into a mock WorkerGlobalScope + const self = global.self = global; + + // use an internal emitter for WorkerGlobalScope events + const events = new EventEmitter(); + // enqueue messages to dispatch after modules are loaded + let q = []; + function flush() { + const buffered = q; + q = null; + buffered.forEach(event => { dispatchEvent(self, event, events); }); + } + threads.parentPort.on('message', data => { + const event = { type: 'message', data }; + if (q == null) return dispatchEvent(self, event, events); + // save timestamp and enqueue for deferred dispatch: + event.timeStamp = Date.now(); + q.push(event); + }); + threads.parentPort.on('error', err => { + err.type = 'Error'; + dispatchEvent(self, err, events); + }); + //threads.parentPort.on('close', () => { + // dispatchEvent(self, { type: 'close' }, events); + //}); + + Object.assign(self, /** @lends WorkerGlobalScope */ { + name, + postMessage(data, transferList) { + threads.parentPort.postMessage(data, transferList); + }, + dispatchEvent(e) { + dispatchEvent(self, e, events); + }, + addEventListener(type, fn) { + events.on(type, fn); + }, + removeEventListener(type, fn) { + events.removeListener(type, fn); + } + }); + + if (type === 'module') { + import(mod).then(flush); + } + else { + require(mod); + Promise.resolve().then(flush); + } +} + + +/** + * @private + * Easier than wiring up getter/setter pairs for `onmessage` et al. + * Does not implement phased capture or propagation. + * @param {object} target The host object on which to fire events. + * @param {object} e A mock Event object + * @param {string} e.type The event type to fire. + * @param {EventEmitter} [emitter=target] The emitter instance to use for firing events. + */ +function dispatchEvent(target, e, emitter) { + const event = new Event(e.type, target); + Object.assign(event, e); + if (target['on'+event.type]) { + target['on'+event.type](e); + } + (emitter || target).emit(event.type, event); +} + +function Event(type, target) { + this.type = type; + this.target = this.currentTarget = target; + this.timeStamp = Date.now(); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..70e8936 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "web-worker", + "version": "0.1.0", + "description": "Consistent Web Workers in browser and Node.", + "main": "dist/node.js", + "browser": "dist/browser.js", + "exports": { + ".": { + "module": "dist/node.module.js", + "node": "dist/node.js", + "require": "dist/node.js" + } + }, + "scripts": { + "prepare": "microbundle -f cjs,esm node.js browser.js", + "test": "eslint" + }, + "repository": "developit/node-web-worker", + "keywords": [ + "worker", + "worker_threads", + "webworker", + "web worker", + "web-worker", + "threads" + ], + "authors": [], + "license": "Apache-2.0", + "homepage": "https://github.com/developit/node-web-worker", + "devDependencies": { + "eslint": "^6.8.0", + "eslint-config-developit": "^1.1.1", + "microbundle": "^0.11.0" + } +}