| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- /**
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- import URL from 'url';
- import VM from 'vm';
- import threads from 'worker_threads';
- const WORKER = Symbol.for('worker');
- const EVENTS = Symbol.for('events');
- class EventTarget {
- constructor() {
- Object.defineProperty(this, EVENTS, {
- value: new Map()
- });
- }
- dispatchEvent(event) {
- event.target = event.currentTarget = this;
- if (this['on'+event.type]) {
- try {
- this['on'+event.type](event);
- }
- catch (err) {
- console.error(err);
- }
- }
- const list = this[EVENTS].get(event.type);
- if (list == null) return;
- list.forEach(handler => {
- try {
- handler.call(this, event);
- }
- catch (err) {
- console.error(err);
- }
- });
- }
- addEventListener(type, fn) {
- let events = this[EVENTS].get(type);
- if (!events) this[EVENTS].set(type, events = []);
- events.push(fn);
- }
- removeEventListener(type, fn) {
- let events = this[EVENTS].get(type);
- if (events) {
- const index = events.indexOf(fn);
- if (index !== -1) events.splice(index, 1);
- }
- }
- }
- function Event(type, target) {
- this.type = type;
- this.timeStamp = Date.now();
- this.target = this.currentTarget = this.data = null;
- }
- // this module is used self-referentially on both sides of the
- // thread boundary, but behaves differently in each context.
- export default threads.isMainThread ? mainThread() : workerThread();
- const baseUrl = URL.pathToFileURL(process.cwd() + '/');
- 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.
- */
- class Worker extends EventTarget {
- constructor(url, options) {
- super();
- const { name, type } = options || {};
- url += '';
- let mod;
- if (/^data:/.test(url)) {
- mod = url;
- }
- else {
- mod = URL.fileURLToPath(new URL.URL(url, baseUrl));
- }
- const worker = new threads.Worker(
- __filename,
- { workerData: { mod, name, type } }
- );
- Object.defineProperty(this, WORKER, {
- value: worker
- });
- worker.on('message', data => {
- const event = new Event('message');
- event.data = data;
- this.dispatchEvent(event);
- });
- worker.on('error', error => {
- error.type = 'error';
- this.dispatchEvent(error);
- });
- worker.on('exit', () => {
- this.dispatchEvent(new Event('close'));
- });
- }
- postMessage(data, transferList) {
- this[WORKER].postMessage(data, transferList);
- }
- terminate() {
- this[WORKER].terminate();
- }
- }
- Worker.prototype.onmessage = Worker.prototype.onerror = Worker.prototype.onclose = null;
- return Worker;
- }
- function workerThread() {
- let { mod, name, type } = threads.workerData;
- // turn global into a mock WorkerGlobalScope
- const self = global.self = global;
- // enqueue messages to dispatch after modules are loaded
- let q = [];
- function flush() {
- const buffered = q;
- q = null;
- buffered.forEach(event => { self.dispatchEvent(event); });
- }
- threads.parentPort.on('message', data => {
- const event = new Event('message');
- event.data = data;
- if (q == null) self.dispatchEvent(event);
- else q.push(event);
- });
- threads.parentPort.on('error', err => {
- err.type = 'Error';
- self.dispatchEvent(err);
- });
- class WorkerGlobalScope extends EventTarget {
- postMessage(data, transferList) {
- threads.parentPort.postMessage(data, transferList);
- }
- // Emulates https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/close
- close() {
- process.exit();
- }
- }
- let proto = Object.getPrototypeOf(global);
- delete proto.constructor;
- Object.defineProperties(WorkerGlobalScope.prototype, proto);
- proto = Object.setPrototypeOf(global, new WorkerGlobalScope());
- ['postMessage', 'addEventListener', 'removeEventListener', 'dispatchEvent'].forEach(fn => {
- proto[fn] = proto[fn].bind(global);
- });
- global.name = name;
- const isDataUrl = /^data:/.test(mod);
- if (type === 'module') {
- import(mod)
- .catch(err => {
- if (isDataUrl && err.message === 'Not supported') {
- console.warn('Worker(): Importing data: URLs requires Node 12.10+. Falling back to classic worker.');
- return evaluateDataUrl(mod, name);
- }
- console.error(err);
- })
- .then(flush);
- }
- else {
- try {
- if (/^data:/.test(mod)) {
- evaluateDataUrl(mod, name);
- }
- else {
- require(mod);
- }
- }
- catch (err) {
- console.error(err);
- }
- Promise.resolve().then(flush);
- }
- }
- function evaluateDataUrl(url, name) {
- const { data } = parseDataUrl(url);
- return VM.runInThisContext(data, {
- filename: 'worker.<'+(name || 'data:')+'>'
- });
- }
- function parseDataUrl(url) {
- let [m, type, encoding, data] = url.match(/^data: *([^;,]*)(?: *; *([^,]*))? *,(.*)$/) || [];
- if (!m) throw Error('Invalid Data URL.');
- if (encoding) switch (encoding.toLowerCase()) {
- case 'base64':
- data = Buffer.from(data, 'base64').toString();
- break;
- default:
- throw Error('Unknown Data URL encoding "' + encoding + '"');
- }
- return { type, data };
- }
|