UNPKG

async-transforms

Version:
181 lines (154 loc) 5.53 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var worker = require('worker_threads'); var path = require('path'); var os = require('os'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var worker__default = /*#__PURE__*/_interopDefaultLegacy(worker); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); var os__default = /*#__PURE__*/_interopDefaultLegacy(os); /** * Determines the absolute directory where this script is contained. * * @return {{dirname: string, module: boolean}} */ function getScriptContext() { try { return {dirname: __dirname, module: false}; } catch (e) { // We have to try __dirname first, as this way we can detect module mode correctly. } try { const {pathname: absolutePath} = new URL((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('index.cjs', document.baseURI).href))); return {dirname: path__default['default'].dirname(absolutePath), module: true}; } catch (e) { throw new Error(`could not resolve __dirname or import.meta.url`); } } const {dirname: scriptDir, module: isModule} = getScriptContext(); const cpuCount = os__default['default'].cpus().length || 4; const workerTarget = path__default['default'].join(scriptDir, './internal-worker.' + (isModule ? 'js' : 'cjs')); /** * @param {string} dep to run script from * @param {Partial<import('.').PoolOptions>} options * @return {function(...any): Promise<any>} */ function pool(dep, options) { /** @type {import('.').PoolOptions} */ const o = Object.assign({ minTasks: 1, tasks: cpuCount * 0.75, expiry: 1000, }, options); o.expiry = Math.max(o.expiry, 0) || 0; if (o.tasks > 0 && o.tasks < 1) { o.tasks = cpuCount * o.tasks; } o.tasks = Math.max(Math.ceil(o.tasks), 0) || 1; o.minTasks = Math.max(0, Math.min(o.tasks, ~~o.minTasks)); if (!path__default['default'].isAbsolute(dep)) { throw new TypeError(`cannot load worker with relative path: ${dep}`); } let preparingNewWorker = false; let activeWorkers = 0; /** @type {Map<worker.Worker, NodeJS.Timeout|undefined>} */ const availableWorkers = new Map(); const prepareWorker = () => { ++activeWorkers; const w = createWorker(dep); w.on('message', ({ok}) => { if (ok !== true) { throw new Error(`got non-ok: ${ok}`); } preparingNewWorker = false; releaseWorker(w); }); }; for (let i = 0; i < o.minTasks; ++i) { prepareWorker(); } /** @type {{args: any[], resolve: (arg: any) => void}[]} */ const pendingTasks = []; return async (...args) => { if (availableWorkers.size) { /** @type {worker.Worker} */ const w = availableWorkers.keys().next().value; const timeout = availableWorkers.get(w); availableWorkers.delete(w); timeout && clearTimeout(timeout); return enact(w, args); } // Start a new worker, but still push the work onto the queue for when it's ready. if (!preparingNewWorker && activeWorkers < o.tasks) { preparingNewWorker = true; prepareWorker(); } return new Promise((resolve) => { pendingTasks.push({args, resolve}); }); }; /** * @param {worker.Worker} w * @param {any[]} args * @return {Promise<any>} */ function enact(w, args) { // While we could use the worker's parentPort, this gives us less risk of crosstalk. const {port1, port2} = new worker__default['default'].MessageChannel(); w.postMessage({args, port: port2}, [port2]); return new Promise((resolve, reject) => { /** @type {(arg: {result: any, error: Error}) => void} */ const handler = ({result, error}) => { port1.off('message', handler); // important to allow GC port1.close(); error ? reject(error) : resolve(result); releaseWorker(w); }; port1.on('message', handler); }); } /** * @param {worker.Worker} w */ function maybeTerimateWorker(w) { if (activeWorkers > o.minTasks) { w.terminate(); availableWorkers.delete(w); } else { availableWorkers.set(w, undefined); } --activeWorkers; } /** * @param {worker.Worker} w */ function releaseWorker(w) { const immediateTask = pendingTasks.shift(); if (immediateTask) { // There's an immediate task, consume it and go. const {args, resolve} = immediateTask; resolve(enact(w, args)); } else if (isFinite(o.expiry)) { // Otherwise, put it into our queue to be deleted soon. const timeout = setTimeout(maybeTerimateWorker.bind(null, w), o.expiry); availableWorkers.set(w, timeout); } else { availableWorkers.set(w, undefined); } } } /** * @param {string} dep */ function createWorker(dep) { if (isModule) { return new worker__default['default'].Worker(workerTarget, {workerData: {dep}}); } // In commonJS mode, we have to _again_ require the script, as the Worker ctor incorrectly // only allows ".js" (which attempts to run as a /module/, because of `type: module`) or // ".mjs" extensions (which is always a module). // This will probably be fixed in a future Node. Sounds like a bug. const code = `require(${JSON.stringify(workerTarget)});`; return new worker__default['default'].Worker(code, {workerData: {dep}, eval: true}); } exports.pool = pool;