Repository: caolan/async
Pull request #1514 (2018-02-14 20:44:11)

fix "RangeError: Maximum call stack size exceeded"
`eachOfLimit` throws a `RangeError: Maximum call stack size exceeded` when the collection is too big and the iteratee is synchronous.
The usual workaround is to use `setImmediate(next)` or [http://caolan.github.io/async/docs.html#ensureAsync](http://caolan.github.io/async/docs.html#ensureAsync).

I have a data analysis program. The `setImmediate(next)` workaround made the whole program slow when there are few new data to analyse.

With a small modification, it was possible to make the program lasting from minutes to seconds.

Here is an exemple of time duration with and without `setImmediate`

```js
const eachOfLimit = require("async/eachOfLimit");

(function() {
    'use strict';

    const collection = new Array(Math.pow(1024, 2));
    for (let i = 0, len = collection.length; i < len; i++) {
        collection[i] = i + 1;
    }

    const timerInit = Date.now();
    eachOfLimit(collection, 1, (value, i, next) => {
        next();
    }, err => {
        console.log("sync", Date.now() - timerInit, "ms");
    });
}());

(function() {
    'use strict';

    const collection = new Array(Math.pow(1024, 2));
    for (let i = 0, len = collection.length; i < len; i++) {
        collection[i] = i + 1;
    }

    const timerInit = Date.now();
    eachOfLimit(collection, 1, (value, i, next) => {
        setImmediate(next);
    }, err => {
        console.log("async", Date.now() - timerInit, "ms");
    });
}());
```


Patch:
diff --git a/lib/internal/eachOfLimit.js b/lib/internal/eachOfLimit.js
index da46fa4..0a1f5bc 100644
--- a/lib/internal/eachOfLimit.js
+++ b/lib/internal/eachOfLimit.js
@@ -15,6 +15,7 @@ export default function _eachOfLimit(limit) {
         var nextElem = iterator(obj);
         var done = false;
         var running = 0;
+        var looping = false;
 
         function iterateeCallback(err, value) {
             running -= 1;
@@ -26,12 +27,13 @@ export default function _eachOfLimit(limit) {
                 done = true;
                 return callback(null);
             }
-            else {
+            else if (!looping) {
                 replenish();
             }
         }
 
         function replenish () {
+            looping = true;
             while (running < limit && !done) {
                 var elem = nextElem();
                 if (elem === null) {
@@ -44,6 +46,7 @@ export default function _eachOfLimit(limit) {
                 running += 1;
                 iteratee(elem.value, elem.key, onlyOnce(iterateeCallback));
             }
+            looping = false;
         }
 
         replenish();


Test patch:
diff --git a/mocha_test/eachOf.js b/mocha_test/eachOf.js
index 925f995..dd73c22 100644
--- a/mocha_test/eachOf.js
+++ b/mocha_test/eachOf.js
@@ -43,6 +43,30 @@ describe("eachOf", function() {
         });
     });
 
+    it('forEachOf no call stack size exceed error', function(done) {
+        var obj = {};
+        var len = 3000;
+        var args = new Array(len * 2);
+        var expected = new Array(len * 2);
+
+        for (var i = 0; i < len; i++) {
+            obj["a" + i] = i;
+            expected[2 * i] = "a" + i;
+            expected[2 * i + 1] = i;
+        }
+
+        async.forEachOf(obj, function(value, key, callback) {
+            var index = parseInt(key.slice(1), 10);
+            args[2 * index] = key;
+            args[2 * index + 1] = value;
+            callback();
+        }, function(err) {
+            assert(err === null, err + " passed instead of 'null'");
+            expect(args).to.eql(expected);
+            done();
+        });
+    });
+
     it('forEachOf - instant resolver', function(done) {
         var args = [];
         async.forEachOf({ a: 1, b: 2 }, function(x, k, cb) {
@@ -139,6 +163,30 @@ describe("eachOf", function() {
         });
     });
 
+    it('forEachOfSeries no call stack size exceed error', function(done) {
+        var obj = {};
+        var len = 3000;
+        var args = new Array(len * 2);
+        var expected = new Array(len * 2);
+
+        for (var i = 0; i < len; i++) {
+            obj["a" + i] = i;
+            expected[2 * i] = "a" + i;
+            expected[2 * i + 1] = i;
+        }
+
+        async.forEachOfSeries(obj, function(value, key, callback) {
+            var index = parseInt(key.slice(1), 10);
+            args[2 * index] = key;
+            args[2 * index + 1] = value;
+            callback();
+        }, function(err) {
+            assert(err === null, err + " passed instead of 'null'");
+            expect(args).to.eql(expected);
+            done();
+        });
+    });
+
     it('forEachOfSeries empty object', function(done) {
         async.forEachOfSeries({}, function(x, callback){
             assert(false, 'iteratee should not be called');
@@ -274,6 +322,18 @@ describe("eachOf", function() {
         setTimeout(done, 25);
     });
 
+    it('forEachOfLimit no call stack size exceed error', function(done) {
+        var count = 0;
+        async.forEachOfLimit(_.range(1024 * 1024), Infinity, function(x, i, callback){
+            count++;
+            callback();
+        }, function(err){
+            if (err) throw err;
+            expect(count).to.equal(1024 * 1024);
+            done();
+        });
+    });
+
     it('forEachOfLimit error', function(done) {
         var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 };
         var call_order = [];