Data Push Apps with HTML5 SSE (2014)
Appendix B. Refactor: JavaScript Globals, Objects, and Closures
You know globals are wrong, don’t you? The prim and proper computer science types tell us that. But they just make life so much easier! No messing about passing long parameter lists (or refactoring long parameter lists into a single object parameter, which then doubles the length of the body code to use). No worrying about scope: they are just there (well, in JavaScript and many languages they are; in PHP you have to either use the globals keyword to declare which globals to use, or use the $_GLOBALS[] superglobal). When you need to modify them, no worrying about having to return values or reference parameters. So what were the good reasons for not using globals? Testing. Yawn. Encapsulation. Side effects. Blah, blah, blah.
But, in the context of data push applications, there is one situation where globals are going to trip us up: when you need to make two or more connections.
NOTE
This appendix just talks about refactoring the JavaScript to not use globals. It is an appendix because it shows general-purpose JavaScript techniques: there is nothing specifically about data push here (except the example code, of course). Basically, it is an appendix because it got a bit too big for a sidebar in the main text!
Introducing the Example
I will use a stripped-down SSE example. This code won’t have interesting data, and it won’t have the fallback code for the older browsers. None of that affects the decision of which approach is better, it just adds more lines of code.
First, the backend:
<?php
header("Content-Type: text/event-stream");
function sendData($data){
echo "data:";
echo json_encode($data)."\n";
echo "\n"; //Extra blank line
@flush();@ob_flush();
}
//--------------------------------------
while(true){
switch(rand(1,10)){
case 1:
sendData( array("comeBackIn10s" => true) );
exit;
case 2:
sendData( array("msg" => "About to sleep 10s") );
sleep(10); //Force a keep-alive timeout
break;
default:
sendData( array("t" => date("Y-m-d H:i:s")) );
sleep(1);
break;
}
}
The while(true)switch(rand(1,10)){...} idiom means loop forever and choose what to do on each loop randomly. Eighty percent of the time it will end up in the default: clause, and just send back a datestamp. You’ve seen code like this back in the very first examples inChapter 2, so I won’t explain it or the sendData() function again.
Of more interest is that 10 percent of the time (case 2:) it will go to sleep for 10 seconds. This is to simulate a dead connection: 10 seconds is enough because I will be setting the keep-alive timeout in the JavaScript to just 5 seconds. I also send back a message so we can see when this happens.
And what about case 1:? This sends back a special flag, and then dies. This represents the scheduled shutdown idea that we look at in Adding Scheduled Shutdowns/Reconnects. As the name of the flag suggests, we want the client to leave us alone for 10 seconds, then reconnect.
How does the frontend look? Like this:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>SSE: Basic With Sleep: Globals</title>
</head>
<body>
<pre id="x">Initializing...</pre>
<script>
var url = "basic_with_sleep.php";
var es = null;
var keepaliveSecs = 5;
var keepaliveTimer = null;
function gotActivity(){
if(keepaliveTimer != null)
clearTimeout(keepaliveTimer);
keepaliveTimer = setTimeout(
connect, keepaliveSecs * 1000);
}
function connect(){
document.getElementById("x").
innerHTML += "\nIn connect";
if(es)es.close();
gotActivity();
es = new EventSource(url);
es.addEventListener("message",
function(e){processOneLine(e.data);},
false);
}
function processOneLine(s){
gotActivity();
document.getElementById("x").
innerHTML += "\n" + s;
var d = JSON.parse(s);
if(d.comeBackIn10s){
if(keepaliveTimer != null)
clearTimeout(keepaliveTimer);
if(es)es.close();
setTimeout(connect,10*1000);
}
}
setTimeout(connect,100);
</script>
</body>
</html>
If you have read Chapter 5, you will recognize that the keepaliveSecs and keepaliveTimer globals and the gotActivity() function are working together to make sure the connection is always up. The connect() function does the job of both connect() andstartEventSource() in most of the code examples in this book; this is just a simplification because there is no fallback handling. processOneLine() just outputs the raw JSON it is receiving. The second half of processOneLine() is where the comeBackIn10s message is handled (this is the inline equivalent of the temporarilyDisconnect() function introduced in Chapter 5).
If you are reading this before reading Chapters 3, 4, and 5, and it is causing your brow to crinkle, just relax about exactly what the code is doing. The important things that I want to point out here are:
§ There are four globals (one is a parameter, the other three are worker variables).
§ Each of the four functions uses at least a couple of those globals.
§ connect() is being called from three different places:
§ The initial global code call (actually after 100ms)
§ After a keep-alive timeout
§ After a request to come back in 10 seconds
§ When connect() is called, it closes the old connection before starting a new one. This is the sole reason that es (the EventSource object) is captured into a global.
Load basic_with_sleep.html into your browser and try it out. It will look something like this:
Initializing...
In connect
{"t":"2014-02-28 09:46:34"}
{"t":"2014-02-28 09:46:35"}
{"t":"2014-02-28 09:46:36"}
{"msg":"About to sleep 10s"}
In connect
{"t":"2014-02-28 09:46:42"}
{"comeBackIn10s":true}
In connect
{"t":"2014-02-28 09:46:53"}
{"t":"2014-02-28 09:46:54"}
.
.
.
When it goes to sleep we get no fresh data, so after 5 seconds the keep-alive timer kicks in, closes the old connection, and starts a new one. So you see a 6-second gap. When it says come back in 10 seconds, we shut down the connection, switch off the keep-alive timer, and politely obey with a 10-second nap, so there is an 11-second gap in the timestamps.
The Problem Is…
…two connections. Find basic_with_sleep.two.html in the book’s source code and try it out. I’m not showing the code here because it is too gruesome. I now have seven globals, and eight global functions. Making this code required a lot of concentration and still I got it wrong, and had to debug my typos in Firebug. OK, game, set, and match to the computer scientists. You were right. Globals are Bad.
NOTE
To be fair, the code in basic_with_sleep.two.html was a really crude, naive approach. A couple of helper functions that take parameters could made it look not so bad. (Like a wig and a Jenna-Louise Coleman mask on Frankenstein. But as you lean in for the kiss you realize something does not smell quite right…)
So, something has to be done. I am going to look at a couple of solutions that JavaScript offers, and compare them.
JavaScript Objects and Constructors
JavaScript is an object-oriented language. Or maybe it isn’t and just pretends to be. If you think arguing points like this is a Fun Thing To Do, do it on your own time, not now, not here. The example code looks a bit like an object, so how about making it into a JavaScript object? The four globals could be the member variables. The four functions would be the member functions.
One great resource on this subject is Secrets of the JavaScript Ninja by John Resig and Bear Bibeault (Manning, 2012).
Let’s have a quick recap on JavaScript objects. But before that, a quick recap on JavaScript functions, and especially the this variable. Functions in JavaScript are first-class objects, which means they can be passed around, it is easy to define callbacks, and they can even have properties assigned on them. You already know you can pass parameters to functions. But there are also a couple of implicit parameters being passed. One is arguments, which is useful for variable-length parameter lists. The other is this. It is called the function context, and this is always set, even when the function is not part of a class. When a function is called normally, this is the global scope (equivalent to window in a browser). When a function is called as a method on an object, this is referring to the object. When a function is an DOM event handler, this will be the DOM object in question.
A function can also be invoked using the new keyword. You do this when the function is a constructor, but in JavaScript the constructor is also the equivalent of the class keyword in other languages: it doesn’t just do initialization tasks, it also describes what is in the object. So, inside a constructor, this refers to the newly created object. Here is an example:
function MyClass(constructorParam){
var privateVariable = "hello";
this.publicVariable = "world";
var privateFunction = function(a,b){
console.log(a + " " + b + constructorParam);
};
this.publicFunction = function(){
privateFunction(
privateVariable,
this.publicVariable
);
};
}
And then you use it like this:
var x = new MyClass("!");
x.publicFunction();
(This will output “hello world!” to the console.)
Notice how both constructorParam and privateVariable act like globals, but they are only visible within the public and private member functions of MyClass. Perfect.
The Code with Objects
So, to make an object we just need to wrap all the code in a constructor function, then put this. in front of everything? Here is what it looks like:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>SSE: Basic With Sleep: OOP (doesn't work)</title>
</head>
<body>
<pre id="x">Initializing...</pre>
<script>
function SSE(url,domId){
this.es = null;
this.keepaliveSecs = 5;
this.keepaliveTimer = null;
this.gotActivity = function(){
if(this.keepaliveTimer != null)
clearTimeout(this.keepaliveTimer);
this.keepaliveTimer = setTimeout(
this.connect, this.keepaliveSecs * 1000);
};
this.connect = function(){
document.getElementById(domId).
innerHTML += "\nIn connect";
if(this.es)this.es.close();
this.es = new EventSource(url);
this.es.addEventListener('message',
function(e){this.processOneLine(e.data);},
false);
this.gotActivity();
};
this.processOneLine = function(s){
this.gotActivity();
document.getElementById(domId).
innerHTML += "\n" + s;
var d = JSON.parse(s);
if(d.comeBackIn10s){
if(this.keepaliveTimer != null)
clearTimeout(this.keepaliveTimer);
if(this.es)this.es.close();
setTimeout(this.connect,10*1000);
}
};
this.connect();
}
setTimeout(function(){new SSE("basic_with_sleep.php", "x");},100);
</script>
</body>
</html>
Save it as basic_with_sleep.oop1.html and try it in the browser. Hhhmmmm…nothing happens. Firebug tells me the error is “TypeError: this.processOneLine is not a function.” Oh, yes it is. Whatever does the browser think this.processOneLine = function(s){...} means?! It cannot be any more function-like than that. Must be a browser bug.
No. The problem is that this means something different at that line. It is the “message” event handler of the EventSource object. So in that event handler this is referring not to our object, but to the EventSource object.
Maybe we could do something clever by moving processOneLine on to es. Then it will be found. But then all the references to this in processOneLine will not work. No, this is the wrong tree to be barking up. There is an easier way. At the top of the constructor, make a reference tothis in a private variable called self:
function SSE(url,domId){
var self = this;
...
The only other change that is needed is to change this. to self. in the “message” event handler. Nowhere else, just there.
NOTE
In fact, you could change all references to this to self in the whole class. You could even argue it is neater and tidier and therefore better.
this.es.addEventListener('message',
function(e){self.processOneLine(e.data);},
false);
basic_with_sleep.oop2.html does this, and if you try it you will see that this simple change got it working. Yeah! Object-oriented JavaScript to save the day. Computer scientists take a bow and then write a recursive function to pat each other on the back.
But I’m not done. Aren’t you curious why the self trick worked? Aren’t you curious why url and domId could be seen inside all our functions without our having to pass them around explicitly?
JavaScript Closures
The reason this works is closures. You can get a lot done with JavaScript without understanding closures, but understanding them gives you so much more power. Basically, closures mean that each time you create a function, it is given references to all the variables that were in scope at the time. I’m not going to go into any more detail; see Secrets of the JavaScript Ninja by John Resig and Bear Bibeault (Manning, 2012) for an in-depth explanation.
What it means for us is that when we define a variable using var in the constructor, it will be available automatically in every function we then go on to define. And, as the self example from the previous section shows, they will also be available inside functions we define inside the functions we define.[43]
It turns out we dived in, like a bull in a china shop, slapping this. in front of everything, when there was an easier way. Let’s go back to the original code, with its four global variables and four global functions. url is the parameter so remove that, but just in front of the other three globals add the constructor definition, and at the end close the constructor, and call connect():
function SSE(url){
var es = null;
var keepaliveSecs = 5;
var keepaliveTimer = null;
.
. (functions, untouched)
.
connect();
}
Get things started by creating an instance:
setTimeout(function(){
new SSE("basic_with_sleep.php");
}, 100);
If you try this out in your browser…it simply works. (See basic_with_sleep.oop3.html in the book’s source code.) All that prefixing this. on either the member variables or the functions was not needed. The self alias was not needed.
The takeaway lesson: when you have a set of global variables, and a set of functions that operate on them, and only a single entry point from outside of those functions, wrap the whole lot in a constructor function, call the entry point from the end of the constructor, and you’re done. (If you have other access points from the outside, go ahead and add public functions, using this.XXX = function(){...}, just for them.)
Tea for Two, and Two for Tea
To use the new constructor to run two connections, and have them update side by side, there are just a few quick changes. Add a separate DOM entry (id="y") for them. Add a domId parameter to the constructor. And, finally, instantiate a second object (our code here uses a second timeout that starts a couple of seconds after the first one).
The full code (basic_with_sleep.oop3.two.html) is shown here:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>SSE: Basic With Sleep: Simple OOP and Two Instances</title>
<style>
pre {float:left;margin:10px;}
</style>
</head>
<body>
<pre id="x">Initializing X...</pre>
<pre id="y">Initializing Y...</pre>
<script>
function SSE(url,domId){
var es = null;
var keepaliveSecs = 5;
var keepaliveTimer = null;
function gotActivity(){
if(keepaliveTimer != null)
clearTimeout(keepaliveTimer);
keepaliveTimer = setTimeout(
connect, keepaliveSecs * 1000);
}
function connect(){
document.getElementById(domId).
innerHTML += "\nIn connect";
if(es)es.close();
gotActivity();
es = new EventSource(url);
es.addEventListener("message",
function(e){processOneLine(e.data);},
false);
}
function processOneLine(s){
gotActivity();
document.getElementById(domId).
innerHTML += "\n" + s;
var d = JSON.parse(s);
if(d.comeBackIn10s){
if(keepaliveTimer != null)
clearTimeout(keepaliveTimer);
if(es)es.close();
setTimeout(connect,10*1000);
}
}
connect();
}
setTimeout(function(){
new SSE("basic_with_sleep.php","x");
}, 100);
setTimeout(function(){
new SSE("basic_with_sleep.php","y");
}, 2000);
</script>
</body>
</html>
NOTE
Bear in mind that modern browsers generally allow a limit of six connections to any single domain. (And those six have to include requests for images, etc., as well as Ajax requests.) So if you try adding lots of SSE objects to the preceding test page, you will only see the first six get any updates.
But, also, don’t do this. Wherever possible use one SSE connection to get all the messages. Use a JSON field to identify each type of message if they are meant for different parts of your application.
There are, however, no restrictions on simultaneous connections to different servers. That is when the code in this appendix becomes useful.
[43] Appreciating that all this stuff that is being passed around is weighing down your scripts is another reason to understand closures! The Function constructor, or the use of a function factory, are two ways to avoid closures.