@xylabs/threads
Version:
Web workers & worker threads as simple as a function call
578 lines (571 loc) • 20.3 kB
JavaScript
var __getOwnPropNames = Object.getOwnPropertyNames;
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __commonJS = (cb, mod) => function __require2() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
// ../../node_modules/.store/tiny-worker-npm-2.3.0-38c7100e1d/package/lib/index.js
var require_lib = __commonJS({
"../../node_modules/.store/tiny-worker-npm-2.3.0-38c7100e1d/package/lib/index.js"(exports, module) {
"use strict";
var _createClass = /* @__PURE__ */ (function() {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function(Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
})();
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var path2 = __require("path");
var fork = __require("child_process").fork;
var worker = path2.join(__dirname, "worker.js");
var events = /^(error|message)$/;
var defaultPorts = { inspect: 9229, debug: 5858 };
var range = { min: 1, max: 300 };
var Worker = (function() {
function Worker2(arg) {
var _this = this;
var args = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : [];
var options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : { cwd: process.cwd() };
_classCallCheck(this, Worker2);
var isfn = typeof arg === "function", input = isfn ? arg.toString() : arg;
if (!options.cwd) {
options.cwd = process.cwd();
}
var debugVars = process.execArgv.filter(function(execArg) {
return /(debug|inspect)/.test(execArg);
});
if (debugVars.length > 0 && !options.noDebugRedirection) {
if (!options.execArgv) {
debugVars = Array.from(process.execArgv);
options.execArgv = [];
}
var inspectIndex = debugVars.findIndex(function(debugArg) {
return /^--inspect(-brk)?(=\d+)?$/.test(debugArg);
});
var debugIndex = debugVars.findIndex(function(debugArg) {
return /^--debug(-brk)?(=\d+)?$/.test(debugArg);
});
var portIndex = inspectIndex >= 0 ? inspectIndex : debugIndex;
if (portIndex >= 0) {
var match = /^--(debug|inspect)(?:-brk)?(?:=(\d+))?$/.exec(debugVars[portIndex]);
var port = defaultPorts[match[1]];
if (match[2]) {
port = parseInt(match[2]);
}
debugVars[portIndex] = "--" + match[1] + "=" + (port + range.min + Math.floor(Math.random() * (range.max - range.min)));
if (debugIndex >= 0 && debugIndex !== portIndex) {
match = /^(--debug)(?:-brk)?(.*)/.exec(debugVars[debugIndex]);
debugVars[debugIndex] = match[1] + (match[2] ? match[2] : "");
}
}
options.execArgv = options.execArgv.concat(debugVars);
}
delete options.noDebugRedirection;
this.child = fork(worker, args, options);
this.onerror = void 0;
this.onmessage = void 0;
this.child.on("error", function(e) {
if (_this.onerror) {
_this.onerror.call(_this, e);
}
});
this.child.on("message", function(msg) {
var message = JSON.parse(msg);
var error = void 0;
if (!message.error && _this.onmessage) {
_this.onmessage.call(_this, message);
}
if (message.error && _this.onerror) {
error = new Error(message.error);
error.stack = message.stack;
_this.onerror.call(_this, error);
}
});
this.child.send({ input, isfn, cwd: options.cwd, esm: options.esm });
}
_createClass(Worker2, [{
key: "addEventListener",
value: function addEventListener(event, fn) {
if (events.test(event)) {
this["on" + event] = fn;
}
}
}, {
key: "postMessage",
value: function postMessage(msg) {
this.child.send(JSON.stringify({ data: msg }, null, 0));
}
}, {
key: "terminate",
value: function terminate() {
this.child.kill("SIGINT");
}
}], [{
key: "setRange",
value: function setRange(min, max) {
if (min >= max) {
return false;
}
range.min = min;
range.max = max;
return true;
}
}]);
return Worker2;
})();
module.exports = Worker;
}
});
// src/master/pool-node.ts
import DebugLogger from "debug";
import {
multicast,
Observable,
Subject
} from "observable-fns";
// src/master/implementation.node.ts
import { EventEmitter } from "events";
import { cpus } from "os";
import path from "path";
import { cwd } from "process";
import { Worker as NativeWorker } from "worker_threads";
var defaultPoolSize = cpus().length;
function resolveScriptPath(scriptPath, baseURL) {
const makeAbsolute = (filePath) => {
return path.isAbsolute(filePath) ? filePath : path.join(baseURL ?? cwd(), filePath);
};
const absolutePath = makeAbsolute(scriptPath);
return absolutePath;
}
function initWorkerThreadsWorker() {
let allWorkers = [];
class Worker extends NativeWorker {
mappedEventListeners;
constructor(scriptPath, options) {
const resolvedScriptPath = options && options.fromSource ? null : resolveScriptPath(scriptPath, (options ?? {})._baseURL);
if (resolvedScriptPath) {
super(resolvedScriptPath, options);
} else {
const sourceCode = scriptPath;
super(sourceCode, { ...options, eval: true });
}
this.mappedEventListeners = /* @__PURE__ */ new WeakMap();
allWorkers.push(this);
}
addEventListener(eventName, rawListener) {
const listener = (message) => {
rawListener({ data: message });
};
this.mappedEventListeners.set(rawListener, listener);
this.on(eventName, listener);
}
removeEventListener(eventName, rawListener) {
const listener = this.mappedEventListeners.get(rawListener) || rawListener;
this.off(eventName, listener);
}
}
const terminateWorkersAndMaster = () => {
Promise.all(allWorkers.map((worker) => worker.terminate())).then(
() => process.exit(0),
() => process.exit(1)
);
allWorkers = [];
};
process.on("SIGINT", () => terminateWorkersAndMaster());
process.on("SIGTERM", () => terminateWorkersAndMaster());
class BlobWorker extends Worker {
constructor(blob, options) {
super(Buffer.from(blob).toString("utf-8"), { ...options, fromSource: true });
}
static fromText(source, options) {
return new Worker(source, { ...options, fromSource: true });
}
}
return {
blob: BlobWorker,
default: Worker
};
}
function initTinyWorker() {
const TinyWorker = require_lib();
let allWorkers = [];
class Worker extends TinyWorker {
emitter;
constructor(scriptPath, options) {
const resolvedScriptPath = options && options.fromSource ? null : process.platform === "win32" ? `file:///${resolveScriptPath(scriptPath).replaceAll("\\", "/")}` : resolveScriptPath(scriptPath);
if (resolvedScriptPath) {
super(resolvedScriptPath, [], { esm: true });
} else {
const sourceCode = scriptPath;
super(new Function(sourceCode), [], { esm: true });
}
allWorkers.push(this);
this.emitter = new EventEmitter();
this.onerror = (error) => this.emitter.emit("error", error);
this.onmessage = (message) => this.emitter.emit("message", message);
}
addEventListener(eventName, listener) {
this.emitter.addListener(eventName, listener);
}
removeEventListener(eventName, listener) {
this.emitter.removeListener(eventName, listener);
}
terminate() {
allWorkers = allWorkers.filter((worker) => worker !== this);
return super.terminate();
}
}
const terminateWorkersAndMaster = () => {
Promise.all(allWorkers.map((worker) => worker.terminate())).then(
() => process.exit(0),
() => process.exit(1)
);
allWorkers = [];
};
process.on("SIGINT", () => terminateWorkersAndMaster());
process.on("SIGTERM", () => terminateWorkersAndMaster());
class BlobWorker extends Worker {
constructor(blob, options) {
super(Buffer.from(blob).toString("utf-8"), { ...options, fromSource: true });
}
static fromText(source, options) {
return new Worker(source, { ...options, fromSource: true });
}
}
return {
blob: BlobWorker,
default: Worker
};
}
var implementation;
var isTinyWorker;
function selectWorkerImplementation() {
try {
isTinyWorker = false;
return initWorkerThreadsWorker();
} catch (ex) {
console.error(ex);
console.debug("Node worker_threads not available. Trying to fall back to tiny-worker polyfill...");
isTinyWorker = true;
return initTinyWorker();
}
}
function getWorkerImplementation() {
if (!implementation) {
implementation = selectWorkerImplementation();
}
return implementation;
}
function isWorkerRuntime() {
if (isTinyWorker) {
return globalThis !== void 0 && self["postMessage"] ? true : false;
} else {
const isMainThread = typeof __non_webpack_require__ === "function" ? __non_webpack_require__("worker_threads").isMainThread : eval("require")("worker_threads").isMainThread;
return !isMainThread;
}
}
// src/master/pool-types.ts
var PoolEventType = /* @__PURE__ */ ((PoolEventType2) => {
PoolEventType2["initialized"] = "initialized";
PoolEventType2["taskCanceled"] = "taskCanceled";
PoolEventType2["taskCompleted"] = "taskCompleted";
PoolEventType2["taskFailed"] = "taskFailed";
PoolEventType2["taskQueued"] = "taskQueued";
PoolEventType2["taskQueueDrained"] = "taskQueueDrained";
PoolEventType2["taskStart"] = "taskStart";
PoolEventType2["terminated"] = "terminated";
return PoolEventType2;
})(PoolEventType || {});
// src/symbols.ts
var $errors = /* @__PURE__ */ Symbol("thread.errors");
var $events = /* @__PURE__ */ Symbol("thread.events");
var $terminate = /* @__PURE__ */ Symbol("thread.terminate");
// src/master/thread.ts
function fail(message) {
throw new Error(message);
}
var Thread = {
/** Return an observable that can be used to subscribe to all errors happening in the thread. */
errors(thread) {
return thread[$errors] || fail("Error observable not found. Make sure to pass a thread instance as returned by the spawn() promise.");
},
/** Return an observable that can be used to subscribe to internal events happening in the thread. Useful for debugging. */
events(thread) {
return thread[$events] || fail("Events observable not found. Make sure to pass a thread instance as returned by the spawn() promise.");
},
/** Terminate a thread. Remember to terminate every thread when you are done using it. */
terminate(thread) {
return thread[$terminate]();
}
};
// src/master/pool-node.ts
var nextPoolID = 1;
function createArray(size) {
const array = [];
for (let index = 0; index < size; index++) {
array.push(index);
}
return array;
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function flatMap(array, mapper) {
return array.reduce((flattened, element) => [...flattened, ...mapper(element)], []);
}
function slugify(text) {
return text.replaceAll(/\W/g, " ").trim().replaceAll(/\s+/g, "-");
}
function spawnWorkers(spawnWorker, count) {
return createArray(count).map(
() => ({
init: spawnWorker(),
runningTasks: []
})
);
}
var WorkerPool = class {
static EventType = PoolEventType;
debug;
eventObservable;
options;
workers;
eventSubject = new Subject();
initErrors = [];
isClosing = false;
nextTaskID = 1;
taskQueue = [];
constructor(spawnWorker, optionsOrSize) {
const options = typeof optionsOrSize === "number" ? { size: optionsOrSize } : optionsOrSize || {};
const { size = defaultPoolSize } = options;
this.debug = DebugLogger(`threads:pool:${slugify(options.name || String(nextPoolID++))}`);
this.options = options;
this.workers = spawnWorkers(spawnWorker, size);
this.eventObservable = multicast(Observable.from(this.eventSubject));
Promise.all(this.workers.map((worker) => worker.init)).then(
() => this.eventSubject.next({
size: this.workers.length,
type: "initialized" /* initialized */
}),
(error) => {
this.debug("Error while initializing pool worker:", error);
this.eventSubject.error(error);
this.initErrors.push(error);
}
);
}
findIdlingWorker() {
const { concurrency = 1 } = this.options;
return this.workers.find((worker) => worker.runningTasks.length < concurrency);
}
async runPoolTask(worker, task) {
const workerID = this.workers.indexOf(worker) + 1;
this.debug(`Running task #${task.id} on worker #${workerID}...`);
this.eventSubject.next({
taskID: task.id,
type: "taskStart" /* taskStart */,
workerID
});
try {
const returnValue = await task.run(await worker.init);
this.debug(`Task #${task.id} completed successfully`);
this.eventSubject.next({
returnValue,
taskID: task.id,
type: "taskCompleted" /* taskCompleted */,
workerID
});
} catch (ex) {
const error = ex;
this.debug(`Task #${task.id} failed`);
this.eventSubject.next({
error,
taskID: task.id,
type: "taskFailed" /* taskFailed */,
workerID
});
}
}
run(worker, task) {
const runPromise = (async () => {
const removeTaskFromWorkersRunningTasks = () => {
worker.runningTasks = worker.runningTasks.filter((someRunPromise) => someRunPromise !== runPromise);
};
await delay(0);
try {
await this.runPoolTask(worker, task);
} finally {
removeTaskFromWorkersRunningTasks();
if (!this.isClosing) {
this.scheduleWork();
}
}
})();
worker.runningTasks.push(runPromise);
}
scheduleWork() {
this.debug("Attempt de-queueing a task in order to run it...");
const availableWorker = this.findIdlingWorker();
if (!availableWorker) return;
const nextTask = this.taskQueue.shift();
if (!nextTask) {
this.debug("Task queue is empty");
this.eventSubject.next({ type: "taskQueueDrained" /* taskQueueDrained */ });
return;
}
this.run(availableWorker, nextTask);
}
taskCompletion(taskID) {
return new Promise((resolve, reject) => {
const eventSubscription = this.events().subscribe((event) => {
if (event.type === "taskCompleted" /* taskCompleted */ && event.taskID === taskID) {
eventSubscription.unsubscribe();
resolve(event.returnValue);
} else if (event.type === "taskFailed" /* taskFailed */ && event.taskID === taskID) {
eventSubscription.unsubscribe();
reject(event.error);
} else if (event.type === "terminated" /* terminated */) {
eventSubscription.unsubscribe();
reject(new Error("Pool has been terminated before task was run."));
}
});
});
}
async settled(allowResolvingImmediately = false) {
const getCurrentlyRunningTasks = () => flatMap(this.workers, (worker) => worker.runningTasks);
const taskFailures = [];
const failureSubscription = this.eventObservable.subscribe((event) => {
if (event.type === "taskFailed" /* taskFailed */) {
taskFailures.push(event.error);
}
});
if (this.initErrors.length > 0) {
throw this.initErrors[0];
}
if (allowResolvingImmediately && this.taskQueue.length === 0) {
await Promise.allSettled(getCurrentlyRunningTasks());
return taskFailures;
}
await new Promise((resolve, reject) => {
const subscription = this.eventObservable.subscribe({
error: reject,
next(event) {
if (event.type === "taskQueueDrained" /* taskQueueDrained */) {
subscription.unsubscribe();
resolve(void 0);
}
}
// make a pool-wide error reject the completed() result promise
});
});
await Promise.allSettled(getCurrentlyRunningTasks());
failureSubscription.unsubscribe();
return taskFailures;
}
async completed(allowResolvingImmediately = false) {
const settlementPromise = this.settled(allowResolvingImmediately);
const earlyExitPromise = new Promise((resolve, reject) => {
const subscription = this.eventObservable.subscribe({
error: reject,
next(event) {
if (event.type === "taskQueueDrained" /* taskQueueDrained */) {
subscription.unsubscribe();
resolve(settlementPromise);
} else if (event.type === "taskFailed" /* taskFailed */) {
subscription.unsubscribe();
reject(event.error);
}
}
// make a pool-wide error reject the completed() result promise
});
});
const errors = await Promise.race([settlementPromise, earlyExitPromise]);
if (errors.length > 0) {
throw errors[0];
}
}
events() {
return this.eventObservable;
}
queue(taskFunction) {
const { maxQueuedJobs = Number.POSITIVE_INFINITY } = this.options;
if (this.isClosing) {
throw new Error("Cannot schedule pool tasks after terminate() has been called.");
}
if (this.initErrors.length > 0) {
throw this.initErrors[0];
}
const taskID = this.nextTaskID++;
const taskCompletion = this.taskCompletion(taskID);
taskCompletion.catch((error) => {
this.debug(`Task #${taskID} errored:`, error);
});
const task = {
cancel: () => {
if (!this.taskQueue.includes(task)) return;
this.taskQueue = this.taskQueue.filter((someTask) => someTask !== task);
this.eventSubject.next({
taskID: task.id,
type: "taskCanceled" /* taskCanceled */
});
},
id: taskID,
run: taskFunction,
then: taskCompletion.then.bind(taskCompletion)
};
if (this.taskQueue.length >= maxQueuedJobs) {
throw new Error(
"Maximum number of pool tasks queued. Refusing to queue another one.\nThis usually happens for one of two reasons: We are either at peak workload right now or some tasks just won't finish, thus blocking the pool."
);
}
this.debug(`Queueing task #${task.id}...`);
this.taskQueue.push(task);
this.eventSubject.next({
taskID: task.id,
type: "taskQueued" /* taskQueued */
});
this.scheduleWork();
return task;
}
async terminate(force) {
this.isClosing = true;
if (!force) {
await this.completed(true);
}
this.eventSubject.next({
remainingQueue: [...this.taskQueue],
type: "terminated" /* terminated */
});
this.eventSubject.complete();
await Promise.all(this.workers.map(async (worker) => Thread.terminate(await worker.init)));
}
};
function PoolConstructor(spawnWorker, optionsOrSize) {
return new WorkerPool(spawnWorker, optionsOrSize);
}
PoolConstructor.EventType = PoolEventType;
var Pool = PoolConstructor;
export {
Pool,
PoolEventType,
Thread
};
//# sourceMappingURL=pool-node.mjs.map