Preview:
export interface ICallFn {
  resolve: (value: unknown) => void;
  reject: (reason?: unknown) => void;
  fnToCall: (...args: any[]) => Promise<any>;
  args: any[];
}

export default class Semaphore {
  currentRequests: ICallFn[];
  runningRequests: number;
  maxConcurrentRequests: number;
  /**
   * Creates a semaphore that limits the number of concurrent Promises being handled
   * @param {*} maxConcurrentRequests max number of concurrent promises being handled at any time
   */
  constructor(maxConcurrentRequests = 1) {
    this.currentRequests = [];
    this.runningRequests = 0;
    this.maxConcurrentRequests = maxConcurrentRequests;
  }

  /**
   * Returns a Promise that will eventually return the result of the function passed in
   * Use this to limit the number of concurrent function executions
   * @param {*} fnToCall function that has a cap on the number of concurrent executions
   * @param  {...any} args any arguments to be passed to fnToCall
   * @returns Promise that will resolve with the resolved value as if the function passed in was directly called
   */
  callFunction(fnToCall, ...args) {
    return new Promise((resolve, reject) => {
      this.currentRequests.push({
        resolve,
        reject,
        fnToCall,
        args
      });
      this.tryNext();
    });
  }

  tryNext() {
    if (!this.currentRequests.length) {
      return;
    }
    if (this.runningRequests < this.maxConcurrentRequests) {
      const { resolve, reject, fnToCall, args } = this.currentRequests.shift();
      this.runningRequests++;
      fnToCall(...args)
        .then((res) => resolve(res))
        .catch((err) => reject(err))
        .finally(() => {
          this.runningRequests--;
          this.tryNext();
        });
    }
  }
}
downloadDownload PNG downloadDownload JPEG downloadDownload SVG

Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!

Click to optimize width for Twitter