@xylabs/threads
Version:
Web workers & worker threads as simple as a function call
315 lines (309 loc) • 10.6 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// src/master/pool-browser.ts
import DebugLogger from "debug";
import { multicast, Observable, Subject } from "observable-fns";
// src/master/implementation.browser.ts
var defaultPoolSize = typeof navigator !== "undefined" && navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 4;
// src/master/pool-types.ts
var PoolEventType = /* @__PURE__ */ function(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;
}({});
// src/symbols.ts
var $errors = Symbol("thread.errors");
var $events = Symbol("thread.events");
var $terminate = Symbol("thread.terminate");
var $transferable = Symbol("thread.transferable");
var $worker = Symbol("thread.worker");
// src/master/thread.ts
function fail(message) {
throw new Error(message);
}
__name(fail, "fail");
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-browser.ts
var nextPoolID = 1;
function createArray(size) {
const array = [];
for (let index = 0; index < size; index++) {
array.push(index);
}
return array;
}
__name(createArray, "createArray");
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
__name(delay, "delay");
function flatMap(array, mapper) {
return array.reduce((flattened, element) => [
...flattened,
...mapper(element)
], []);
}
__name(flatMap, "flatMap");
function slugify(text) {
return text.replaceAll(/\W/g, " ").trim().replaceAll(/\s+/g, "-");
}
__name(slugify, "slugify");
function spawnWorkers(spawnWorker, count) {
return createArray(count).map(() => ({
init: spawnWorker(),
runningTasks: []
}));
}
__name(spawnWorkers, "spawnWorkers");
var WorkerPool = class WorkerPool2 {
static {
__name(this, "WorkerPool");
}
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: PoolEventType.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: PoolEventType.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: PoolEventType.taskCompleted,
workerID
});
} catch (ex) {
const error = ex;
this.debug(`Task #${task.id} failed`);
this.eventSubject.next({
error,
taskID: task.id,
type: PoolEventType.taskFailed,
workerID
});
}
}
run(worker, task) {
const runPromise = (async () => {
const removeTaskFromWorkersRunningTasks = /* @__PURE__ */ __name(() => {
worker.runningTasks = worker.runningTasks.filter((someRunPromise) => someRunPromise !== runPromise);
}, "removeTaskFromWorkersRunningTasks");
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: PoolEventType.taskQueueDrained
});
return;
}
this.run(availableWorker, nextTask);
}
taskCompletion(taskID) {
return new Promise((resolve, reject) => {
const eventSubscription = this.events().subscribe((event) => {
if (event.type === PoolEventType.taskCompleted && event.taskID === taskID) {
eventSubscription.unsubscribe();
resolve(event.returnValue);
} else if (event.type === PoolEventType.taskFailed && event.taskID === taskID) {
eventSubscription.unsubscribe();
reject(event.error);
} else if (event.type === PoolEventType.terminated) {
eventSubscription.unsubscribe();
reject(new Error("Pool has been terminated before task was run."));
}
});
});
}
async settled(allowResolvingImmediately = false) {
const getCurrentlyRunningTasks = /* @__PURE__ */ __name(() => flatMap(this.workers, (worker) => worker.runningTasks), "getCurrentlyRunningTasks");
const taskFailures = [];
const failureSubscription = this.eventObservable.subscribe((event) => {
if (event.type === PoolEventType.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 === PoolEventType.taskQueueDrained) {
subscription.unsubscribe();
resolve(void 0);
}
}
});
});
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 === PoolEventType.taskQueueDrained) {
subscription.unsubscribe();
resolve(settlementPromise);
} else if (event.type === PoolEventType.taskFailed) {
subscription.unsubscribe();
reject(event.error);
}
}
});
});
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: /* @__PURE__ */ __name(() => {
if (!this.taskQueue.includes(task)) return;
this.taskQueue = this.taskQueue.filter((someTask) => someTask !== task);
this.eventSubject.next({
taskID: task.id,
type: PoolEventType.taskCanceled
});
}, "cancel"),
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: PoolEventType.taskQueued
});
this.scheduleWork();
return task;
}
async terminate(force) {
this.isClosing = true;
if (!force) {
await this.completed(true);
}
this.eventSubject.next({
remainingQueue: [
...this.taskQueue
],
type: PoolEventType.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);
}
__name(PoolConstructor, "PoolConstructor");
PoolConstructor.EventType = PoolEventType;
var Pool = PoolConstructor;
export {
Pool,
PoolEventType,
Thread
};
//# sourceMappingURL=pool-browser.mjs.map