Search code examples
javascripttypescriptpromisethrottling

Throttle amount of promises open at a given time


The following TypeScript performs each call to doSomething(action) one at a time. (Meaning the second item in the list does not get a call made until the first one is done).

async performActionsOneAtATime() {
    for (let action of listOfActions) {
        const actionResult = await doSomethingOnServer(action);
        console.log(`Action Done: ${actionResult}`);
    }
 }

This one will send all the requests to the server right away (without waiting for any responses):

async performActionsInParallel() {
    for (let action of listOfActions) {
        const actionResultPromise = doSomething(action);
        actionResultPromise.then((actionResult) => {
            console.log(`Action Done: ${actionResult}`);
        });
    }
}

But what I really need is a way to throttle them. Maybe have 10 or 20 calls open at a time. (One at at a time is too slow, but all 600 will overload the server.)

But I am having a hard time figuring this out.

Any suggestions on how I can throttle the number of calls to X open at a time?

(This question uses TypeScript, but I would be fine with an ES6 JavaScript answer.)


Solution

  • You can do this in one short function. (Returns values in order per Mulan's suggestion. Thanks!)

    /**
     * Performs a list of callable actions (promise factories) so
     * that only a limited number of promises are pending at any
     * given time.
     *
     * @param listOfCallableActions An array of callable functions,
     *     which should return promises.
     * @param limit The maximum number of promises to have pending
     *     at once.
     * @returns A Promise that resolves to the full list of values
     *     when everything is done.
     */
    function throttleActions(listOfCallableActions, limit) {
      // We'll need to store which is the next promise in the list.
      let i = 0;
      let resultArray = new Array(listOfCallableActions.length);
    
      // Now define what happens when any of the actions completes.
      // Javascript is (mostly) single-threaded, so only one
      // completion handler will call at a given time. Because we
      // return doNextAction, the Promise chain continues as long as
      // there's an action left in the list.
      function doNextAction() {
        if (i < listOfCallableActions.length) {
          // Save the current value of i, so we can put the result
          // in the right place
          let actionIndex = i++;
          let nextAction = listOfCallableActions[actionIndex];
          return Promise.resolve(nextAction()).then(result => {
            // Save results to the correct array index.
            resultArray[actionIndex] = result;
          }).then(doNextAction);
        }
      }
    
      // Now start up the original <limit> number of promises.
      // i advances in calls to doNextAction.
      let listOfPromises = [];
      while (i < limit && i < listOfCallableActions.length) {
        listOfPromises.push(doNextAction());
      }
      return Promise.all(listOfPromises).then(() => resultArray);
    }
    
    // Test harness:
    
    function delay(name, ms) {
      return new Promise((resolve, reject) => setTimeout(() => {
        console.log(name);
        resolve(name);
      }, ms));
    }
    
    var ps = [];
    for (let i = 0; i < 10; i++) {
      ps.push(() => {
        console.log(`begin ${i}`);
        return delay(`complete ${i}`, Math.random() * 3000);
      });
    }
    
    throttleActions(ps, 3).then(result => console.log(result));