Skip to content

Instantly share code, notes, and snippets.

@jswartwood
Created January 6, 2012 14:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jswartwood/1570839 to your computer and use it in GitHub Desktop.
Save jswartwood/1570839 to your computer and use it in GitHub Desktop.
Hiccup?
<!DOCTYPE html>
<html>
<body>
<p>These should output "last", "ok", then "all" for 10 promises and then another 10.</p>
<div id="out"></div>
<script type="text/javascript" src="https://raw.github.com/briancavalier/when.js/dev/when.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
var out = document.getElementById("out")
, lastPromise
;
for (var i = 0; i < 10; i++) {
(function( pos ) {
var andDefer = when.defer()
, allPromises = [ lastPromise, andDefer ]
;
when(lastPromise).then(function() {
out.innerHTML += "last done: " + pos + "<br>";
});
lastPromise = when.all(allPromises).then(function() {
out.innerHTML += "all done: " + pos + "<br>";
});
andDefer.then(function( val ) {
out.innerHTML += val + ": " + pos + "<br>";
});
setTimeout(function() {
andDefer.resolve("ok");
}, 1000);
})(i);
}
setTimeout(function() {
out.innerHTML += "<br>";
for (var i = 0; i < 10; i++) {
(function( pos ) {
var andDefer = when.defer()
, allPromises = [ lastPromise, andDefer ]
;
when(lastPromise).then(function() {
out.innerHTML += "last done: " + pos + "<br>";
});
lastPromise = when.all(allPromises).then(function() {
out.innerHTML += "all done: " + pos + "<br>";
});
andDefer.then(function( val ) {
out.innerHTML += val + ": " + pos + "<br>";
});
setTimeout(function() {
andDefer.resolve("ok");
}, 1000);
})(i);
}
}, 10000);
@jswartwood
Copy link
Author

Simplified version of the issue I'm seeing with: https://github.com/jswartwood/and1

@jswartwood
Copy link
Author

Making lastPromise a real promise didn't seem to help. I did try http://jsfiddle.net/jswartwood/pCLrg/3/ but that isn't my use-case. If you look at my And1 repo, you may get a clearer picture of what I'm trying to do.

Basically, And1 comes from:
In some of my cmd-line node scripts I need to queue up async actions so they run ordered and chaining callbacks is a pain. I know node has some fs sync commands, but I'm going to need this with exec or spawn as well. With the cases I'm looking at most calls will be done in the initial pass-through (like this loop example). However, it is possible for additional calls to be made later (via some decision in an async callback), so I will need it to run correctly if lastPromise has already been resolved.

@briancavalier
Copy link

Hey Jake,

tl;dr I think when.reduce() will be your friend :)

The fact that lastPromise is a value initially is what causes the different ordering of the output of the pos === 0 items. First, a callback is registered with lastPromise, which fires immediately. Next, a callback is registered for the array containing lastPromise and andDefer. Internally, that registers a callback with both lastPromise and andDefer. So the all() callback tied to a callback that has made its way into andDefers callback queue before the following call to andDefer.then. So, you can see that when andDefer is resolved in iteration 0, it will first cause the all cascade to happen, outputting "all done: 0", followed by the "ok: 0"

The unanswered question is, why does "last done: 1" appear before "ok: 0". I don't have a complete answer for you, but I think it has something to do with the ordering guarantees that your loop is making (or not making) between andDefer and lastPromise, and between lastPromise and the array passed to when.all. What makes me think that is that the relative order of each i item (i.e. "last", "ok", and "all" for each i) is preserved--except for i === 0, which can be explained (above).

That said, the situation you described sounds like a reduce operation. So, if what you want is guaranteed ordering of a list of tasks that return promises, have a look at when.reduce. It's your basic reduce() function extended to cover promises. It guarantees order of iteration. For example:

var finalResultPromise = when.reduce(
    [task1, task2, task3],
    function(resultOfPreviousTask, task /*, i, total */) {
        // i and total are passed in, but we don't need them in this example
        // i is the current task index in the array of tasks above
        // total is the length of the array above
        return task(resultOfPreviousTask);
    },
    initialValue // or undefined if you don't need an initial val
);

when(finalResultPromise, function(finalResult) {
    console.log(finalResult);
})

or, if you don't need to chain result values, and just want to collect all the results, but still need to guarantee ordering:

var resultsPromise = when.reduce(
    [task1, task2, task3],
    function(resultsSoFar, task /*, i, total */) {
        resultsSoFar.push(task());
        return resultsSoFar;
    },
    [] // intial value of resultsSoFar
);

when(resultsPromise, function(results) {
    console.log(results);
});

If you don't need guaranteed task ordering, have a look at when.map()

@jswartwood
Copy link
Author

The unanswered question is, why does "last done: 1" appear before "ok: 0".

Is really my main issue. The "all" happening before or after the "ok" is not as much of an issue (as long as it is relatively consistent). But I can't have an initializing method for the second task run before the cleanup/done method for the previous task.

Also, I'm not sure that I really expressed the scenario well enough leading into this discussion. An application may make a call to a queue method:

queue: function( andOne, runNow ) {
    var useAnd = typeof andOne === "function"
        , andDefer = when.defer()
        , allPromises = [ lastPromise, andDefer ]
    ;

    if (useAnd && runNow) {
        andOne(andDefer);
    } else {
        when(lastPromise).then(function() {
            if (useAnd) {
                andOne(andDefer);
            } else {
                andDefer.resolve(andOne);
            }
        });
    }

    lastPromise = when.all(allPromises);

    return andDefer;
}

To be used like:

andOne
    .queue(function( one ) {
        // Execute some setup stuff
        console.log("start testing...");
        // Start something async
        setTimeout(function() {
            one.resolve("test");
        }, 4000);
    })
    .then(function() {
        // This happens when done
        console.log("test done");
    })

These queue calls can be called in the same code-pass (like the loop above) or later like starting the second loop.

I will look into a rewrite with the reduce, but I'm not sure it will be as applicable since I don't really have list of promises.

Fyi a slightly more complete semi-real-world use-case example: https://gist.github.com/1572043
Also posting a link to my "kick" solve (since that may wind up being my permanent fix): http://jsfiddle.net/jswartwood/pCLrg/5/

@briancavalier
Copy link

Ah, ok, thanks for the extra example ... now I understand (I hope!) what you're trying to do. I think it can be simplified by using when() and taking advantage of Promises/A results forwarding. I've found that once I got my brain around those two things, I very rarely need to actually create a deferred with when.defer(). In my experience, it simplifies the code a lot when you simply return values or promises (gotten from calling some other function, possibly) back up the stack instead of creating deferreds and passing them down through the stack.

Here's a working example of the kind of queueing I think you're getting at. It relies on the when() + return up the stack model. Try it out in node and let me know if this does what you need. If not, let's definitely discuss more ... this is a pretty interesting case!

var when = require('./when');

var lastPromise;
function queue(andOne, runNow) {

    var execute = typeof andOne === 'function'
        ? andOne
        : function() { return andOne; };

    lastPromise = runNow
        ? execute()
        : when(lastPromise, execute);

    return lastPromise;
}

// ...
// Use it

function functionToQueue() {
    console.log('start testing...');

    // I'm not sure what your setTimeout here is for, but typically, you just
    // want to return something useful here that will be consumed by the next
    // callback in the chain
    // But, in order to *simulate* something async here, i.e. to delay any subsequent
    // callbacks in the chain, we'll just do:
    var d = when.defer();
    setTimeout(function () {
        d.resolve('test');
        // 'test' will be forwarded to the next callback in this promise's chain
        // It may not be obvious what "this promise" is in this structure, tho
    }, 4000);

    // If you don't care about the 'test' value getting forwarded, you can do:
    //setTimeout(d.resolve, 4000);

    // Very important to return the promise here!
    return d.promise;
}

// Kick off the first test
when(queue(functionToQueue),
    function() {
        // This will be called after the setTimeout
        console.log('test 1 done');
    }
);

// Immediately kick off the 2nd test, which should wait until test 1 is done before starting
when(queue(functionToQueue),
    function() {
        // This will be called after the setTimeout
        console.log('test 2 done');
    }
);

@briancavalier
Copy link

I'm betting you know this already, but these two are essentially equivalent:

when(something).then(doCoolStuff);

// is equivalent to

when(something, doCoolStuff);

The second one will create 1 fewer promise, and so be (negligibly) faster, and will probably compress slightly better. Otherwise, you won't notice a difference.

@jswartwood
Copy link
Author

Nice. Yes, that is more akin to what I was looking for, although the API for the module got a little twisted in all of that. I've just done a new push to https://github.com/jswartwood/and1 to finish out my first-draft prototype. Now, I just need a little simplification (if possible); I'll be looking to merge in aspects of your Promises/A example above to clean out some of those pesky defers (I even ended up adding another in jswartwood/and1@13b3cb7). I'm going to throw some doc in the code when I get home to explain my intentions for the usage.

Also, I'm thinking that any further queue conversation should probably move to jswartwood/and1#1; it has become less about the initial hiccup (of misordered "last done: 1" vs "ok: 0") and more about the actual queue implementation.

@briancavalier
Copy link

Cool. Feel free to @ me on that issue if you have any more when.js questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment