Fallbacks: There Has to Be a Better Way! - Data Push Apps with HTML5 SSE (2014)

Data Push Apps with HTML5 SSE (2014)

Chapter 7. Fallbacks: There Has to Be a Better Way!

In the previous chapter we looked at long-poll as a way to push data from server to clients that do not support SSE. Its advantage over SSE is that it works just about everywhere, and its disadvantage is that the slightly higher latency and slightly higher bandwidth use can become significant for high-frequency updates. In this chapter we will look at two alternatives that are almost as good as native SSE, from the latency and bandwidth point of view.

The first fallback we look at uses Ajax, just as long-poll did, but using readyState == 3, instead of readyState == 4. In a nutshell it means we get each piece of data as the server pushes it out, while the connection is still alive, in contrast to long-poll where we don’t see any of the data until the server closes the connection. (If you skipped over the sidebar Ajax readyState, this might be a fine time to go back and review what the Ajax readyState values mean.)

This is a nice approach, only slightly less efficient than SSE, so it is ironic that it gives us hardly any more desktop browser coverage.[29] Why? Because most of the browsers where it works already have native SSE support! However, this technique does work in Android 4.x (representing about two-thirds of Android users at the time of writing).

The second fallback is specifically for Internet Explorer 8 and above. There is nothing particularly IE-specific in the technique, so it is strange that it either does not work, or only works with some hacks in each of Firefox, Chrome, Safari, and Opera. But we already have native SSE for those browsers, so who cares? The key thing about this technique is that adding IE8/IE9/IE10 support gives us 28% more browser coverage.[30]

Commonalities

As in Chapter 6, I will introduce the techniques as a minimal example first, before grafting them onto the FX demo. I will use the same backend for both techniques (XHR and iframe) that are being introduced in this chapter. See abc_stream.php in the book’s source code, which looks like this:

<?php

header("Content-Type: text/plain");

if(array_key_exists("HTTP_USER_AGENT",$_SERVER)

&& strpos($_SERVER["HTTP_USER_AGENT"],"Chrome/") !== false)

echo str_repeat(" ",1023)."\n";

@ob_flush();@flush();

$ch = "A";

while(true){

echo json_encode($ch.$ch)."\n";

@ob_flush();@flush();

if($ch == "Z")break;

++$ch;

sleep(1);

}

?>

We output the MIME type as text/plain. Note that we cannot use the text/event-stream that we use with SSE because browsers that don’t support SSE don’t know it, so they ask the user if they want to save it as a file!

The next line outputs exactly 1,024 bytes of whitespace. It is only needed for the Chrome browser, so here I use a user-agent check (array_key_exists("HTTP_USER_AGENT",$_SERVER) asks if we have been told a user-agent, and strpos($string,$substring)! == false is PHP’s partial string-matching idiom, asking if $substring is found anywhere in $string).

After that, the rest of the code is easy: we output 26 strings, each one second apart. After 26 seconds we close the connection (simply so you can see how the browsers react when this happens). Just like in all the SSE code we created in earlier chapters, the @ob_flush();@flush(); idiom is used to make sure the content is sent immediately and not buffered.

NOTE

SSE has worked since Chrome 6, and it is one of the browsers that automatically upgrades itself, so realistically no one is still using a version of Chrome that needs any of these fallback techniques. But you will need this 1,024-bytes-of-whitespace code if you want to follow along with this chapter using Chrome. I also wanted to show this because it is a useful troubleshooting technique: when something does not work in a particular browser, a bunch of whitespace can often work wonders.[31]

By the way, none of these buffering tricks could get any of the examples in this chapter to work with Opera 12! (But Opera has supported SSE since Opera 11.0, so we can live with that.)

On the frontend, one thing that both techniques introduced in this chapter have in common is that we don’t get a new message each time the backend sends a new message. Instead, we get a long string holding all messages since we connected. This string gets longer and longer as time goes on. This creates two challenges for us:

§ Extract just the new message(s).

§ Avoid excessive memory usage.

For the first of those, we use the following function, where s is the full data received so far, prevOffset is where we have read up to so far (0 on the first call), and callback is the function that will process one message. The function returns the new furthest point processed, and that is what you pass in as prevOffset on the next call. If there was no new data, the input value of prevOffset ends up getting returned:

function getNewText(s,prevOffset,callback){

if(!s)return prevOffset;

var lastLF = s.lastIndexOf("\n") + 1;

if(lastLF == 0 || prevOffset == lastLF)return prevOffset;

var lines = s.substring(prevOffset,lastLF - 1).split(/\n/);

for(var ix in lines)callback(lines[ix]);

return lastLF; //Starting point for next time

}

This also shows one other thing we have to be careful of (that SSE took care of for us, and was never an issue with long-polling): it is possible to get half a message. If you recall from Chapter 3, we decided on a protocol of one JSON message per line. If the server sends the message{"x":3,"y":4}\n, we will almost always receive {"x":3,"y":4}\n. But it is not guaranteed. We might get {"x":3,"y. Then a short while later our Ajax callback is called again and this time we get ":4}\n, so that s now equals {"x":3,"y":4}\n. Once we know this might happen, of course, dealing with it is easy: simply look for the last LF in the input string, and ignore anything after that for the moment. That is what the s.lastIndexOf("\n") piece of JavaScript does. (The +1 is because it returns the index of the \n, and next time we want to start from just afterthat character.)

By comparing prevOffset with lastLF, we find out if we have any new data (which implies we have at least one whole new line). s.substring(prevOffset,lastLF-1) extracts just the new data. Then .split(/\n/) breaks it apart into one array entry per line. Finally, we can call our callback once for each line found.

What about the memory overflow challenge? This involves simply watching the size of the string, and once it gets rather large, killing the connection and reconnecting. You can decide the definition of “rather large” on a case-by-case basis, but I tend to use 32,768 unless I have a good reason not to. (What would be a good reason? For instance, if I was sending large blocks of data, and 32KB might fill up with just two or three messages.) This is not shown in our simple implementations for XHR and iframe, but will be shown when we graft them onto the FX application later in this chapter.

XHR

If you have already studied the long-poll code, there is not that much new to actually say about this code. We prepare an XMLHttpRequest object (because this code won’t ever work with Internet Explorer 6, I don’t bother with the check for XMLHttpRequest not being available), connecting to abc_stream.php, and setting the onreadystatechange() function. We call send() with a 50ms delay, for the sake of Safari (just as in the long-poll code); if we don’t do this it all works, but it shows the “spinning circle” busy cursor all the time. We also add a custom variable to the xhr object, called prevOffset.

So, let’s take a closer look at the onreadystatechange function. It does two things. First, it creates a log of each time it is called, which it appends to <pre id="x"> (we use a <pre> so you can see the carriage returns). By the way, if we get called with no new content, it returns immediately. Then the last line of our onreadystatechange function uses getNewText(), which we developed earlier in this chapter. That will fill in <p id="latest"> with the most recent line received. The code is as follows:

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8" />

<title>Simple XHR Streaming Test</title>

<script>

function getNewText(s,prevOffset,callback){

if(!s)return prevOffset;

var lastLF = s.lastIndexOf("\n")+1;

if(lastLF == 0 || prevOffset == lastLF)return prevOffset;

var lines = s.substring(prevOffset, lastLF - 1).split(/\n/);

for(var ix in lines)callback(lines[ix]);

return lastLF; //Starting point for next time

}

function process(line){

document.getElementById("latest").innerHTML = line;

}

</script>

</head>

<body>

<p id="latest">Preparing...</p>

<hr/>

<pre id="x">Preparing...</pre>

<script>

var s="",s2prev="";

var xhr = new XMLHttpRequest();

xhr.prevOffset = 0;

xhr.open("GET", "abc_stream.php");

xhr.onreadystatechange = function(){

var s2 = this.readyState + ":" +

this.status + ":" + this.responseText;

if(s2 == s2prev)return;

s2prev = s2;

s += s2 + "<br/>\n";

document.getElementById("x").innerHTML = s;

this.prevOffset = getNewText(

this.responseText,this.prevOffset,process);

};

setTimeout(function(){xhr.send(null)}, 50);

</script>

</body>

</html>

So, put simple_xhr_test.html and abc_stream.php in the same directory, and open them in a supporting browser. You should see something like:

"CC"

1:0:

2:200:

3:200:"AA"

3:200:"AA"

"BB"

3:200:"AA"

"BB"

"CC"

After 26 seconds, you will see "ZZ" at the top of the screen. At the bottom, you will see these two sections:

3:200:"AA"

"BB"

"CC"

"DD"

...

"YY"

"ZZ"

4:200:"AA"

"BB"

"CC"

"DD"

...

"YY"

"ZZ"

You can see that you get all of "AA" to "ZZ" in readyState==3; then when the backend server shuts the connection, you get sent a readyState==4 signal, too.

This seems a good time to point out that opening abc_stream.php directly in most browsers has surprising "AA", then "AA" "BB". Instead it sits there doing nothing for 26 seconds, then suddenly shows all of "AA" to "ZZ". You only get to see the partially loaded data when using XMLHttpRequest. In fact, this is exactly why the iframe technique, which I’ll introduce next, does not really work in those browsers.

iframe

The XHR technique shown in the previous section does not work in Internet Explorer: the problem is that IE does not set the xhr.responseText variable until xhr.readyState is 4! The whole point of the XHR technique was that the xhr.readyState never reaches 4. So this is a fatal blow. But all is not lost. The trick we use with Internet Explorer is to load that data into a dynamically created <iframe>, and then we go and look at the source of that iframe! The first time I heard that idea, I was so impressed I jumped out of my chair and went to explain it to my cat. Yes, people deal with excitement in different ways. Cats too, as it turns out.

This fallback appears to work in most browsers, not just IE variants, but different browsers require a differing amount of data to be received from the server before they will make the data available. In IE6/7/8, only a few bytes need to be received before it starts working, and unless your messages are short, you should not need to worry about it.

But to follow along with this chapter in other browsers, you will need some extra hackery. In Chrome the requirement appears to be 1,024 bytes, just like it was in the XHR technique we looked at earlier. Our abc_stream.php script already sends out that much whitespace for Chrome. In Firefox it needs 2,048 bytes (so for a while I didn’t realize this technique even worked in Firefox), which wasn’t needed for the XHR technique. Add the highlighted line shown here to abc_stream.php to have it work immediately in Firefox too:

header("Content-Type: text/plain");

if(array_key_exists("HTTP_USER_AGENT", $_SERVER)

&& strpos($_SERVER["HTTP_USER_AGENT"], "Chrome/")!==false)

echo str_repeat(" ",1023)."\n";

if(array_key_exists("HTTP_USER_AGENT", $_SERVER)

&& strpos($_SERVER["HTTP_USER_AGENT"], "Firefox/")!==false)

echo str_repeat(" ",2047)."\n";

@ob_flush();@flush();

...

NOTE

Remember: I do not use the preceding code in the main FX application, because Chrome and Firefox will never need to use either the iframe or the XHR fallback techniques; they will always use native SSE. These hacks are just so you can follow along with this section without having to use Internet Explorer.

If you were paying attention, I casually mentioned that the data is available in IE6, IE7, and IE8. Hang on…didn’t I say earlier that this technique only works in Internet Explorer 8? The problem is that Internet Explorer 7 and earlier do not allow us to access the contents of a child iframe fromJavaScript. The test we can do to see if we can access an iframe’s contents is:

if(window.postMessage){ /* OK */ }

windows.postMessage returns true in Internet Explorer 8 and above, false in Internet Explorer 7 and earlier.

NOTE

The developer tools in IE10 and later have a compatibility mode that allows it to pretend it is IE9, IE8, or IE7. When it is pretending to be IE7, windows.postMessage returns true, meaning the iframe will appear to work in IE7. I believe this is a bug/limitation of IE10’s compatibility mode, nothing more.

The iframe technique is inferior to the XHR technique in one particular way: we have to poll. But this is a different type of polling from that introduced in Chapter 6, because we are not polling the server. Instead, we are polling for changes in an iframe. It is completely localized polling, and relatively quick and light. But it still adds a bit of latency. In other words, the new messages get pushed from server to client immediately, but it takes us a little while to discover and process the new message. In the example shown here, we use setInterval(...,500), which means we look for new messages every 500ms. So, the mean latency we add is 250ms. If we reduce the setInterval interval to 100ms, then the mean latency introduced is reduced to 50ms. The downside is more CPU use on the client for the extra polling. You need to balance the latency needs of your application against the desired CPU usage on the client. The code is as follows:

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8" />

<title>Simple IFrame-Streaming Test</title>

<script>

function getNewText(s,prevOffset,callback){

if(!s)return prevOffset;

var lastLF = s.lastIndexOf("\n")+1;

if(lastLF == 0 || prevOffset == lastLF)return prevOffset;

var lines = s.substring(prevOffset, lastLF - 1).split(/\n/);

for(var ix in lines)callback(lines[ix]);

return lastLF; //Starting point for next time

}

</script>

</head>

<body>

<p id="latest">Preparing...</p>

<hr/>

<pre id="x">Preparing...</pre>

<script>

function connectIframe(){

iframe = document.createElement("iframe");

iframe.setAttribute("style", "display: none;");

iframe.setAttribute("src", "abc_stream.php");

document.body.appendChild(iframe);

var prevOffset = 0;

setInterval(function(){

var s = iframe.contentWindow.document.body.innerHTML;

prevOffset = getNewText(s,prevOffset,function(line){

document.getElementById("latest").innerHTML = line;

});

document.getElementById("x").innerHTML = s;

}, 500);

}

if(window.postMessage){

document.getElementById("x").innerHTML = "OK";

setTimeout(connectIframe, 100);

}

else{

document.getElementById("x").innerHTML = "Sorry!";

}

</script>

</body>

</html>

Starting at the bottom of this code, we look for window.postMessage, and if it exists we call connectIframe(): there has to be a 100ms delay, otherwise we get an HTML parsing error when we try to create the iframe. In connectIframe, the first four lines create an <iframe>dynamically, with display:none CSS to make it invisible, and the src set to our streaming data source. Then we use setInterval to set up a regular timer, and every 500ms we fetch the contents of the iframe. Just as in the XHR demo in the previous section, we put all contents so far in the "x" element, and just the most recent message in the "latest" element.

Put simple_iframe_test.html in the same directory as abc_stream.php and open it in IE8 or above, and you should see the “latest” element changing every second.

Grafting XHR/Iframe onto Our FX Application

The steps to do this will be very similar to how we grafted long-poll onto the FX application in Chapter 6. There are some minor backend changes, and we add some frontend code that looks like the simple code introduced earlier in this chapter, as well as the feature-detection code to wire it up.

XHR on the Backend

Do you remember this code from Chapter 6?

$GLOBALS["is_longpoll"] = array_key_exists("longpoll",$_POST)

|| array_key_exists("longpoll",$_GET);

$GLOBALS["is_sse"] = !($GLOBALS["is_longpoll"]);

Our clients (both XHR and iframe) will identify themselves as using XHR, so modify it as follows:

$GLOBALS["is_longpoll"] = array_key_exists("longpoll",$_POST)

|| array_key_exists("longpoll",$_GET);

$GLOBALS["is_xhr"] = array_key_exists("xhr",$_POST)

|| array_key_exists("xhr",$_GET);

$GLOBALS["is_sse"] = !($GLOBALS["is_longpoll"] || $GLOBALS["is_xhr"]);

There is nothing else to do server side. The format of the data that is pushed is identical to that using long-poll. Basically xhr is given solely to set the correct MIME type (text/plain and not text/event-stream, because the latter will cause some browsers to prompt for the user to save it to an external file). (The file with the preceding addition is found in the book’s source code as fx_server.xhr.php).

XHR on the Frontend

Add the following block of code to the fx_client.longpoll.html file we had as of the end of the previous chapter:

function getNewText(s,prevOffset){

if(!s)return prevOffset;

var lastLF = s.lastIndexOf("\n") + 1;

if(lastLF == 0 || prevOffset == lastLF)return prevOffset;

var lines = s.substring(prevOffset, lastLF - 1).split(/\n/);

for(var ix in lines)processNonSSE(lines[ix]);

return lastLF; //Starting point for next time

}

function startXHR(){

if(xhr)xhr.abort();

xhr = new XMLHttpRequest();

xhr.prevOffset = 0;

xhr.onreadystatechange = function(){

this.prevOffset = getNewText(

this.responseText,this.prevOffset);

};

var u = url;

u += "xhr=1&t=" + (new Date().getTime());

xhr.open("GET", u);

if(last_id)xhr.setRequestHeader("Last-Event-ID", last_id)

xhr.send(null);

}

The getNewText function is as we saw earlier, but instead of taking a callback as a parameter we hardcode processNonSSE() as the callback. This is used for both the XHR and iframe techniques (and, you may remember, is also being used by long-poll). The startXHR() function is similar to the simple example we made earlier in the chapter, but ironically it is actually simpler: there is no messing around reporting the various xhr.readyState values; we use one line to process them all. When readyState is 0, 1, or 2, responseText is empty, so getNewText will do nothing (and return 0). Note that it copes with the case where xhr.responseText is null. xhr is a private variable, of the SSE object, that was defined in the previous chapter.

If the server closes the connection and readyState is 4, then there are two possible situations. Either there was no new data since the last call to onreadystatechange, or there was new data (perhaps we had previously received half a message, and were just waiting for the final few bytes and the LF). Either way, getNewText() does the right thing. It is the kind of function you can take home to meet the family, without having to worry about it embarrassing you.

Iframe on the Frontend

First add another private variable to our SSE object, next to where we define es and xhr:

var iframe = null;

NOTE

As mentioned in the previous chapter, es, xhr, and iframe are exclusive, meaning they could all be named server, or something, and share the same variable. I chose to use three distinct private variables in this book for code clarity.

Then add this function:

function startIframe(){

var u = url;

u += "xhr=1&t=" + (new Date().getTime());

iframe = document.createElement("iframe");

iframe.setAttribute("style", "display: none;");

iframe.setAttribute("src", u);

document.body.appendChild(iframe);

var prevOffset = 0;

setInterval(function(){

if(!iframe.contentWindow.document.body)return;

var s = iframe.contentWindow.document.body.innerHTML;

prevOffset=getNewText(s, prevOffset);

}, 500);

}

This is basically the same code we saw earlier in the chapter, but using the global URL, and with the logging code cut out. It needs a bit of enhancing to be production-ready, though. First pass the lastId variable (done in the URL, not as a header). The other changes, shown next, are to tidy up from a previous call when this function is called a second time (and you remember that happens when our keep-alive mechanism has had to kick in):

function startIframe(){

if(iframe)iframe.parentNode.removeChild(iframe);

if(iframeTimer)clearInterval(iframeTimer);

var u = url;

if(last_id)u += "last_id="

+ encodeURIComponent(last_id) + "&";

u += "xhr=1&t=" + (new Date().getTime());

iframe = document.createElement("iframe");

iframe.setAttribute("style", "display: none;");

iframe.setAttribute("src", u);

document.body.appendChild(iframe);

var prevOffset = 0;

iframeTimer = setInterval(function(){

var s = iframe.contentWindow.document.body.innerHTML;

prevOffset = getNewText(s, prevOffset);

}, 500);

}

This also needs one more private variable: var iframeTimer = null;.

Wiring Up XHR

The connect() function currently looks like:

function connect(){

gotActivity();

if(window.EventSource)startEventSource();

else startLongPoll();

}

Add this line:

function connect(){

gotActivity();

if(window.EventSource)startEventSource();

else if(window.XMLHttpRequest &&

typeof new XMLHttpRequest().responseType != "undefined")

startXHR();

else startLongPoll();

}

The browser detection is a little complicated. What we need for this to work is XMLHttpRequest2. The first part of the change to the function checks if XMLHttpRequest is defined. Just about every single browser will return true for this, as this is defined in the first version of XHR. When XHR got a bunch of new features the designers decided against calling the enhanced object XMLHttpRequest2, so it is still called XMLHttpRequest. Unfortunately they also decided against any kind of version number. There is also no object directly related to the XMLHttpRequest2 functionality we are using. Humbug. So, we are left with testing by coincidence: all browsers[32] that define a responseType element on their XMLHttpRequest objects also give us access to the responseText data in readyState==3.

NOTE

To force using XHR on browsers that support SSE, for testing purposes, put this at the top of connect():

if(true)startXHR();else

Wiring Up Iframe

If you thought the feature detection for XHR was complicated, you ain’t seen nothing yet. The feature detection for the iframe technique is in two parts. The first part goes at the top of the HTML file. We met Internet Explorer’s special macro language in the previous chapter. Here we use it to set a JavaScript global to true for IE9 and earlier, and false for everyone else. (We don’t use iframe for IE10 and later because the XHR technique works, which is lucky because the special IE macro language no longer does!) Near the top of the <head> part of the HTML file add the highlighted code (the other code shown here is what we already had in fx_client.longpoll.html):

<script>var isIE9OrEarlier = false;</script>

<!--[if lte IE 7]>

<script src="json2.min.js"></script>

<![endif]-->

<!--[if lte IE 9]>

<script>

isIE9OrEarlier = true;

</script>

<![endif]-->

<script>

Object.keys=Object.keys || function(o,k,r){

r = [];

for(k in o)if(o.hasOwnProperty(k))r.push(k);

return r;

}

</script>

Now with our new isIE9OrEarlier global in our grubby little hands, add the following lines to connect():

function connect(){

gotActivity();

if(window.EventSource)start_eventsource();

else if(isIE9OrEarlier){

if(window.postMessage)startIframe();

else startLongPoll();

}

else if(window.XMLHttpRequest &&

typeof new XMLHttpRequest().responseType != "undefined")

startXHR();

else startLongPoll();

}

In plain English: if IE9 and earlier, then either use iframe (i.e., IE8 and IE9, because only they have the window.postMessage function defined) or long-poll (i.e., for IE5.5, IE6, and IE7). If IE10 or IE11, then fall through and use XHR instead.

NOTE

For completeness I should tell you that to force testing of the iframe technique on browsers that support SSE, put this at the top of connect():

if(true)startIframe();else

One more change is to add a couple more clauses to the disconnect() function:

function disconnect(){

if(keepaliveTimer){

clearTimeout(keepaliveTimer);

keepaliveTimer = null;

}

if(es){

es.close();

es = null;

}

if(xhr){

xhr.abort();

xhr = null;

}

if(longPollTimer){

clearTimeout(longPollTimer);

longPollTimer = null;

}

if(iframeTimer){

clearTimeout(iframeTimer);

iframeTimer = null;

}

if(iframe){

iframe.parentNode.removeChild(iframe);

iframe = null;

}

}

Thanks for the Memories

What was it I forgot? I’m sure there was something. Sigh, my memory just gets worse…that was it! Memory management! Both the XHR and iframe approaches are storing all messages sent by the server; basically it is one big message under the surface. This wasn’t a problem with SSE because the EventSource object treats each message separately, and wasn’t a problem with long-poll because each message was a complete connection. If you run a script for long enough, it is going to be a problem for XHR and iframe, though: the buffer is going to keep getting larger and larger until it starts to drag down the client system.

The solution is as simple as it is crude: when the one big message gets too big, make a fresh connection. There are some downsides, and it is fair to say that the lack of this issue is the biggest advantage native SSE has over the XHR fallback. Before examining the downsides, let’s look at the code. It involves the addition of code to getNewText() (which is used by both XHR and iframe, but not used by native SSE and not used by long-poll), and nowhere else:

function getNewText(s,prevOffset){

var lastLF = s.lastIndexOf("\n")+1;

if(lastLF == 0 || prevOffset == lastLF)return prevOffset;

var lines = s.substring(prevOffset, lastLF - 1).split(/\n/);

for(var ix in lines)processNonSSE(lines[ix]);

if(lastLF > 65536){

console.log("Received " + lastLF +

" bytes (" + s.length + "). Will reconnect.");

disconnect();

setTimeout(connect, 1);

}

return lastLF; //Starting point for next time

}

In other words, once the buffer is over 64KB, disconnect, then connect again. The call to connect() is done on a 1ms timer just to avoid potential problems with recursive calls.

The first downside to point out is that the choice of 64KB is arbitrary. It takes about 2.5 minutes for the FX demo to fill it. If each message is bigger, or the messages come through more quickly, you might want to increase the buffer size. If all your users are on desktop machines, you could increase it by 10 times or more; even on a mobile device, 64KB is not that big.

The second downside is that any half-messages get lost—remember the discussion from earlier that led us to use s.lastIndexOf("\n"). These half-messages are going to be rare (hopefully), so you could change the condition to be if(lastLF > 65536 && lastLF == s.length), telling it to always wait for a clean point to break the connection. Just bear in mind that this means that theoretically it could never disconnect (causing a memory issue).

The third issue is the same problem we had with long-poll: we might miss a message during the time between the disconnect and the next connect. However, if we send the lastId received (as we do in the FX demo), then the second and third downsides become neutralized: we don’t end up missing anything, and all we have is a bit of inefficiency.

Putting the FX Baby to Bed

And so that finishes the FX application we have been developing over the past five chapters—99+% browser coverage, with the most efficient available technique we can find for each of those users (if you lost track of which users are using which technique, see Table 7-1 in the following sidebar), for a realistically complex data-push application, dealing with production issues like servers and sockets disappearing on us, scheduled shutdowns, and more.

In Chapter 9, the FX application will be revived when we look at authentication and other security-related issues. But before that, there are a few aspects of SSE that we have not used, and these will be covered in the next chapter.

WHO ENDS UP WHERE?

Table 7-1 summarizes how the browser detection works.

Table 7-1. Which start() function to use, based on user’s browser

Function

Browser

startEventSource()

§ Basically all Firefox and Chrome[a]

§ Desktop Safari 5.0+

§ iOS Safari 4.0+

§ Android 4.4+ (earlier where Chrome is default browser)

§ Chrome for Android (all versions)

§ Firefox for Android (all versions)

§ Opera since 11.0

§ Opera Mobile since 11.1

§ BlackBerry since 7.0

startXHR()

§ IE10+

§ Firefox 3.6 (and earlier)

§ Safari 3.x

§ Android 4.1 to 4.3 (unless Chrome is default browser)

§ Android 3.x

startIframe()

§ IE8

§ IE9

startLongpoll()

§ IE6

§ IE7

§ Android 2.x

§ Anything else not in the preceding list that has Ajax support

(none)

§ Any browser with JavaScript disabled

[a] Technically since Firefox 6 and Chrome 6, but they have been auto-updating since Firefox 4, and Chrome since it came out of beta, so you can reasonably expect no one is still using versions that do not support SSE.


[29] The only desktop browsers it adds for us are Firefox 3.x and Safari 3.x.

[30] At the time of writing, and based on global stats. Also, you could say we already had IE8+ support, because the long-poll approach of Chapter 6 works, too. Rephrasing for pedants: for another 28% of users, it gives us a solution that is almost as efficient as native SSE.

[31] What is more, when prefixing, some whitespace does make a difference—it is a strong hint that there is browser optimization, typically caching, to blame.

On that theme, there is a way to get the XHR technique to work in Android 2.x, not just Android 4.x! Change the echo json_encode($ch.$ch)."\n"; line (which outputs exactly 3 bytes) to echo json_encode($ch.$ch).str_repeat(" ",1021)."\n";, (which outputs exactly 1,024 bytes). Yep, 210 bytes. Smells like a buffer to me. But this is a really nasty hack, because every single message we send has to be padded. If the messages you want to send just happen to be that big, and you are latency sensitive, bandwidth sensitive, and sending quite frequent messages (meaning using long-poll for the Android 2.x users leaves you dissatisfied), then this may be just what you want. For the rest of us, it is better just to use long-poll for Android 2.x.

[32] Okay, all browsers that I have tried it on. Remember, out there in the real world this fallback is only going to be used by Android 4.x, and this feature detection works without problems there.