awaitqueue
Version:
TypeScript utility to enqueue asynchronous tasks and run them sequentially one after another
160 lines (159 loc) • 7.15 kB
JavaScript
;
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;
constructor() {
logger.debug('constructor()');
}
get size() {
return this.pendingTasks.size;
}
async push(task, name, options) {
name = name ?? task.name;
logger.debug(`push() [name:${name}, options:%o]`, options);
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) => {
if (name && options?.removeOngoingTasksWithSameName) {
for (const pendingTask of this.pendingTasks.values()) {
if (pendingTask.name === name) {
pendingTask.reject(new errors_1.AwaitQueueRemovedTaskError(), {
canExecuteNextTask: false,
});
}
}
}
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, { canExecuteNextTask }) => {
// 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);
// May execute next pending task (if any).
if (canExecuteNextTask) {
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()');
for (const pendingTask of this.pendingTasks.values()) {
logger.debug(`stop() | stopping task [name:${pendingTask.name}]`);
pendingTask.reject(new errors_1.AwaitQueueStoppedError(), {
canExecuteNextTask: 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(), {
canExecuteNextTask: true,
});
}
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, { canExecuteNextTask: true });
}
}
}
exports.AwaitQueue = AwaitQueue;