(picture)

July 16, 2003

JavaScript, asynchrony and closures

A couple of my GWS applications are driven by a fairly large chunk of JavaScript. There's some DHTML, then a slew of objects modeling Groove services, and they all call down to a SOAP helper, which uses XMLHTTP to talk to the Groove process. It's neat, and looks great, and I think it'll be maintenance-light.

Rather, I thought it would be maintenance-light. But one itch just had to be scratched: responsiveness. If Groove is very busy, or is prompting for password at the time, the HTTP call blocks. Which in this case means that my non-Groove application stops responding, too. And the startup performance isn't great, because I kick in when the application loads, and my GWS calls pretty much block the user interface until they're done.

To which there's only one solution: make the HTTP calls asynchronous. Here's where I'm really glad the stack is built in JavaScript: the result is nearly zero refactoring, because I can use closures. Gory details follow.

The bottom of the stack looks like this (Some, but not much, pseudocode mixed in here to keep it readable):


function SOAPCall(sReq,sURL,pObject)
{
var HTTP = CreateObject("Msxml2.XMLHTTP.5.0");
HTTP.open( sReq, sURL, false /* synchronous */ );
HTTP.setRequestHeader( "SOAPAction", theAction );
// ... build the message, then HTTP.send()
return parsed( HTTP.responseXML );
}

So, I build a SOAP message and punt it down the wire; the return message is some XML, which gets passed to a parser and a factory where the result (an object or an enumeration) is built and returned to the caller. Errors throw.

The caller has a nice high-level approach, for example:


var mydiv = // something local
var f = new g.GrooveFileDescriptor();
f.SetName( sFileName );
try
{
// this does a SOAP call
pStream = pFilesToolData.ReadFile( f );
// then use the stream (here, the contents of a text file)
mydiv.innerText = pStream.ReadText();
pStream.Close();
}
catch( _ex )
{
alert( _ex.description );
}

And there's a middle layer, where the GrooveFileDescriptor, GrooveFilesToolData and my other classes live.

Let's assume that we can make the HTTP calls asynchronously. My code will fire off a Web request, carry on running, and later be interrupted. But that SOAPCall() function can't have a meaningful return value anymore, because the request won't be done. Everything gets split into fragments. At the lowest level we have something like:


function StartSOAPCall(sReq,sURL,pObject)
{
var HTTP = CreateObject("Msxml2.XMLHTTP.5.0");
HTTP.open( sReq, sURL, true /* asynchronous */ );
HTTP.setRequestHeader( "SOAPAction", theAction );
// ... build the message, then HTTP.send()
}
function FinishSOAPCall()
{
// But how did we get here?
return parsed( HTTP.responseXML );
// And where does that 'return to'? The original caller is long gone...
}

and at the top, the code seems likely to become

var f = new g.GrooveFileDescriptor();
f.SetName( sFileName );
// kick off a SOAP call
pFilesToolData.ReadFile( f, CallbackFunction );
return;
...
function CallbackFunction( pStream /* and what else?? mydiv?? */ )
{
// use the stream (here, the contents of a text file)
mydiv.innerText = pStream.ReadText();
pStream.Close();
}
What other parameters might my callback need? Plenty, as it turns out (local variables, not just "this"). And what about error handling - where will errors be thrown to?

There's a simple switch on the HTTP object which makes it asynchronous: HTTP.open( sReq, sURL, true /* asynchronous */ );. Then you fire and forget; some while later, a COM event fires ("onreadystatechange"), and if readystate is "4", we have data. The "onreadystatechange" handler is can be set easily enough, but it doesn't get any context. Not even a pointer to the HTTP object which fired the event! How are we supposed to know what fired, to even find its readystate, let alone what we should do with it?

A singleton caller? - too clunky. Global variables? - please no! Some sort of timer? Aaaargh!

Closures (basically, anonymous inner functions, which retain the surrounding context) really help fix this. Additionally, I made the synchronous/asynchronous choice completely optional, by just adding an optional callback parameter to the generic SOAP call. Here's some of the reworked code (actually, this is nearly all of the rework right here).

The only wrinkle I'm eliding below is error handling. Instead of throwing, I construct an error object, and pass it back; below we can see the caller check for one of these.


function SOAPCall(sReq,sURL,pObject,fnCallback)
{
var HTTP = CreateObject("Msxml2.XMLHTTP.5.0");
if( fnCallback )
{
HTTP.open( sReq, sURL, true /* asynchronous */ );
HTTP.onreadystatechange = function()
{
// We're in an anonymous function, which will be called sometime
// by some COM thing, but we still know what "HTTP" means
// because it's in the scope in which this function is declared
// - we're in a closure
if( HTTP.readyState!=4 ) return;
fnCallback( parsed( HTTP.responseXML ) );
};
}
else
{
HTTP.open( sReq, sURL, false /* synchronous */ );
}
HTTP.setRequestHeader( "SOAPAction", theAction );
// ... build the message, then HTTP.send()
if( fnCallback )
{
// No need to return anything, the call is happening in background
}
else
{
return parsed( HTTP.responseXML );
}
}

And the caller...

var mydiv = // something local
var f = new g.GrooveFileDescriptor();
f.SetName( sFileName );
pFilesToolData.ReadFile( f, function( pStream )
{
if( IsError( pStream ) )
{
// That's not a stream, it's an Error...
}
// use the stream (here, the contents of a text file)
mydiv.innerText = pStream.ReadText();
pStream.Close();
} );

The callback is declared inline -- literally, as the second parameter to the .ReadFile call -- and it's a closure.

---

For comparison...

  • Using the Mozilla SOAP API shows pretty much this exact thing (If only I'd seen it beforehand!). They don't talk about the way JavaScript closures make this really powerful, though.
  • Remote Scripting (which I hadn't heard of before) can call server functions asynchronously from a Web page; this uses a helper object which gives you wait() and cancel() methods.
  • The corresponding C# technique (described in Using Delegates Asynchronously by Richard Blewett) is very general, but looks like slightly more work.