5

I know this isn't in the scope of a Array.map but I'd like to wait until the previous item has finished its promise before starting the next one. It just happens that I need to wait for the previous entry to be saved in the db before moving forwards.

const statsPromise = stats.map((item) => {
    return playersApi.getOrAddPlayer(item, clubInfo, year); //I need these to wait until previous has finished its promise.
});

Promise.all(statsPromise)
.then((teamData) => {
  ..//
});

playersApi.getOrAddPlayer returns a new Promise

Edit

Reading more on it, it seems its important to show playersApi.getOrAddPlayer

getOrAddPlayer: function (item, clubInfo, year) {
    return new Promise((resolve, reject) => {

        var playerName = item.name.split(' '),
            fname = playerName[0].caps(),
            sname = playerName[1].caps();

                Players.find({
                    fname: fname,
                    sname: sname,
                }).exec()
                .then(function(playerDetails, err){
                    if(err) reject(err);
                    var savePlayer = new Players();
                    //stuff
                    savePlayer.save()
                    .then(function(data, err){
                        if(err)  reject(err);
                        item._id = data._id;
                        resolve(item);
                    });
                });
            });
}
nem035
  • 34,790
  • 6
  • 87
  • 99
Jamie Hutber
  • 26,790
  • 46
  • 179
  • 291
  • @FelixKling I have re-opened this because I feel the answer in the dup you mentioned was not very good. I know there are other dups out there that are better; if you can find them, let me know. –  Dec 17 '16 at 04:54
  • You'll forgive the agnosticism of my comments, but I don't really work with promises (I know I _should_, but considering I don't do this professionally there's not a reason to) but what I may consider doing is keeping an array of items needed to be completed outside the scope of the promise function, then just `Array.pop()`-ing the data off and creating a new promise based on that which would do the same thing. – Jhecht Dec 17 '16 at 05:01
  • Related: [Promise version of a “while” loop?](http://stackoverflow.com/q/37552459/218196) – Felix Kling Dec 17 '16 at 05:56
  • Also [Reduce an array to chained promises](http://stackoverflow.com/documentation/javascript/231/promises/5917/reduce-an-array-to-chained-promises#t=201612171244341259699) in SO Documentation. – Roamer-1888 Dec 17 '16 at 14:24
  • [Bluebird's `Promise.mapSeries()`](http://bluebirdjs.com/docs/api/promise.mapseries.html) will do this. – jfriend00 Dec 17 '16 at 18:31

5 Answers5

15

You can use reduction instead of mapping to achieve this:

stats.reduce(
  (chain, item) =>
    // append the promise creating function to the chain
    chain.then(() => playersApi.getOrAddPlayer(item, clubInfo, year)),
  // start the promise chain from a resolved promise
  Promise.resolve()
).then(() => 
  // all finished, one after the other
);

Demonstration:

const timeoutPromise = x => {
  console.log(`starting ${x}`);
  return new Promise(resolve => setTimeout(() => {
    console.log(`resolving ${x}`);
    resolve(x);
  }, Math.random() * 2000));
};

[1, 2, 3].reduce(
  (chain, item) => chain.then(() => timeoutPromise(item)),
  Promise.resolve()
).then(() =>
  console.log('all finished, one after the other')
);

If you need to accumulate the values, you can propagate the result through the reduction:

stats
  .reduce(
    (chain, item) =>
      // append the promise creating function to the chain
      chain.then(results =>
        playersApi.getOrAddPlayer(item, clubInfo, year).then(data =>
          // concat each result from the api call into an array
          results.concat(data)
        )
      ),
    // start the promise chain from a resolved promise and results array
    Promise.resolve([])
  )
  .then(results => {
    // all finished, one after the other
    // results array contains the resolved value from each promise
  });

Demonstration:

const timeoutPromise = x => {
  console.log(`starting ${x}`);
  return new Promise(resolve =>
    setTimeout(() => {
      console.log(`resolving result for ${x}`);
      resolve(`result for ${x}`);
    }, Math.random() * 2000)
  );
};

function getStuffInOrder(initialStuff) {
  return initialStuff
    .reduce(
      (chain, item) =>
        chain.then(results =>
          timeoutPromise(item).then(data => results.concat(data))
        ),
      Promise.resolve([])
    )
}

getStuffInOrder([1, 2, 3]).then(console.log);

Variation #1: Array.prototype.concat looks more elegant but will create a new array on each concatenation. For efficiency purpose, you can use Array.prototype.push with a bit more boilerplate:

stats
  .reduce(
    (chain, item) =>
      chain.then(results =>
        playersApi.getOrAddPlayer(item, clubInfo, year).then(data => {
          // push each result from the api call into an array and return the array
          results.push(data);
          return results;
        })
      ),
    Promise.resolve([])
  )
  .then(results => {

  });

Demonstration:

const timeoutPromise = x => {
  console.log(`starting ${x}`);
  return new Promise(resolve =>
    setTimeout(() => {
      console.log(`resolving result for ${x}`);
      resolve(`result for ${x}`);
    }, Math.random() * 2000)
  );
};

function getStuffInOrder(initialStuff) {
  return initialStuff
    .reduce(
      (chain, item) =>
        chain.then(results =>
          timeoutPromise(item).then(data => {
            results.push(data);
            return results;
          })
        ),
      Promise.resolve([])
    );
}

getStuffInOrder([1, 2, 3]).then(console.log);

Variation #2: You can lift the results variable to the upper scope. This would remove the need to nest the functions to make results available via the nearest closure when accumulating data and instead make it globally available to the whole chain.

const results = [];
stats
  .reduce(
    (chain, item) =>
      chain
        .then(() => playersApi.getOrAddPlayer(item, clubInfo, year))
        .then(data => {
          // push each result from the api call into the globally available results array
          results.push(data);
        }),
    Promise.resolve()
  )
  .then(() => {
    // use results here
  });

Demonstration:

const timeoutPromise = x => {
  console.log(`starting ${x}`);
  return new Promise(resolve =>
    setTimeout(() => {
      console.log(`resolving result for ${x}`);
      resolve(`result for ${x}`);
    }, Math.random() * 2000)
  );
};

function getStuffInOrder(initialStuff) {
  const results = [];
  return initialStuff.reduce(
    (chain, item) =>
      chain
        .then(() => timeoutPromise(item))
        .then(data => {
          results.push(data);
          return results;
        }),
    Promise.resolve()
  );
}

getStuffInOrder([1, 2, 3]).then(console.log);

nem035
  • 34,790
  • 6
  • 87
  • 99
  • 1
    Nice one pal :) I must say, I didn't get what the ` year)), // start the promise chain from a resolved promise Promise.resolve()` comma in here is doing? We are returning the chain of the promise with each loop. I just don't understand the way the function is set up with the , – Jamie Hutber Dec 18 '16 at 14:14
  • Also in this example, we aren't passing the values returning from playersApi to the then – Jamie Hutber Dec 18 '16 at 14:16
  • The comma separates the two arguments passed into `stats.reduce`. The first argument is a function that takes the promise chain and the current item and further builds the chain and the second argument is an empty resolved promise from which to start the chain. – nem035 Dec 18 '16 at 19:44
  • As far as accumulating the values, there are two approaches. One is to create an array in the outer scope and push the result every time a promise resolves, `(chain, item) => chain.then(() => playersApi.getOrAddPlayer(item, clubInfo, year)).then((result) => outerArray.push(result));`. The other is to propagate the results through the chain either with recursion, as shown in [guest271314's answer](http://stackoverflow.com/a/41195776/3928341), or by passing the results through each call to `reduce`, or to each call to `playersApi.getOrAddPlayer`. I'll add an example. – nem035 Dec 18 '16 at 19:56
  • 1
    I love this approach ... so smart! Thx! – M'sieur Toph' Jul 09 '20 at 08:25
3

If you are fine with using promise library, you can use Promise.mapSeries by Bluebird for this case.

Example:

const Promise = require("bluebird");
//iterate over the array serially, in-order
Promise.mapSeries(stats, (item) => {
  return playersApi.getOrAddPlayer(item, clubInfo, year));
}).then((teamData) => {
  ..//
});
alpha
  • 1,103
  • 7
  • 10
1

You could use a kind of recursion:

function doStats([head, ...tail]) {
  return !head ? Promise.resolve() :
    playersApi.getOrAddPlayer(head, clubInfo, year)
      .then(() => doStats(tail));
}

doStats(stats)
  .then(() => console.log("all done"), e => console.log("something failed", e));

Another classic approach is to use reduce:

function doStats(items) {
  return items.reduce(
    (promise, item) => 
      promise.then(() => playersApi.getOrAddPlayer(item, clubInfo, year)),
    Promise.resolve());

By the way, you could clean up your getOrAddPlayer function quite a bit, and avoid the promise constructor anti-pattern, with:

getOrAddPlayer: function (item, clubInfo, year) {
    var playerName = item.name.split(' '),
        fname = playerName[0].caps(),
        sname = playerName[1].caps();

    return Players.find({fname, sname}).exec()
      .then(playerDetails => new Players().save())
      .then({_id} => Object.assign(item, {_id}));
}
  • Hot dam, this is some clean JS. I am pretty new the whole promise chain :) Interesting in your first function that you can use `Promise.resolve()` as a marker for when its finished. – Jamie Hutber Dec 17 '16 at 12:22
  • In cleaning up `GetOrAddPlayer` I believed that you had to return a promise. I didn't realise that you can just return the whole promise chain from Mongoose? – Jamie Hutber Dec 17 '16 at 12:22
  • @JamieHutber Exactly. You already have the promise that came back from Mongoose; you don't need to wrap it in another promise. That's the [explicit promise constructor anti-pattern](http://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it). –  Dec 17 '16 at 12:25
  • *Interesting in your first function that you can use `Promise.resolve()` as a marker for when it's finished.* Right. This corresponds to the notion that if you feed it zero items, you're successfully done. –  Dec 17 '16 at 12:26
  • I had never thought to use `Promise.resolve` like this. I have always imagined it as a way to just.,.. well actually yes I did but not to apply it like this :D Now I just need to return the data collected in `playersApi.getOrAddPlayer` and then we're golden :) – Jamie Hutber Dec 18 '16 at 14:21
1

You can use a recursion solution

const statsPromise = (function s(p, results) {
  return p.length ? playersApi.getOrAddPlayer(p.shift(), clubInfo, year) : results;
})(stats.slice(0), []);

statsPromise
.then((teamData) => {
//do stuff
});

let n = 0;
let promise = () => new Promise(resolve => 
                setTimeout(resolve.bind(null, n++), 1000 * 1 + Math.random()));

let stats = [promise, promise, promise];

const statsPromise = (function s(p, results) {
  return p.length ? p.shift().call().then(result => {
    console.log(result);
    return s(p, [...results, result])
  }) : results;
})(stats.slice(0), []);
    
statsPromise.then(res => console.log(res))
guest271314
  • 1
  • 15
  • 104
  • 177
0

I gave it a thought but I didn't find a better method than the reduce one.

Adapted to your case it would be something like this:

const players = [];
const lastPromise = stats.reduce((promise, item) => {
  return promise.then(playerInfo => {
    // first iteration will be undefined
    if (playerInfo) {
       players.push(playerInfo)
    }
    return playersApi.getOrAddPlayer(item,  clubInfo, year);
  });
}, Promise.resolve());

// assigned last promise to a variable in order to make it easier to understand
lastPromise.then(lastPlayer => players.push(lastPlayer));

You can see some explanation about this here.

Antonio Val
  • 3,200
  • 1
  • 14
  • 27