6

Regarding this jsFiddle, I am trying to dynamically add a "deferred" which is created when an event triggers, so the done callback is only called when all deferred are resolved, including those added later:

Relevant code:

var promises = [ deferred1, ... ];
var p = when.all(promises).then(function() {
  console.log('All done!!');
  //! trigger
});

promises.push( deferredFromEvent ); // << ignored

update: suggestions using Q or jQuery are welcome, I am looking for one that works

Hontoni
  • 1,332
  • 1
  • 16
  • 27
  • Nice question! I *adore* working with Promises and this challenge cheered me up :) – Ates Goral Feb 24 '13 at 03:53
  • @AtesGoral ^_^ I found that when working with a lot of deferred objects and events things get to slow(~300 obj = ~500ms till promise fires) . I made a much simpler solution that works fast(0ms). I'll put it here after i check your answer for speed.. – Hontoni Feb 24 '13 at 06:33

4 Answers4

1

Treat your fixed promises as a separate bundle than the dynamic one(s).

Wait on the bundled promises and then pipe their combined outcome through a $.when() on an initially empty array. You can dynamically push new promises into this array any time.

http://jsfiddle.net/atesgoral/YVkVa/1/

HTML:

<p>You have 5 seconds to inject!</p>
<button id="inject">Inject Deferred 3</button>
<button id="resolve">Resolve Deferred 3</button>
<p>Deferred 1: <span id="dfd1">pending<span></p>
<p>Deferred 2: <span id="dfd2">pending<span></p>
<p>Deferred 3: <span id="dfd3">pending<span></p>
<h1 id="result"></h1>

JavaScript:

var dfd1 = $.Deferred(),
    dfd2 = $.Deferred(),
    fixed = [ dfd1.promise(), dfd2.promise() ],
    multiplexed = $.when.apply($, fixed), // Bundle the fixed ones
    reservoir = []; // Reservoir for dynamic promises

// The solution to your problem lies here. The rest is scaffolding.
var ultimate = multiplexed.pipe(function () {
    return $.when.apply($, reservoir);
});

ultimate.done(function() {
    $("#result").text("Done!");
});

window.setTimeout(function () {
    dfd1.resolve();
    $("#dfd1").text("resolved");
}, 2500);

window.setTimeout(function () {
    dfd2.resolve();
    $("#dfd2").text("resolved");
}, 5000);

var dfd3 = $.Deferred();

$("#inject").click(function () {
    reservoir.push(dfd3.promise());
});

$("#resolve").click(function () {
    dfd3.resolve();
    $("#dfd3").text("resolved");
});
Ates Goral
  • 137,716
  • 26
  • 137
  • 190
0

Create all of your promises that you know you need up-front. Build a .when with them. Save the promise returned from the .when. As you add new events, which require new promises, add new .whens, using the promise from previous .whens, plus whatever new promises you've completed.

Your application will have multiple single points of failure, if you're using the final .when as your "Go ahead with the app.". IE: if any one promise fails at any point, then any .when created after that is also going to fail.

...but if that's your intent, or you've got some solid error-handling, it should do you.

I'm trying to keep this library-agnostic -- usually, I use my own implementation, which is half-way between something jQuery does, and something that Crockford did in a recent talk, but if we assume that:

functions return promise-handlers
"when" returns a promise-handler promise-handlers have at least a .done and .fail -- or accept two arguments, or whatever and whatever happens inside of a function will control whether the promise is rejected/resolved or kept/broken (or whatever), then you might end up with a bunch of functionality that looks like:

var doing = doSomething(),     // returns promise
    making = makeSomething(),  // returns promise
    loading = loadSomething(), // returns promise


    dependencies_lvl_1 = when(doing, making, loading);

Later on, you might add a new module or widget -- maybe it saves some work:

var saving = saveSomething(), //returns promise
    dependencies_lvl_2 = when(dependencies_lvl_1, saving);

Maybe after that, you need to switch pages, but you need your data to cache first

var caching = cacheData(),   // returns promise
    when(dependencies_lvl_2, caching)
        .done(goToNextPage)
        .fail(handleError);

If you look at it, you know for a fact that as long as when returns a promise which only succeeds if all promises are kept (and when all promises are kept), and none of them broke, then dependencies_lvl_2 there, includes all dependencies from dependencies_lvl_1, plus additional promises.

The level-3 .when, then, has a resolution depending on every single thing which has been added to the chain.

And as long as you keep caching your promises into variables (or some sort of future-access), you can keep chaining these together, into eternity.

Norguard
  • 26,167
  • 5
  • 41
  • 49
  • 'As you add new events, which require new promises' >> which require the original promise.(if i'm not confused about the terms). Anyway, this is exactly what I am trying to avoid, the appending of when.done.. am looking for a simple way to "update" the promises array later in the code, and somehow let know the promise framework that the array contents is altered.. – Hontoni Feb 19 '13 at 18:07
  • may be cancel the original 'when' and recreate a new one? – Hontoni Feb 19 '13 at 18:10
  • @Antonimo Think about that for a minute: you want a `when` which fires when all dependencies are done. But you want it to fire when you add new dependencies. But those dependencies can be added arbitrarily at any time in the future. But ***promises can only be resolved one time***. So what happens if all of the current promises have resolved, but you want to push a new one? What does your `when` do? `when`, itself, returns a promise (which caches one single value and one single state, never to be updated, and informs all listeners past, present and future). – Norguard Feb 19 '13 at 18:29
  • Meanwhile, if you cache the return value of the first `when`, you don't need to keep the rest of the promises around: the first `when` is a promise, and the outcome of that promise depends on the outcome of all of the previous promises. So just feed that `when` into a new `when`, if you have new promises you need. The second `when().done` will only be called IF every promise in the first when was successful, AND any promises you're adding to the second one are successful. The point of `when` is to remove the need to manage synchronicity, and create a synchronus bottleneck. `push` kills that – Norguard Feb 19 '13 at 18:32
  • If you're looking to fire a message any time the queue only contains finished promises, and all promises are successful, then you will need a moderator or an observer or pub/sub, which will fire their event multiple times (ie: each time you add a promise, or each time the queue if only filled with 100%-resolved promises). – Norguard Feb 19 '13 at 18:35
  • you are right, id do want some error exeption when push.ing to resolved'all', because I am not expecting more at that point. I do think i can make it work using one global defer() and recreating it when promises array updates.. – Hontoni Feb 19 '13 at 19:15
  • Then just make your own cue, give it a push, and delete and recreate a `when`, every time you push something, manage your own subscriptions in a separate cue, et cetera, create a promise inside of your cue, with each new `when` you create, also create a new promise, set the resolution of that promise to the resolution of the new `when`, etc. ...if you'll notice, this is a *lot* of work to keep yourself from doing the following: `all_initial_promises = when(p1, p2, p3, p4);` `new_promises = (all_initial_promises, new_1, new_2);` – Norguard Feb 19 '13 at 19:24
  • @Antonimo You're not going to get a generalized solution for this. And it's not going to be generalized for the exact reason I pointed out. `when` is supposed to be a gateway which guarantees that the code after is going to be 100% synchronous based on all of the async results you added. How can you guarantee synchronicity if you can arbitrarily add asynchronicity, asynchronously? So there won't be a general solution, because if there was, you wouldn't be looking for `when`, you'd be looking for a Moderator/event-Emitter, which fires when triggered, every time, or one time, etc. – Norguard Feb 19 '13 at 19:28
  • using Q.allResolved( that._promises[name] )).then(function(){ seems to call 'then' ONLY when all promises are fulfilled, but it is called as many times as the event triggered. – Hontoni Feb 19 '13 at 21:24
  • @Antonimo If you do it that way, every time you pass something into `then` it's going to get called. `.allResolved` stores an array of promises. When all of the promises are finished, it calls everything that was added to `then`. If you continue to add things to `then`, each time you add something to `then` it's going to get called (assuming that all of the promises resolved). That's how promises work. They wait until they have the final value, cache that value, and then return it to every single thing that asks for it, before or after the promise is resolved... – Norguard Feb 19 '13 at 23:14
  • That's why saving a `when` and putting it into a `when` saves so many headaches. Because that first `when` will only succeed if ALL promises in the `when` are finished, and ALL came back true, and then any time you use a `then` on your `when`, it will fire everything that you subscribe to it, for forever (after it succeeds). – Norguard Feb 19 '13 at 23:18
0

"... so the done callback is only called when all deferred are resolved, including those added later" makes no sense, however I think I know what you mean.

If I understand correctly then you want what might be termed a "re-firable when()" - something like this (based on jQuery) :

function PromiseSet(memory, once) {//javascript Constructor
    var flags = [];
    if(memory) flags.push('memory');
    if(once) flags.push('once');
    var promises = [],
        doneCallbacks = $.Callbacks(flags.join(' ')),
        failCallbacks = $.Callbacks(flags.join(' '));
    this.add = function(promise, val) {
        promises.push(promise);
        if(val) { this.fire(val); }
        return this;
    };
    this.done = function(fn) {
        doneCallbacks.add(fn);
        return this;
    };
    this.fail = function(fn) {
        failCallbacks.add(fn);
        return this;
    };
    this.fire = function(val) {
        val = val || null;
        $.when.apply($, promises).then(
            function() { doneCallbacks.fire(val); },
            function() { failCallbacks.fire(val); }
        );
        return this;
    };
    return this;
}

untested

All methods return this to make them chainable.

If I've written the constructor correctly, then you can control its detailed behaviour by passing booleans to new PromiseSet(). For your propised usage, I think you need to pass (true, false), but try other settings to see what happens.

Sample sequence :

var myPromiseSet = new PromiseSet(true, false);
myPromiseSet.add(promise1);
myPromiseSet.add(promise2).add(promise3);
myPromiseSet.done(myDoneFunction).fail(myFailFunction);
myPromiseSet.fire("foo");
myPromiseSet.add(promise4).fire();
Beetroot-Beetroot
  • 18,022
  • 3
  • 37
  • 44
0

Take a look to this solution. By this implementation you able to track a lot of dinamic added promises. If you are using jQuery Deferred you could do this way. jsFiddle

note that I've used _.every method from lodash library, so you'll need to install lodash too

function doAlotOfAsyncThings(callback) {
    var promises = [];
    var def = $.Deferred();
    console.log('Adding first promise');
    promises.push(def.promise());

    setTimeout(function() {
        // Dinamically adding second promise. Important note: last added promise must be added to array BEFORE previous promise has been resolved
        var def2 = $.Deferred();
        var def3 = $.Deferred();
        console.log('Dinamically adding second and third promises');
        promises.push(def2.promise());
        promises.push(def3.promise());
        console.log('Resolving first promise');
        def.resolve();

        setTimeout(function() {
            console.log('Resolving second and third promises');
            def2.resolve();
            def3.resolve();
        }, 1500);
    }, 1000);


    function checkAllDone() {
        console.log('Checking $.when');
        $.when.apply(null, promises).then(function() {
            // we have to check state of every promise in array
            var all_resolved = _.every(promises, function(elem) { return elem.state() == 'resolved'; });
            if(all_resolved) {
                console.log('All promises are resolved! callback();')
                callback();
            } else {
                console.log('Hm, seems that some promises were dinamically added, waiting for them..');
                checkAllDone();
            }
        });
    }
    checkAllDone();
}

doAlotOfAsyncThings(function(){
    console.log('Done');
});

With Q.js it's even shorter.
In this solution I use _.any method from lodash library

function doAlotOfAsyncThings(callback) {
    var promises = [];
    var def = Q.defer();
    console.log('Adding first promise');
    promises.push(def.promise);

    setTimeout(function() {
        // Dinamically adding second promise. Important note: last added promise must be added to array BEFORE previous promise has been resolved
        var def2 = Q.defer();
        var def3 = Q.defer();
        console.log('Dinamically adding second and third promises');
        promises.push(def2.promise);
        promises.push(def3.promise);
        console.log('Resolving first promise');
        def.resolve();

        setTimeout(function() {
            console.log('Resolving second and third promises');
            def2.resolve();
            def3.resolve();
        }, 1500);
    }, 1000);


    function checkAllDone() {
        console.log('Checking $.when');
        Q.all(promises).then(function() {
            // Q remove resolved promises from array so we have to check if array contains some non-undefined elements (those elements would be non-resolved dinamically added promises)
            if(_.any(promises)) {
                console.log('Hm, seems that some promises were dinamically added, waiting for them..');
                checkAllDone();
            } else {
                console.log('All promises are resolved! callback();')
                callback();
            }
        });
    }
    checkAllDone();
}

doAlotOfAsyncThings(function(){
    console.log('Done');
});
Denis
  • 2,429
  • 5
  • 33
  • 61