The task queue implementation below provides a simple, drop-in solution for ensuring dynamically generated asynchronous tasks are executed sequentially. A typical use case for this would be update-requests that need to be executed in the order they were created. It works by chaining the submitted promises and assumes that all promises are eventually fulfilled or rejected.
// The entry we store in the queue
// Contains the task supplier (so as to ensure the promise is not triggered before its time) and the respective resolve/reject functions
type Entry = {
lazyTask: () => Promise<{}>;
resolve: (v?: {} | PromiseLike<{}>) => void;
reject: (reason?: any) => void;
};
export class TaskQueue {
private queue = new Array<Entry>();
// Pushes a task into the queue and returns a promise that will be fulfilled/rejected when the has been processed
public push<T>(lazyTask: () => Promise<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
let entry = { lazyTask: lazyTask, resolve: resolve, reject: reject };
this.pushTask(entry);
});
}
private executeNext() {
if (this.queue.length === 0) {
return;
}
let item = this.queue[0];
try {
let task = item.lazyTask();
task
.then((val) => {
item.resolve(val);
this.popTask();
})
.catch((error) => {
item.reject(error);
this.popTask();
});
} catch (e1) {
// Even if the client screwed up - the show must go on
try {
item.reject(e1);
} catch (e2) {
// The rejection handler threw an error so we just log it and move on - no point in trying again
// You may want to log the error to e.g. Sentry here
console.error(e2);
}
this.popTask();
}
}
private popTask() {
this.queue.shift();
this.executeNext();
}
private pushTask(entry: Entry) {
this.queue.push(entry);
// If it is the first and only task we need to trigger the execution of the queue
if (this.queue.length === 1) {
this.executeNext();
}
}
}