UNPKG

threads

Version:

Web workers & worker threads as simple as a function call

291 lines (290 loc) 12.6 kB
"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;