threads
Version:
Web workers & worker threads as simple as a function call
291 lines (290 loc) • 12.6 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Pool = exports.Thread = exports.PoolEventType = void 0;
const debug_1 = __importDefault(require("debug"));
const observable_fns_1 = require("observable-fns");
const ponyfills_1 = require("../ponyfills");
const implementation_1 = require("./implementation");
const pool_types_1 = require("./pool-types");
Object.defineProperty(exports, "PoolEventType", { enumerable: true, get: function () { return pool_types_1.PoolEventType; } });
const thread_1 = require("./thread");
Object.defineProperty(exports, "Thread", { enumerable: true, get: function () { return thread_1.Thread; } });
let 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.replace(/\W/g, " ").trim().replace(/\s+/g, "-");
}
function spawnWorkers(spawnWorker, count) {
return createArray(count).map(() => ({
init: spawnWorker(),
runningTasks: []
}));
}
class WorkerPool {
constructor(spawnWorker, optionsOrSize) {
this.eventSubject = new observable_fns_1.Subject();
this.initErrors = [];
this.isClosing = false;
this.nextTaskID = 1;
this.taskQueue = [];
const options = typeof optionsOrSize === "number"
? { size: optionsOrSize }
: optionsOrSize || {};
const { size = implementation_1.defaultPoolSize } = options;
this.debug = debug_1.default(`threads:pool:${slugify(options.name || String(nextPoolID++))}`);
this.options = options;
this.workers = spawnWorkers(spawnWorker, size);
this.eventObservable = observable_fns_1.multicast(observable_fns_1.Observable.from(this.eventSubject));
Promise.all(this.workers.map(worker => worker.init)).then(() => this.eventSubject.next({
type: pool_types_1.PoolEventType.initialized,
size: this.workers.length
}), 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);
}
runPoolTask(worker, task) {
return __awaiter(this, void 0, void 0, function* () {
const workerID = this.workers.indexOf(worker) + 1;
this.debug(`Running task #${task.id} on worker #${workerID}...`);
this.eventSubject.next({
type: pool_types_1.PoolEventType.taskStart,
taskID: task.id,
workerID
});
try {
const returnValue = yield task.run(yield worker.init);
this.debug(`Task #${task.id} completed successfully`);
this.eventSubject.next({
type: pool_types_1.PoolEventType.taskCompleted,
returnValue,
taskID: task.id,
workerID
});
}
catch (error) {
this.debug(`Task #${task.id} failed`);
this.eventSubject.next({
type: pool_types_1.PoolEventType.taskFailed,
taskID: task.id,
error,
workerID
});
}
});
}
run(worker, task) {
return __awaiter(this, void 0, void 0, function* () {
const runPromise = (() => __awaiter(this, void 0, void 0, function* () {
const removeTaskFromWorkersRunningTasks = () => {
worker.runningTasks = worker.runningTasks.filter(someRunPromise => someRunPromise !== runPromise);
};
// Defer task execution by one tick to give handlers time to subscribe
yield delay(0);
try {
yield 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: pool_types_1.PoolEventType.taskQueueDrained });
return;
}
this.run(availableWorker, nextTask);
}
taskCompletion(taskID) {
return new Promise((resolve, reject) => {
const eventSubscription = this.events().subscribe(event => {
if (event.type === pool_types_1.PoolEventType.taskCompleted && event.taskID === taskID) {
eventSubscription.unsubscribe();
resolve(event.returnValue);
}
else if (event.type === pool_types_1.PoolEventType.taskFailed && event.taskID === taskID) {
eventSubscription.unsubscribe();
reject(event.error);
}
else if (event.type === pool_types_1.PoolEventType.terminated) {
eventSubscription.unsubscribe();
reject(Error("Pool has been terminated before task was run."));
}
});
});
}
settled(allowResolvingImmediately = false) {
return __awaiter(this, void 0, void 0, function* () {
const getCurrentlyRunningTasks = () => flatMap(this.workers, worker => worker.runningTasks);
const taskFailures = [];
const failureSubscription = this.eventObservable.subscribe(event => {
if (event.type === pool_types_1.PoolEventType.taskFailed) {
taskFailures.push(event.error);
}
});
if (this.initErrors.length > 0) {
return Promise.reject(this.initErrors[0]);
}
if (allowResolvingImmediately && this.taskQueue.length === 0) {
yield ponyfills_1.allSettled(getCurrentlyRunningTasks());
return taskFailures;
}
yield new Promise((resolve, reject) => {
const subscription = this.eventObservable.subscribe({
next(event) {
if (event.type === pool_types_1.PoolEventType.taskQueueDrained) {
subscription.unsubscribe();
resolve(void 0);
}
},
error: reject // make a pool-wide error reject the completed() result promise
});
});
yield ponyfills_1.allSettled(getCurrentlyRunningTasks());
failureSubscription.unsubscribe();
return taskFailures;
});
}
completed(allowResolvingImmediately = false) {
return __awaiter(this, void 0, void 0, function* () {
const settlementPromise = this.settled(allowResolvingImmediately);
const earlyExitPromise = new Promise((resolve, reject) => {
const subscription = this.eventObservable.subscribe({
next(event) {
if (event.type === pool_types_1.PoolEventType.taskQueueDrained) {
subscription.unsubscribe();
resolve(settlementPromise);
}
else if (event.type === pool_types_1.PoolEventType.taskFailed) {
subscription.unsubscribe();
reject(event.error);
}
},
error: reject // make a pool-wide error reject the completed() result promise
});
});
const errors = yield Promise.race([
settlementPromise,
earlyExitPromise
]);
if (errors.length > 0) {
throw errors[0];
}
});
}
events() {
return this.eventObservable;
}
queue(taskFunction) {
const { maxQueuedJobs = Infinity } = this.options;
if (this.isClosing) {
throw 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) => {
// Prevent unhandled rejections here as we assume the user will use
// `pool.completed()`, `pool.settled()` or `task.catch()` to handle errors
this.debug(`Task #${taskID} errored:`, error);
});
const task = {
id: taskID,
run: taskFunction,
cancel: () => {
if (this.taskQueue.indexOf(task) === -1)
return;
this.taskQueue = this.taskQueue.filter(someTask => someTask !== task);
this.eventSubject.next({
type: pool_types_1.PoolEventType.taskCanceled,
taskID: task.id
});
},
then: taskCompletion.then.bind(taskCompletion)
};
if (this.taskQueue.length >= maxQueuedJobs) {
throw Error("Maximum number of pool tasks queued. Refusing to queue another one.\n" +
"This 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({
type: pool_types_1.PoolEventType.taskQueued,
taskID: task.id
});
this.scheduleWork();
return task;
}
terminate(force) {
return __awaiter(this, void 0, void 0, function* () {
this.isClosing = true;
if (!force) {
yield this.completed(true);
}
this.eventSubject.next({
type: pool_types_1.PoolEventType.terminated,
remainingQueue: [...this.taskQueue]
});
this.eventSubject.complete();
yield Promise.all(this.workers.map((worker) => __awaiter(this, void 0, void 0, function* () { return thread_1.Thread.terminate(yield worker.init); })));
});
}
}
WorkerPool.EventType = pool_types_1.PoolEventType;
/**
* Thread pool constructor. Creates a new pool and spawns its worker threads.
*/
function PoolConstructor(spawnWorker, optionsOrSize) {
// The function exists only so we don't need to use `new` to create a pool (we still can, though).
// If the Pool is a class or not is an implementation detail that should not concern the user.
return new WorkerPool(spawnWorker, optionsOrSize);
}
PoolConstructor.EventType = pool_types_1.PoolEventType;
/**
* Thread pool constructor. Creates a new pool and spawns its worker threads.
*/
exports.Pool = PoolConstructor;