UNPKG

concurrent-promise-queue

Version:

Allows promises to be queued up and executed at a maximum rate defined by time or max concurrency

127 lines (126 loc) 5.99 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConcurrentPromiseQueue = void 0; const uuid_1 = require("uuid"); class ConcurrentPromiseQueue { constructor(options) { this.maxNumberOfConcurrentPromises = options.maxNumberOfConcurrentPromises || 1000; this.unitOfTimeMillis = options.unitOfTimeMillis || 100; this.maxThroughputPerUnitTime = options.maxThroughputPerUnitTime || 1000; this.promisesToExecute = []; this.promisesBeingExecuted = {}; this.promiseExecutedCallbacks = {}; this.promiseCompletedTimesLog = []; this.reattemptTimeoutId = null; } numberOfQueuedPromises() { return this.promisesToExecute.length; } numberOfExecutingPromises() { return Object.keys(this.promisesBeingExecuted).length; } /** * The queue takes a function that returns a promise. * This function will be called at the point where the promise is going to be executed. * * @param promiseSupplier - A function that returns a promise. */ addPromise(promiseSupplier) { // return a promise that will complete when the promise from the promise supplier has been run. return new Promise(((resolve, reject) => { // add the promise to list of promises to be executed and also register a callback with the same id // so that when this promise has been executed, we can call the callback and resolve the promise to return to the caller const id = uuid_1.v4(); this.promisesToExecute.push({ id, promiseSupplier, }); this.promiseExecutedCallbacks[id] = (result) => { if (result.isSuccess) { resolve(result.result); } else { reject(result.error); } }; // call execute to kick off the processing of promises if it hasn't already started. this.execute(); })); } execute() { // check to see if we have anything to execute if (this.promisesToExecute.length === 0) { return; } // check to see how many promises have been run in the last unit of time const now = new Date(); const startOfTimeUnit = new Date(now.getTime() - this.unitOfTimeMillis); const promisesFinishedInLastUnitTime = this.promiseCompletedTimesLog.filter(time => { return time.getTime() >= startOfTimeUnit.getTime(); }); const numberOfPromisesFinishedInLastUnitTime = promisesFinishedInLastUnitTime.length; const numberOfPromisesBeingExecuted = Object.keys(this.promisesBeingExecuted).length; const numberOfPromisesLeftInConcurrencyLimit = this.maxNumberOfConcurrentPromises - numberOfPromisesBeingExecuted; const numberOfPromisesLeftInRateLimit = this.maxThroughputPerUnitTime - numberOfPromisesFinishedInLastUnitTime; const numberOfPromisesToStart = Math.min(numberOfPromisesLeftInConcurrencyLimit, numberOfPromisesLeftInRateLimit); if (numberOfPromisesToStart <= 0) { // if we are not starting any more promises, we should check to see if we are going to start more later if (!this.reattemptTimeoutId) { // given we are in the situation where no more promises are being started, we need to decide how long to wait const periodToWaitToReattemptPromisesMillis = numberOfPromisesFinishedInLastUnitTime > 0 ? now.getTime() - promisesFinishedInLastUnitTime[0].getTime() : this.unitOfTimeMillis; this.reattemptTimeoutId = setTimeout(() => { this.reattemptTimeoutId = null; this.execute(); }, periodToWaitToReattemptPromisesMillis); } return; } // if we can run more promises, run more promises until we hit the max or run out of promises for (let count = 0; count < numberOfPromisesToStart; count++) { const nextPromiseToStart = this.promisesToExecute.shift(); if (!nextPromiseToStart) { return; } const id = nextPromiseToStart.id; const promiseExecutionListener = this.promiseExecutedCallbacks[id]; if (!promiseExecutionListener) { continue; } this.promisesBeingExecuted[id] = nextPromiseToStart; // run the promise and pass the result back to the callback associated with this promise nextPromiseToStart.promiseSupplier() .then(res => { delete this.promiseExecutedCallbacks[id]; delete this.promisesBeingExecuted[id]; promiseExecutionListener({ isSuccess: true, result: res, error: null, }); }) .catch(err => { delete this.promiseExecutedCallbacks[id]; delete this.promisesBeingExecuted[id]; promiseExecutionListener({ isSuccess: false, result: null, error: err, }); }) .finally(() => { // eslint-disable-next-line no-shadow const now = new Date(); // eslint-disable-next-line no-shadow const startOfTimeUnit = new Date(now.getTime() - this.unitOfTimeMillis); this.promiseCompletedTimesLog.push(now); this.promiseCompletedTimesLog = this.promiseCompletedTimesLog.filter(time => { return time.getTime() >= startOfTimeUnit.getTime(); }); this.execute(); }); } } } exports.ConcurrentPromiseQueue = ConcurrentPromiseQueue;