Home Reference Source

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'));
			});
	}
}