Implementing a batching mechanism with promises

In many scenarios, it is common to perform multiple asynchronous operations concurrently and then collect the results together for further processing. However, there are cases where it is necessary to limit the number of concurrent operations for better resource management or to comply with API rate limits.

In JavaScript, promises allow us to work with asynchronous operations in a more structured manner. We can leverage promises to implement a batching mechanism that limits the concurrency of multiple asynchronous operations while still maintaining the benefits of parallel processing.

Batching with Promises

To implement a batching mechanism, we need to define a few components:

  1. Batcher: This is the main class responsible for managing the batching process. It receives a list of tasks and a batch size as parameters.
  2. Task: A task represents an individual asynchronous operation that needs to be executed. It can be a function that returns a promise or a promise itself.
  3. Executor: The executor is responsible for executing the tasks in batches and collecting their results.

Let’s start by implementing the Batcher class:

class Batcher {
    constructor(tasks, batchSize) {
        this.tasks = tasks;
        this.batchSize = batchSize;
    }

    run() {
        const executor = new Executor(this.batchSize);
        return executor.execute(this.tasks);
    }
}

Next, let’s implement the Executor class:

class Executor {
    constructor(batchSize) {
        this.batchSize = batchSize;
    }

    async execute(tasks) {
        const results = [];

        for (let i = 0; i < tasks.length; i += this.batchSize) {
            const batch = tasks.slice(i, i + this.batchSize);
            const batchResults = await Promise.all(batch.map(this.executeTask));
            results.push(...batchResults);
        }

        return results;
    }

    executeTask(task) {
        if (typeof task === 'function') {
            return task();
        }

        return task;
    }
}

In the Executor class, we iterate over the tasks array in batches of size batchSize. We use Promise.all() to execute each batch of tasks concurrently. The executeTask() method is responsible for executing an individual task. It checks if the task is a function (representing a promise-returning function) or a promise itself, and executes it accordingly.

To use the batching mechanism, create an instance of the Batcher class and call the run() method:

const tasks = [
    asyncTask1,
    asyncTask2,
    asyncTask3,
    //...
];

const batchSize = 3;

const batcher = new Batcher(tasks, batchSize);
batcher.run()
    .then(results => {
        console.log(results);
    })
    .catch(error => {
        console.error(error);
    });

Here, tasks is an array of asynchronous tasks, batchSize defines the maximum number of concurrent tasks, and batcher.run() starts the batching process. The run() method returns a promise that resolves with the array of collected results.

By limiting the concurrency with promises, we can handle API rate limits more effectively, reduce resource usage, and improve overall performance.

Conclusion

Implementing a batching mechanism with promises allows us to manage multiple asynchronous operations more efficiently. By controlling the concurrency, we can optimize resource usage and improve the overall performance of our applications.

With the Batcher and Executor classes implemented, you can easily integrate this batching mechanism into your own projects and harness the benefits of parallel processing with controlled concurrency.

References

  1. Promise - MDN Web Docs #javascript #promises
  2. Concurrency Control - Wikipedia #programming #concurrency