0

In the code below, I am trying to do the following:

  • Have Stats(), getOverallStats() and GetGroups() to run in parallel. Each returns a promise.
  • The forEach in GetGroups.then() should run sequentially to ensure the output is in the correct order.
  • Once ALL of the above is complete, then run some more code.

However, I am getting very confused with the promises! The logging gives me:

looping
here
looping
looping

But what I am looking for is here to be at the end.

Finally, at the moment I have hardcoded loopAgelist[1] for testing purposes. But, I actually want to be able to loop through loopAgelist[] with a timeout in between! I would appreciate if someone could explain some promise 'rules' to use in these complicated cases.

    var loopAgeList;
    var looppromises = [];
    getAgeGroupList().then(function (loopAgeList) {
        var statsPromise = Stats(loopAgeList[1]);
        var oStatsPromise = getOverallStats();
        var grpPromise = GetGroups(loopAgeList[1]).then(function (groups) {
            var promise = Parse.Promise.as();
            groups.forEach(function (grp) {
                promise = promise.then(function () {    // Need this so that the tables are drawn in the correct order (force to be in series)
                    console.log("looping")
                    if (grp != "KO"){
                        var standingsPromise = Standings(loopAgeList[1], grp);
                        looppromises.push(standingsPromise);
                    }

                    var fixPromise = GetFixtures(loopAgeList[1], grp);
                    looppromises.push(fixPromise);
                    return fixPromise;
                });
            });
            return Parse.Promise.all(looppromises);
        });
        var promises = [statsPromise, oStatsPromise, grpPromise, looppromises];
        Parse.Promise.all(promises).then(function(results) {
            console.log("here");
        });
    });
SimpleOne
  • 816
  • 3
  • 11
  • 21

3 Answers3

1

The rewrite can be improved significantly by adopting a couple simple style rules: (1) there's no need to create a resolved promise and then chain to it (in fact, most would consider this an anti-pattern), (2) promises to be run together by iterating an array of operands is the perfect application of array .map (not reduce), (3) most importantly, smaller, testable, promise-returning functions always clears up the mystery.

Putting all that together, the main function can be as simple as this...

function loopOverOnce(agegroup) {
    let statsPromise = Stats(agegroup);
    let oStatsPromise = getOverallStats();
    let grpPromise = GetGroups(agegroup).then(function(groups) {
        return getStandingsAndFixturesForGroups(groups, agegroup);
    });
    return Parse.Promise.all([statsPromise, oStatsPromise, grpPromise]);
}

Let's write getStandingsAndFixturesForGroups. It's only job will be map the groups and aggregate promises to do work on each...

function getStandingsAndFixturesForGroups(groups, agegroup) {
    let promises = groups.map(function(group) {
        return getStandingsAndFixturesForGroup(group, agegroup);
    });
    return Parse.Promise.all(promises);
}

Now, getStandingsAndFixturesForGroup, a function to do the async work on a single group, conditionally for part of the work...

function getStandingsAndFixturesForGroup(group, agegroup) {
    let promises = (group != "KO")?  [ Standings(agegroup, grp) ] : [];
    promises.push(GetFixtures(agegroup, group));
    return Parse.Promise.all(promises);  // this is your standings promise (conditionally) and fixtures promise
}

Done. I'd test this code in the reverse order that it's presented here.

EDIT The OP also asks how to perform several promises, serially, interspersed with timeouts. Here's my advice.

First, a slightly simpler version of your delay function, which is a good example when it is right to create a new promise (because there's nothing at bottom to call to get one)

function delay(interval) {
    return new Promise(function(resolve, reject){
        setTimeout(function() {resolve();}, interval);
    });
};

And reducing is a good way to build a list of promises, including interspersed delays...

getAgeGroupList().then(function (loopAgeList) {
    loopAgeList.reduce(function(promise, agegroup) {
        return promise.then(function() {
            let promises = [loopOverOnce(agegroup), delay(15000)];
            return Promise.all(promises);
        });
    }, Promise.as());
});

A couple notes: this results in a sequence like loopOverOnce, timeout, loopOverOnce, timeout, ... etc.. If you'd like a timeout first, reverse the order of the little chain in the inner loop:

[ delay(15000), loopOverOnce(agegroup) ]

Final note is that all this could be made even shorter and prettier by adopting ES6 fat arrow syntax for anonymous functions, e.g.

loopAgeList.reduce((promise, agegroup) => {
    promise.then(() => Promise.all([loopOverOnce(agegroup), delay(15000)]));
}, Promise.as());
danh
  • 55,236
  • 10
  • 89
  • 124
  • Your code is certainly much better structured than mine! Could you please explain your (1) in a bit more detail - why is it an anti-pattern? Should the `loopAgeList.reduce()` also be re-written using `map`? – SimpleOne May 09 '18 at 19:23
  • @SimpleOne - :-) about creating promises then chaining, it's not so much bad as unnecessary. (This author considers it bad, see the second section in https://runnable.com/blog/common-promise-anti-patterns-and-how-to-avoid-them). There's no need to create a promise if the thing you're calling returns a promise. In the case of Parse, the internals return promises (often passed on from mongo). – danh May 09 '18 at 21:37
  • @SimpleOne - my answer failed to address the loopAgeList part of your question. I'll edit now to do so. – danh May 09 '18 at 21:38
  • Perhaps I am missing something here but the `loopAgeList.forEach()` does not give loopOverOnce, timeout, loopOverOnce, timeout, ... . Doesn't the `forEach` need to be chained so it waits after each iteration? If I add `console.log(agegroup)` on entry to the `forEach()` I quickly get a list of all age groups without a delay in between. – SimpleOne May 10 '18 at 08:29
  • @SimpleOne, you're quite right! I was so intent on scrubbing the reduces, that I missed the key point. We can do this a number of ways, but not what I suggested, and using reduce is a good way to avoid nesting. Edited. – danh May 10 '18 at 14:52
0

The problem is, that you pass a nested array to Promise.all:

  var promises = [statsPromise, oStatsPromise, grpPromise, looppromises];

Just flatten that:

 var promises = [statsPromise, oStatsPromise, grpPromise, ...looppromises];
 // Or
 var promises = [statsPromise, oStatsPromise, grpPromise].concat(looppromises);

However you still need to await promise somewhere to ensure that the chain finished its execution, otherwise looppromise will be empty.


All in all it might be better to use async / await to make everything much more readable:

(async function() {

  const ageGroups = await getAgeGroupList();

  const statsPromise = Stats(ageGroups[1]);
  const overallStatsPromise = getOverallStats();

  const groups = await GetGroups(ageGroups[1]);

  for(const group of groups) {
    const [standings, fixtures] = await Promise.all(
       Standings(ageGroups[1], group),
       GetFixtures(ageGroups[1], group)
    );
    // Do something with standings & fixtures
  }

   const [stats, overallStats] = await Promise.all(statsPromise, overallStatsPromise);

   // Do whatever you want with stats etc.
})();
Jonas Wilms
  • 106,571
  • 13
  • 98
  • 120
0

I have re-written it using reduce and seem to have it working. Comments on this would be welcome (i.e. are there any issues with this code).

        function loopOverOnce(agegroup) {
        var statsPromise = Stats(agegroup);
        var oStatsPromise = getOverallStats();

        var grpPromise = GetGroups(agegroup).then(function (groups) {
            function getStandingsAndFixtures(groups) {
                var promise = Parse.Promise.as();
                return groups.reduce(function (promise, grp) {
                    return promise.then(function (result) {
                        var standingsPromise = Parse.Promise.as();
                        if (grp != "KO") {
                            standingsPromise = Standings(agegroup, grp);
                        }
                        var fixPromise = GetFixtures(agegroup, grp);
                        console.log("fixPromise");
                        return Parse.Promise.all([standingsPromise, fixPromise]);
                    });
                }, promise);
            }
            var sfPromise = getStandingsAndFixtures(groups).then(function () { console.log("Test1") });
            return sfPromise;
        });

        return Parse.Promise.all([statsPromise, oStatsPromise, grpPromise]).then(function () { console.log("Test2") });
    }

    getAgeGroupList().then(function (loopAgeList) {
        // https://stackoverflow.com/questions/39538473/using-settimeout-on-promise-chain
        function delay(t, v) {
            return new Promise(function (resolve) {
                setTimeout(resolve.bind(null, v), t)
            });
        }

        var promise = Parse.Promise.as();
        loopAgeList.reduce(function (promise, agegroup) {
            return promise.then(function () {
                return delay(15000).then(function () {
                    return loopOverOnce(agegroup);
                });
            });
        }, promise);
    });
SimpleOne
  • 816
  • 3
  • 11
  • 21
  • You may already be satisfied with how this is working, but there's some room for improvement illustrated in my answer. – danh May 09 '18 at 19:14