src/utils/PromiseQueue.js
const resolvePromises = (promises, result) => {
promises.forEach(p => p.resolve(result));
};
const rejectPromises = (promises, err) => {
promises.forEach(p => p.reject(err));
};
/**
* The PromiseQueue class offers a promise queue, enabling us to execute promise in sequence, with some restrictions.
* It provides a method "queue", which is in charge of queueing and executing every callback it receives.
*
* Example:
* we have an empty queue and we add a functionA to execute. Once funcionA is executing, we queue functionB, functionC
* and functionB again. When functionA ends its execution, the queue decides to execute the last call of functionB.
* At this point, functionC call is rejected because there are more priority calls. Once functionB ends its execution,
* the first call to functionB is resolved or rejected depending on the result.
*
* NOTE: more use cases of this class can be seen in test/unit/utils/PromiseQueue
* @private
*/
export class PromiseQueue {
constructor() {
this.executionQueue = [];
this.executing = false;
}
/**
* Method which receives a callback expected to return a promise and the type of its callback.
* It also returns a promise which will be resolved when the callback ends its execution, or rejected if an error
* occurs or the callback aren't going to be executed.
* When there are multiple callbacks waiting for being executed, it executes the last one; rejecting all the prior
* added callbacks with a different type. Once the selected callback ends its execution, all the prior promises of the
* callbacks with the same type are rejected or resolved in function of its result.
*
* @param cb function to execute. It musts return a promise.
* @param type type of the callback to execute. Useful in order to decide how to resolve or reject callbacks which
* aren't going to be executed because there are more priority ones.
* @returns Promise Promise which will be resolved or rejected with the result of executing the callback.
*/
queue(cb, type) {
const executionPromise = this._addToExecutionQueue({
cb,
type,
});
if (!this.executing) {
this._startExecution();
}
return executionPromise;
}
_addToExecutionQueue(executionData) {
return new Promise((resolve, reject) => {
executionData.resolve = resolve;
executionData.reject = reject;
this.executionQueue.push(executionData);
});
}
_startExecution() {
this.executing = true;
return this._decideKeepExecuting();
}
_decideKeepExecuting() {
return this.executionQueue.length > 0 ?
this._executeNextFunction()
.then(() => this._decideKeepExecuting())
:
this._endExecution();
}
_endExecution() {
return Promise.resolve()
.then(() => {
this.executing = false;
});
}
_executeNextFunction() {
const functionToExecute = this.executionQueue.pop();
const sameTypePromises = this._getSameTypePromises(functionToExecute.type);
this._cleanExecutionQueue(functionToExecute.type);
return functionToExecute.cb()
.then((result) => {
functionToExecute.resolve(result);
resolvePromises(sameTypePromises, result);
})
.catch((err) => {
functionToExecute.reject(err);
rejectPromises(sameTypePromises, err);
});
}
_cleanExecutionQueue(type) {
this._rejectDifferentTypePromises(type);
this.executionQueue = [];
}
_getSameTypePromises(type) {
return this.executionQueue
.filter(executionData => executionData.type === type);
}
_rejectDifferentTypePromises(type) {
return this.executionQueue
.filter(executionData => executionData.type !== type)
.map((executionData) => {
executionData.reject(Error('Unable to call this function because there are more priority ones'));
});
}
}