Saturday, 25 July 2015

JavaScript: expectations re-adjusted re Promises

G'day:
First off a proactive apology. I generally sit in the pub on my Saturday afternoons in Galway (every second weekend), and write some ballocks on this blog. I think we're all used to that. I'm doing that now, but prior to kicking off with the writing I have already been sitting around for two hours watching the All Blacks play the Springboks in a hard-fought, narrow-margined 20-27 victory to NZ. And this entailed drinking four pints of Guinness as the build-up and match was under way, and I'm tucking into my fifth now. So f*** knows how coherent this article will be. The up-side is that I wrote the code for this beforehand :-S

OK, so the framing for this article is the previous article: "JavaScript: getting my brain around Promises", and my admission here is that I didn't actually get my brain entirely around JavaScript promises. I set my bar slightly too high for how I'd expect JavaScript to work, as it turns out. I'm spoilt by CFML, I guess. I'll update key points of that article, but make sure to read that one first as it's still mostly all right (IMO), and this article just clarifies a coupla points I didn't quite "get" last time.

My chief misapprehension was that JavaScript Promises themselves imparted some manner of asynchronicity to code execution, but this is not the case. They are just a handling mechanism for other code which itself is asynchronous. Once I wrote some code that respected that, things made more sense to me.

Here are some examples of how Promises help streamlining async code.


Single Promise

First up I've got a file with some helper functions in it:

// ajaxTestsSharedCode.js

var startTime = new Date();
var lap = function(){
    var elapsed = new Date() - startTime;
    return " (" + elapsed + "ms)";
}


var getAsyncResponse = function(targetUrl, ident){
    ident = ident || 0;

    return new Promise(function(resolve, reject){
        console.log("    (" + ident + ") Beginning of Promise" + lap());

        var xhr = new XMLHttpRequest();
        xhr.onload = function(){
            console.log("    (" + ident + ") Response received" + lap());
            resolve(this.responseText);
        };
        xhr.open("get", targetUrl, true);
        xhr.send();

        console.log("    (" + ident + ") Request sent" + lap());
    });

};


  • lap() just helps me show how long each segment of code takes;
  • and getAsyncResponse() actually provides the async code (via an AJAX request & handling thereof), an the Promissory code (which I then call in various tests).

getAsyncResponse() stands some scrutiny. It returns a Promise, and the Promise is based around the fulfilment of an AJAX request using the standard XHR class. To drive the situation home, AJAX requests are intrinsically asynchronous, so we need to give the XHR call an onload event handler (onload fires when the XHR request receives a response to the request). The onload handler resolves the Promise.

So the sequence of events when calling getAsyncResponse() is:
  1. create a Promise;
  2. its executor makes an XHR request;
  3. the XHR object is given an onload handler;
  4. and the promise is returned to the calling code.
  5. When the XHR gets a response, the onload handler resolves the Promise when its called.

Note that none of this code actually provides a handler for the resolve() call in the onload handler.

Here's the first test:

// ajaxReturnsPromise.js

console.log("Before initiating request" + lap());
request = getAsyncResponse("../slow.cfm", 1);

console.log("Before binding resolver" + lap());
request.then(function(value){
    console.log("        Response handled: " + value + lap());
});

console.log("End of mainline code" + lap());

This uses the getAsyncResponse() function to hit slow.cfm (see below), and then binds a resolution handler to the returned Promise to simple output the value returned from slow.cfm once it arrives. slow.cfm is as follows:

<cfset sleep(5000)>OK

(to any non-CFMLers reading: that pauses the current thread for 5sec... it just gives an obvious delay when looking at the output of the JavaScript code).

The results of all this are satisfying:

Before initiating request (1ms)
    (1) Beginning of Promise (8ms)
    (1) Request sent (9ms)
Before binding resolver (9ms)
End of mainline code (10ms)
    (1) Response received (5020ms)
        Response handled: OK (5022ms)

Just to make clear what's going on... all this Promise nonsense allows a function to return to the calling code a value (a Promise) even if the function still has work to do. Obviously the calling code can't dive in to use the value straight away, as the async process might still be going on. But what it can do is bind an event handler to the resolve (ie: "I now have a value") event, and doing something with that value. So it kinda allows one to write linear code which deals with non-linear code execution.

It's important to note that the value returned from the async process can never be directly dealt with from the mainline code; but the mainline code can define what code gets run when the value is actually available. And - as shown in my previous article - the calling code can continue to chain then() calls, and it will all just work.

I'm artificially adding a 5sec delay here, but it doesn't matter how long the delay is; there might not even be a delay. The good thing about Promises is that one can bind resolution (and rejection) handlers to the Promise even after it has either been fulfilled or rejected. All that means is that the handlers bound by subsequent then() calls are executed immediately. Using then() to provided the code pretty much means one can ignore the fact that code is running asynchronously.

Quite cool.

Promise.all() and Promise.race()


Now that I have my example Promise-creating function, I can also demonstrate Promise.all() and Promise.race() better.

Promise.all()

Here's an example with all():

// allAjaxRequests.js

console.log("Before creating array" + lap());
var promises = [1,2,3,4,5].map(function(i){
    return getAsyncResponse("../responder.cfm?ident=" + i, i);
});
console.log("After creating array" + lap());


console.log("Before calling all()" + lap());
var all = Promise.all(promises);
console.log("After calling all()" + lap());


console.log("Before binding resolver" + lap());
all.then(function(value){
    var indent = "        ";
    console.log(indent + "all() handled" + lap());
    var values = value.forEach(function(v, i){
        console.log(indent + "(" + i + ")" + v);
    });
});
console.log("After binding resolver" + lap());


console.log("End of mainline code" + lap());

Here's the biz:

  • I create an array of Promises with my afore-listed getAsyncResponse() function;
  • I give that array to Promise.all()
  • I bind a resolution handler to the Promise returned by the Promise.all() call

Oh... In this example I'm calling responder.cfm:

param URL.ident="unknown";
pauseFor = 1000 + (randRange(0, 20) * 100);
sleep(pauseFor);
writeOutput("Response after #pauseFor#ms from request with ident [#URL.ident#]");

This just hangs around for 1-3sec, then returns a message identifying the ident it was called with, and how long it delayed itself.

The output of this is quite pleasing, compared to my tests in the last article (it helps I'm now doing sensible tests!):

    (1) Beginning of Promise (7ms)
    (1) Request sent (8ms)
    (2) Beginning of Promise (10ms)
    (2) Request sent (10ms)
    (3) Beginning of Promise (11ms)
    (3) Request sent (12ms)
    (4) Beginning of Promise (14ms)
    (4) Request sent (15ms)
    (5) Beginning of Promise (17ms)
    (5) Request sent (18ms)
After creating array (18ms)
Before calling all() (18ms)
After calling all() (19ms)
Before binding resolver (20ms)
After binding resolver (20ms)
End of mainline code (21ms)
    (2) Response received (1749ms)
    (5) Response received (2139ms)
    (4) Response received (2233ms)
    (3) Response received (2642ms)
    (1) Response received (2931ms)
        all() handled (2932ms)
        (0)Response after 2900ms from request with ident [1]
        (1)Response after 1700ms from request with ident [2]
        (2)Response after 2600ms from request with ident [3]
        (3)Response after 2200ms from request with ident [4]
        (4)Response after 2100ms from request with ident [5]

The things to note here are:

  • the Promises are made;
  • and the resolution handlers are bound;
  • and the mainline code finishes;
  • but then the responses start coming back;
  • and once they're all resolved, then the all() resolution handler runs.
  • Best of all, the whole process only takes as long as the longest XHR request takes to fulfill. There's 11500ms worth of requests being run there, but they all run in parallel, and everything's fulfilled once the longest one (2900ms - coincidentally the first one) resolves.

Nice one.

race()

What about Promise.race()? As per the previous article, my race()-testing code is identical to the all()-testing code - except for calling race() instead of all() - so I won't repeat it. It's on GitHub anyhow: raceAjaxRequests.js. But here are the results;

Before creating array (1ms)
   (1) Beginning of Promise (8ms)
   (1) Request sent (9ms)
   (2) Beginning of Promise (9ms)
   (2) Request sent (9ms)
   (3) Beginning of Promise (10ms)
   (3) Request sent (10ms)
   (4) Beginning of Promise (10ms)
   (4) Request sent (11ms)
   (5) Beginning of Promise (11ms)
   (5) Request sent (12ms)
After creating array (12ms)
Before calling race() (13ms)
After calling race() (13ms)
Before binding resolver (14ms)
After binding resolver (14ms)
End of mainline code (14ms)
   (5) Response received (1235ms)
        race() handled (1237ms)
        Response after 1200ms from request with ident [5]
   (2) Response received (1337ms)
   (1) Response received (2230ms)
   (4) Response received (2339ms)
   (3) Response received (2543ms)

The random delay has worked in my favour here: the last request made happened to be the quickest one, so the first one to get back to its resolution hander and resolve. Which allowed the Promise.race() to also then resolve. So we see the race() resolving before the earlier four Promises resolve. This is what I was hoping to see yesterday.

Thanks

I would have been continuing to mislabour under my quizzical misapprehensions of how this all "worked" in JavaScript had it not been for the help of Ryan Guill, Jessica Kennedy, and my man Acker Apple putting their oars in via various channels (Ryan via DM, Jessica via the JavaScript sub-channel on the #cfml Slack channel, and Acker on comments on the last article). Thanks for the time you took to wade through my bullsh!t and help me out. This article is dedicated to you lot.

I have a PHP article to write, then I'm gonna have a look at the viability of this in CFML. I have the PHP code written, so I might get both of those articles out the door whilst I await my flight back to London tomorrow (I have 4h in the terminal, so plenty of typing time).

That wasn't so bad for a person finishing their fifth pint of Guinness, was it?

--
Adam