Wednesday 4 December 2019

Promise.all with a Limit

A few months ago I wrote a post about how to limit the number of async operations that we run in parallel in JavaScript. Last week I came up with something related, but a bit different. What if we want to run n async operations and wait for all of them to complete, but we want to limit how many of them we run in parallel. A good example of this is if we want to do several http requests and combine their results, but we don't want to flood the server with all the requests at the same time (indeed browsers manage this themselves limiting how many requests to the same server they open in parallel, so we won't be overloading the server, but at JavaScript level the requests are already launched, and the timeout starts to count...). Waiting for all the async operations to complete is what we get with Promise.all, but it does not provide a limit option.

Well, the idea seems simple to implement, here it goes my take on it:

class PromiseAllWithLimit{
    //tasks: Array of functions that return a Promise
    constructor(tasks, maxItems){
        this.allTasks = tasks;
        this.maxItems = maxItems;
        this.taskResults = [];
        this.nextTaskId = 0;
        this.resolveAll = null; //points to the resolve function to invoke when all tasks are complete
        this.completedCounter = 0;
    }

    //tasks: Array of functions that return a Promise
    //returns a Promise that gets resolved when all tasks are done
    run(){
         while(this.nextTaskId < this.maxItems){
            this._runNextTask();
        }
        return new Promise((res) => this.resolveAll = res);
    }

    _runNextTask(){
        //we need this var to get it trapped in the closure to run in the task.then()
        let curTaskId = this.nextTaskId
        let nextTask = this.allTasks[this.nextTaskId];
        this.nextTaskId++;
        nextTask().then((result) => {
            this.completedCounter++;
            this.taskResults[curTaskId] = result;
            if (this.nextTaskId < this.allTasks.length){
                this._runNextTask();
            }
            else{
                //no more tasks to launch
                //if all tasks are complete, complete the All Promise
                if(this.completedCounter === this.allTasks.length){
                    this.resolveAll(this.taskResults);
                }
            }
        });
    }
}

That we can use like this:

function getRandomInt(max) {
    return Math.floor(Math.random() * Math.floor(max));
  }

  //returns Promise
function getPost(postId){
    console.log("getPost " + postId + " started");
    return new Promise(res => {
        setTimeout(() => {
            console.log("---> getPost " + postId + " finishing");
            res(`${postId} - content`)
        }, 2000 * (1 + getRandomInt(2)));
    });
}

(async () => {
    let tasks = [];
    for (let i=0; i<10; i++){
        tasks.push(() => getPost("postId_" + i));
    }
    let results = await new PromiseAllWithLimit(tasks, 5).run();
    console.log("all tasks are complete");
    console.log(results.join("\n"));
})();

There's an important point to notice. While Promise.all receives an array of promises, our PromiseAllWithLimit receives an array of functions that return a Promise. I'm calling these functions "tasks" in my code (that have little to do with .Net Tasks), let's think of them as "lazy-promises". I can not directly pass Promises, cause that would mean that we are already running all the functions, that is just what we want to limit. We return a Promise that will get resolved once all the "tasks" are complete (so we store its resolve "callback" for later invokation).

I've uploaded the code to this gist.

No comments:

Post a Comment