UNPKG

awaitqueue

Version:

TypeScript utility to enqueue asynchronous tasks and run them sequentially one after another

151 lines (150 loc) 6.71 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AwaitQueue = void 0; const Logger_1 = require("./Logger"); const errors_1 = require("./errors"); const logger = new Logger_1.Logger('AwaitQueue'); class AwaitQueue { // Queue of pending tasks (map of PendingTasks indexed by id). pendingTasks = new Map(); // Incrementing PendingTask id. nextTaskId = 0; // Whether stop() method is stopping all pending tasks. stopping = false; constructor() { logger.debug('constructor()'); } get size() { return this.pendingTasks.size; } async push(task, name) { name = name ?? task.name; logger.debug(`push() [name:${name}]`); if (typeof task !== 'function') { throw new TypeError('given task is not a function'); } if (name) { try { Object.defineProperty(task, 'name', { value: name }); } catch (error) { } } return new Promise((resolve, reject) => { const pendingTask = { id: this.nextTaskId++, task: task, name: name, enqueuedAt: Date.now(), executedAt: undefined, completed: false, resolve: (result) => { // pendingTask.resolve() can only be called in execute() method. Since // resolve() was called it means that the task successfully completed. // However the task may have been stopped before it completed (via // stop() or remove()) so its completed flag was already set. If this // is the case, abort here since next task (if any) is already being // executed. if (pendingTask.completed) { return; } pendingTask.completed = true; // Remove the task from the queue. this.pendingTasks.delete(pendingTask.id); logger.debug(`resolving task [name:${pendingTask.name}]`); // Resolve the task with the obtained result. resolve(result); // Execute the next pending task (if any). const [nextPendingTask] = this.pendingTasks.values(); // NOTE: During the resolve() callback the user app may have interacted // with the queue. For instance, the app may have pushed a task while // the queue was empty so such a task is already being executed. If so, // don't execute it twice. if (nextPendingTask && !nextPendingTask.executedAt) { void this.execute(nextPendingTask); } }, reject: (error) => { // pendingTask.reject() can be called within execute() method if the // task completed with error. However it may have also been called in // stop() or remove() methods (before or while being executed) so its // completed flag was already set. If so, abort here since next task // (if any) is already being executed. if (pendingTask.completed) { return; } pendingTask.completed = true; // Remove the task from the queue. this.pendingTasks.delete(pendingTask.id); logger.debug(`rejecting task [name:${pendingTask.name}]: %s`, String(error)); // Reject the task with the obtained error. reject(error); // Execute the next pending task (if any) unless stop() is running. if (!this.stopping) { const [nextPendingTask] = this.pendingTasks.values(); // NOTE: During the reject() callback the user app may have interacted // with the queue. For instance, the app may have pushed a task while // the queue was empty so such a task is already being executed. If so, // don't execute it twice. if (nextPendingTask && !nextPendingTask.executedAt) { void this.execute(nextPendingTask); } } }, }; // Append task to the queue. this.pendingTasks.set(pendingTask.id, pendingTask); // And execute it if this is the only task in the queue. if (this.pendingTasks.size === 1) { void this.execute(pendingTask); } }); } stop() { logger.debug('stop()'); this.stopping = true; for (const pendingTask of this.pendingTasks.values()) { logger.debug(`stop() | stopping task [name:${pendingTask.name}]`); pendingTask.reject(new errors_1.AwaitQueueStoppedError()); } this.stopping = false; } remove(taskIdx) { logger.debug(`remove() [taskIdx:${taskIdx}]`); const pendingTask = Array.from(this.pendingTasks.values())[taskIdx]; if (!pendingTask) { logger.debug(`stop() | no task with given idx [taskIdx:${taskIdx}]`); return; } pendingTask.reject(new errors_1.AwaitQueueRemovedTaskError()); } dump() { const now = Date.now(); let idx = 0; return Array.from(this.pendingTasks.values()).map(pendingTask => ({ idx: idx++, task: pendingTask.task, name: pendingTask.name, enqueuedTime: pendingTask.executedAt ? pendingTask.executedAt - pendingTask.enqueuedAt : now - pendingTask.enqueuedAt, executionTime: pendingTask.executedAt ? now - pendingTask.executedAt : 0, })); } async execute(pendingTask) { logger.debug(`execute() [name:${pendingTask.name}]`); if (pendingTask.executedAt) { throw new Error('task already being executed'); } pendingTask.executedAt = Date.now(); try { const result = await pendingTask.task(); // Resolve the task with its resolved result (if any). pendingTask.resolve(result); } catch (error) { // Reject the task with its rejected error. pendingTask.reject(error); } } } exports.AwaitQueue = AwaitQueue;