Jamie Hutber
Jamie Hutber

Reputation: 28126

Javascript Map that waits for previous promise before starting next?

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);
                    });
                });
            });
}

Upvotes: 5

Views: 4311

Answers (5)

nem035
nem035

Reputation: 35501

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);


Upvotes: 15

user663031
user663031

Reputation:

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}));
}

Upvotes: 1

Antonio Val
Antonio Val

Reputation: 3340

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.

Upvotes: 0

guest271314
guest271314

Reputation: 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))

Upvotes: 1

alpha
alpha

Reputation: 1113

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) => {
  ..//
});

Upvotes: 3

Related Questions