18

My problem is that I don't know how to know when a dynamic promise array has all the promises resolved.

Here an example:

var promiseArray = [];
promiseArray.push(new Promise(){/*blablabla*/});
promiseArray.push(new Promise(){/*blablabla*/});
Promise.all(promiseArray).then(function(){
    // This will be executen when those 2 promises are solved.
});
promiseArray.push(new Promise(){/*blablabla*/});

I have a problem here. The Promise.all behavior will be executed when the previous 2 promises are solved, BUT, before those 2 promises were solved, a third promise where added and this new one won't be take in account.

So, what I need, is say something like: "Hey Promise.all, you have a dynamic array to check". How can I do it?

Remember that this is just an example. I know I can move the line Promise.all to the last line, but actually the new promises are added dynamically when another promises are solved, and the new promises could add new promises as well, so, it's a really dynamic array.

The real use case that I have is something like this:

  1. I use Twitter API to check if there are new Tweets (using the Search Api).
  2. In case I found new Tweets, I add it to a MongoDB (here we have Promises).
  3. In case that those new Tweets are related to a user that I do not have in my MongoDB (here we have new promises because I have to go to MongoDB to check if I have that user), we go to Twitter API to get user info (more promise) and we add those new users to MongoDB (yes, more promises).
  4. Then, I go to MongoDB to insert new values to associate the new tweets with those new users (more promises! wiii!).
  5. When all the queries to MongoDB are Resolved (all the selects, updates, inserts), close the MongoDB connection.

Another hard example:

var allPromises = [];

allPromises.push(new Promise(function(done, fail){
    mongoDB.connect(function(error){
        //Because mongoDB works with callbacks instead of promises
        if(error)
            fail();
        else
            ajax.get('/whatever').then(function(){
                if (somethingHappens) {
                    allPromises.push(new Promise(function(done, fail){ //This promise never will be take in account
                        // bla bla bla
                        if (somethingHappens) {
                            allPromises.push(new Promise(function(done, fail){ //This promise never will be take in account
                                // bla bla bla
                            }));
                        } else {
                            ajax.get('/whatever/2').then(function(){
                                if (somethingHappens) {
                                    allPromises.push(new Promise(function(done, fail){ //This promise never will be take in account
                                        // bla bla bla
                                    }));
                                }
                            });
                        }
                    }));
                } else {
                    ajax.get('/whatever/2').then(function(){
                        if (somethingHappens) {
                            allPromises.push(new Promise(function(done, fail){ //This promise never will be take in account
                                // bla bla bla
                                    if (somethingHappens) {
                                        allPromises.push(new Promise(function(done, fail){ //This promise never will be take in account
                                            // bla bla bla
                                        }));
                                    } else {
                                        ajax.get('/whatever/2').then(function(){
                                            if (somethingHappens) {
                                                allPromises.push(new Promise(function(done, fail){ //This promise never will be take in account
                                                    // bla bla bla
                                                }));
                                            }
                                        });
                                    }
                            }));
                        }
                    });
                }
            });
    });
}));

Promise.all(allPromises).then(function(){
    // Soooo, all work is done!
    mongodb.close()!
});

So, now, a beauty example. We need to call the showAllTheInformation function when the last (we don't know which is the last) promise is called. How do you do it?:

var name = 'anonimus';
var date = 'we do not know';

function userClikOnLogIn() {
    $http.get('/login/user/password').then(function(data){
        if (data.logguedOk) {
            $http.get('/checkIfIsAdmin').then(function(data){
                if (data.yesHeIsAnAdmin) {
                    $http.get('/getTheNameOfTheUser').then(function(data){
                        if(data.userHasName) {
                            $http.get('/getCurrentDate').then(function(data){
                                currentDate = data.theNewCurrentDate;
                            });
                        }
                    });
                }
            });
        }
    });
}

function showAllTheInformation() {
    alert('Hi ' + name + ' today is:' + date);
}

here another example with more context: https://jsfiddle.net/f0a1s79o/2/

Broda Noel
  • 1,760
  • 1
  • 19
  • 38
  • What's your use case for dynamically adding promises? – Bergi Jun 14 '16 at 01:43
  • @Bergi I just added more info about the use case – Broda Noel Jun 14 '16 at 01:53
  • 1
    What is issue with using steps 1-5 as described at Question? – guest271314 Jun 14 '16 at 01:54
  • 1
    @guest271314 Because I have 300 forEach adding 300 promises that when they are solved probably add 300 more promises or probably not. So you do not know who and when is added the last promise in the array. So you don't know when you should call the Promise.all – Broda Noel Jun 14 '16 at 02:00
  • @NoelBroda Whether one, two or three-hundred promises should not matter. If process requires five steps, return `Promise` to iterable within `Promise.all()` when step 5 completes. – guest271314 Jun 14 '16 at 02:02
  • @NoelBroda: Have a look at [this](http://stackoverflow.com/q/30872003/1048572) and [that](http://stackoverflow.com/q/36449621/1048572). Unless you show us your actual code that you have problems with, we can't help you with it. – Bergi Jun 14 '16 at 02:04
  • Why do you push promises to array within first call to `.push()`? Also, all callbacks parameters are identified by `done`, `fail` ? – guest271314 Jun 14 '16 at 02:10
  • Oh my. The first thing you must do is to [avoid the `Promise` constructor antipattern](http://stackoverflow.com/q/23803743/1048572). And then, follow my [rules of thumb](http://stackoverflow.com/a/25756564/1048572) to `return` a promise from *every* *single* `function` in which you are doing something asynchronous. Suddenly you'll realise that you don't need that `allPromises` array any more - you just got a promise that fulfills when everything is done already. – Bergi Jun 14 '16 at 02:13
  • @guest271314 why I added more promise inside the first Promise? Because in case that I found new tweets, we have to save it, and search new things related those new tweets (or maybe not). It's like: "in case the user is logged properly, execute an ajax to see if he is an admin, and in that case, execute an ajax to see if he is another thing, and then... ... and then if all promises are done, say 'Hello!'" – Broda Noel Jun 14 '16 at 02:13
  • @NoelBroda `Promise.all()` may not be necessary a single `Promise()`, if using constructor, could probably be utilized to achieve expected result. You can remove first `.push()`, use `.push` to store values from ajax calls to `promiseArray` within first `Promise` constructor returned, call `done` at fifth `ajax.get('/whatever/2')` – guest271314 Jun 14 '16 at 02:15
  • Please, check the new beauty example – Broda Noel Jun 14 '16 at 02:26
  • @NoelBroda Which example is actual Question, first or second? Either example could use `Promise` constructor, `resolve` at last nested `.then()` or callback – guest271314 Jun 14 '16 at 02:28
  • @guest271314 no because if data.logguedOk is false, the last then() never will be called – Broda Noel Jun 14 '16 at 02:34
  • _"no because if data.logguedOk is false, the last then() never will be called"_ What is expected result if `data.logguedOk` is `false`? – guest271314 Jun 14 '16 at 02:36
  • @guest271314 showAllTheInformation should be called. – Broda Noel Jun 14 '16 at 02:39
  • @guest271314 and in case of data.yesHeIsAnAdmin is false, we also need to call the showAllTheInformation function, and the same in case of data.userHasName is false – Broda Noel Jun 14 '16 at 02:40
  • As you have no other success answer, I suggest [this method I implemented](http://stackoverflow.com/a/12622730/443685). let me know please – Saic Siquot Jun 14 '16 at 13:41

5 Answers5

22

You can make a neat little recursive function to wrap Promise.all to handle additions to the original promise:

/**
 * Returns a Promise that resolves to an array of inputs, like Promise.all.
 *
 * If additional unresolved promises are added to the passed-in iterable or
 * array, the returned Promise will additionally wait for those, as long as
 * they are added before the final promise in the iterable can resolve.
 */
function iterablePromise(iterable) {
  return Promise.all(iterable).then(function(resolvedIterable) {
    if (iterable.length != resolvedIterable.length) {
      // The list of promises or values changed. Return a new Promise.
      // The original promise won't resolve until the new one does.
      return iterablePromise(iterable);
    }
    // The list of promises or values stayed the same.
    // Return results immediately.
    return resolvedIterable;
  });
}

/* Test harness below */

function timeoutPromise(string, timeoutMs) {
  console.log("Promise created: " + string + " - " + timeoutMs + "ms");
  return new Promise(function(resolve, reject) {
    window.setTimeout(function() {
      console.log("Promise resolved: " + string + " - " + timeoutMs + "ms");
      resolve();
    }, timeoutMs);
  });
}

var list = [timeoutPromise('original', 1000)];
timeoutPromise('list adder', 200).then(function() {
  list.push(timeoutPromise('newly created promise', 2000));
});
iterablePromise(list).then(function() { console.log("All done!"); });

In ES6 with lambdas and without comments, this can be even shorter:

function iterablePromise(iterable) {
  return Promise.all(iterable).then((resolvedIterable) => {
    if (iterable.length != resolvedIterable.length) {
      return iterablePromise(iterable);
    }
    return resolvedIterable;
  });
}

Or, as Rads expressed with async/await in their answer, but as a function:

async function iterablePromise(iterable) {
  let resolvedIterable = [];
  while (iterable.length !== resolvedIterable.length) {
    resolvedIterable = await Promise.all(iterable);  // implicit "then"
  }
  return resolvedIterable;
}

Bear in mind that this only covers addition, and that it's still a little dangerous: You need to ensure that the callback order is such that any promises in flight add themselves to the list before the Promises.all callback can be invoked.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • 1
    Wow, finally a reasonable implementation of a function that does this, complete with a good explanation! So much better than all the deleted answers together :-) – Bergi Jun 14 '16 at 18:16
  • Just want to say I finally got around to implementing this, and it works great. I find this more intuitive than the other solutions available. – Josiah Feb 21 '17 at 18:20
  • 1
    Brilliant. Why is this not the accepted answer? WTF is wrong with people? – Lea Verou Apr 06 '17 at 22:40
  • This should be added to the Promise spec, seriously. – Eran Medan Oct 09 '17 at 00:46
  • 1
    This works awesome for recursive functions that could add additional promises as a result. You can add to the iterable before resolving the base case. Thanks for this! – Brad Swerdfeger Feb 04 '20 at 04:15
  • This has the disadvantage that any promise added later will not be resolved before all initial promises have been resolved, hasn't it? Please correct me if I am wrong. – Christoph Thiede Sep 16 '21 at 09:59
  • 1
    @ChristophThiede I'm not sure which sense you mean. If you mean about delays or ordering, in most cases the asynchronous work is started as soon as the Promise is created, so this method poses no additional concerns; for exceptions like Google's RPC library that wait for a call to `then` before they begin, you could manually call `.then` or just initiate `Promise.all` or `iterablePromise` again. If you're talking about the caveat in the JSDoc and footnote, it just reminds that Promises are one-shot mechanisms: after the `iterablePromise` resolves it won't respect anything you add to it later. – Jeff Bowman Sep 16 '21 at 13:19
4

I know I am late to the party here. However, for those who felt sad and didn't find a quick answer, here is a dirty (explanation later) way of moving ahead without having to re-architect IF you are sure that a NEW promise will NOT be added AFTER completion of all existing promises.

var promiseArray = [], completedPromises = [];
promiseArray.push(new Promise(){/*blablabla1*/});
promiseArray.push(new Promise(){/*blablabla2*/});
while(completedPromises.length != promiseArray.length) completedPromises = await Promise.all(promiseArray);

Elsewhere in the code (before completion of all previous promises:

promiseArray.push(new Promise(){/*blablabla3*/});

Hope this helps someone. I created a stack overflow account after years of freeloading only for this :)

Rads
  • 166
  • 1
  • 7
2

There's no way out. You have to put all the promises in the array before calling Promise.all in it. In the example you presented, that's as simple as moving the last line to the top.

In case you are asynchronously filling the array, you should get a promise for that array, and use .then(Promise.all.bind(Promise)). If you don't know when you stop adding promises, this is impossible anyway as they might never all be resolved at all.


Regarding your "beauty example", you will want to learn about the magic of chaining. As I previosly said in the comments, you have to return a promise from every function in which you are doing anything asynchronous. Indeed, just add the missing returns:

function userClikOnLogIn() {
    return $http.get('/login/user/password').then(function(data){
//  ^^^^^^
        if (data.logguedOk) {
            return $http.get('/checkIfIsAdmin').then(function(data){
//          ^^^^^^
                if (data.yesHeIsAnAdmin) {
                    return $http.get('/getTheNameOfTheUser').then(function(data){
//                  ^^^^^^
                        if(data.userHasName) {
                            return $http.get('/getCurrentDate').then(function(data){
//                          ^^^^^^
                                currentDate = data.theNewCurrentDate;
                            });
                        }
                    });
                }
            });
        }
    });
}

userClikOnLogIn().then(function showAllTheInformation() {
//               ^^^^^ now you can chain onto it!
    alert('Hi ' + name + ' today is:' + date);
});

There is no array of promises here that dynamically grows, it's just that every function is returning a promise for the (asynchronous) result of the things it does.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 2
    I'm sorry but I think this is a bad answer. You are recommending OP to reproduce [the pyramid of doom](http://www.telerik.com/blogs/what-is-the-point-of-promises) that is one of the main reason promises where introduced in the first place. – Quentin Roy Jun 14 '16 at 04:03
  • @Roque This Answer provides correct result. Requirement is for five steps to complete before calling final function – guest271314 Jun 14 '16 at 04:11
  • @guest271314 It is not because it can work that it is a proper way to do it. Promises, and how to effectively use them, are often misunderstood while they are a very powerful tool. Have a look at [this article](https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html) for example. – Quentin Roy Jun 14 '16 at 04:15
  • _"It is not because it can work that it is a proper way to do it"_? Is there only one "way to do it"? – guest271314 Jun 14 '16 at 04:18
  • @guest271314 [this](https://gist.github.com/royhowie/d26813b8628cf993676088cfeac9c49e) is probably how I'd do it. – royhowie Jun 14 '16 at 04:19
  • @royhowie Returns same results as `js` at Answer. – guest271314 Jun 14 '16 at 04:30
  • 2
    @Roque: No, you're mistaken. This is not a callback pyramid of doom. Promises are used correctly as return values here - the pyramid you're seeing is just nested if blocks, which naturally leads to indentation even without promises. This is exactly the control flow that is wanted. Of course you can also use exceptions, but neither approach is "bad" or wrong. – Bergi Jun 14 '16 at 05:19
  • 1
    @Bergi I agree it is not a callback pyramid and it is a bit more complex that most pyramid. But I still find it a bit ugly and it is avoidable. I think it is a perfect use case for manual reject. – Quentin Roy Jun 14 '16 at 05:21
  • Here a example created with a little bit more of context https://jsfiddle.net/f0a1s79o/1/ as you can see there a lot of DB calls that all of them should be finished before closing the connection. How can I do it? – Broda Noel Jun 14 '16 at 17:43
  • @NoelBroda: As I said, avoid the promise constructor antipattern. Use `return Promise.all(values.map(/* some promise-returning function */));` for loops. Don't use `forEach` or push promises to some array at indeterminate times. – Bergi Jun 14 '16 at 17:47
  • @Bergi, please, can you modify https://jsfiddle.net/f0a1s79o/2/ and show me how you would do it? – Broda Noel Jun 14 '16 at 17:50
  • @NoelBroda [Here you are](https://jsfiddle.net/f0a1s79o/3/). Notice that I've oversimplified the promisification of mongodb, you should use a proper one. – Bergi Jun 14 '16 at 17:57
  • @Bergi, looks to have all the sense. Let me check it this night in my home, and I gonna give you the feedback – Broda Noel Jun 14 '16 at 18:10
  • What if an "if" statement have a forEach inside calling multiple http requests and I need to wait for all of them? – Broda Noel Jun 30 '16 at 21:49
  • @NoelBroda: [You don't use `forEach` with promises](http://stackoverflow.com/a/37576787/1048572), but that's another question – Bergi Jun 30 '16 at 21:52
  • Check this example: how do you do it in this example? https://jsfiddle.net/b0rhayof/ – Broda Noel Jun 30 '16 at 21:52
  • I know I shuoldn't use it in this way, but, I have a to it. I need to call some services saving different objects. I'll try to change it but I'm sure I won't have luck – Broda Noel Jun 30 '16 at 21:55
  • @NoelBroda: You don't need luck, you just need `map` and `Promise.all`. If you still got any problems, please [ask a new question](http://stackoverflow.com/questions/ask) – Bergi Jun 30 '16 at 21:57
  • Everything working perfectly after use return Promise.all(myArray.map(function(item) {... Thank you a lot! – Broda Noel Jul 03 '16 at 22:35
1

If you can instrument the promises or their usage, and scoping issues allow for it, then I think you could approach the problem more simply: how many promises are oustanding?

In other words, you don't need to keep track of all the promises, just count them.

var outstanding = 0;

var p1 = new Promise(){/*blablabla*/};
var p2 = new Promise(){/*blablabla*/};

++outstanding;
p1.then( (data) => { ...
  if (0 >= --outstanding) 
    // All resolved!
}

// dynamic set of promises, so later we decide to add another:
var p3 = new Promise(){/*blablabla*/};
++outstanding;
p3.then( ... );  // as above

To improve the above, wrap it all into the meta-promise (equivelent to the one that would be returned by Promise.all for a static set of promises)...

  // Create a promise that tracks completion of a dynamic set of instrumented promises.
  getCompletionP() { 
    let rslv = null;
    const p = new Promise(
      function(resolve, reject) {
        rslv = resolve;
      } );
    p.resolve = rslv;
    assert( p.resolve );
    p.scheduled = 0;
    p.onSchedule = function() { ++this.scheduled; };
    p.onComplete = function()  { if (0 >= --this.scheduled) this.resolve(); };
    return p;
  }

Now call cp.onSchedule() before every call to then(), and cp.onComplete at the end of each then(), and cp will resolve after all your promises then functions are complete. (You would need to handle the catch statements too, of course.)

This will resolve when all code scheduled via Promise.then calls is complete, whereas the question asks for something that will resolve when all promises are resolved. That could be achieved by adding calls after the resolve statement of the promises instead, but that is impossible if using 3rd party libraries, and I think they would be functionally the same.

This won't work for all cases, but since the accepted answer is that it (dynamic set of promises) can't be done, I figure this could still be useful though it got more complicated (messy) as I wrote it out!

Tom
  • 17,103
  • 8
  • 67
  • 75
0

@JeffBowman and @Bergi have the right idea: recursive waiting and counting promises. Here is my implementation in Coffeescript )

Promise = require 'bluebird'

class DynamicPromiseCollection

    promises = []

    add:(p)->
        promises.push p

    wait_for_all:->
        #
        # Wait for all current promises, then check for new promises...
        # ...if there are new promises, then keep waiting ( recursively ).
        #
        # Resolve only when all promises are done, and there are no new promises.
        #
        make_promise = ->
            num_before = promises.length
            p = Promise.all(promises).then ->
                num_after = promises.length
                if num_after > num_before
                    return make_promise() # recursive -- wait again
                else
                    return true # all done now
        p = make_promise()
        return p


#
# let's test this...
#
promises = new DynamicPromiseCollection()


#
# pretend to get remote data
#
get_remote_data = ->
    new Promise (resolve,reject)->
        setTimeout ->
            resolve "data"
        ,500

#
# get data, wait, then get more data...
#
promises.add get_remote_data().then (data)->
    console.log "got " + data
    promises.add get_remote_data().then (data)->
        console.log "got " + data

#
# this should wait for both data
#
promises.wait_for_all().then ->
    console.log "...and wait_for_all is done."
Nick Perkins
  • 8,034
  • 7
  • 40
  • 40