|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
export default threads.isMainThread ? mainThread() : workerThread(); |
|
|
|
const baseUrl = URL.pathToFileURL(process.cwd() + '/'); |
|
|
|
function mainThread() { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
if (!mod) return mainThread(); |
|
|
|
|
|
const self = global.self = global; |
|
|
|
|
|
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); |
|
} |
|
|
|
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 }; |
|
} |
|
|