Monday, May 3, 2010

Comet on Fab.js (Shakes Fist at Google Chrome)

‹prev | My Chain | next›

Last night I made great progress with comet and fab.js. Great progress that is until I tried it in an actual browser. Then I failed miserably.

So far, I can move a character about a <canvas> room by clicking anywhere in the room. In addition to the character walking to the mouse click, the character also sends the coordinate information to the fab.js server. That coordinate information is broadcasted to view-only listeners.

As of yesterday, this is what my curl client was seeing:
cstrom@whitefall:~/repos/fab$ curl http://localhost:4011/view -Ni
HTTP/1.1 200 OK
content-type: text/html
Connection: keep-alive
Transfer-Encoding: chunked

<html><body>
<script type="text/javascript">
alert('{"id":"me","x":370,"y":142}')
</script>
<script type="text/javascript">
alert('{"id":"me","x":206,"y":228}')
</script>
...
I have never really had need to do comet before, but it seems a simple enough concept. If I stick that output in a <iframe>, it ought to initiate javascript alerts. Much to my chagrin, there were no alerts.

I spent a good long time digging through chunked transfer encoding documentation and mucking with the output formats—all for naught. Finally, in a fit of frustration, I found myself clicking on the <canvas> room over and over and over again until... I finally saw an alert. And not just one alert, but every alert that I had expected to see in my frustration.

It seems that Google Chrome buffers comet responses until a certain threshold is reached. After that threshold is reached, the all subsequent comet pushes are interpreted immediately. Since I want the first request to be interpreted by the browser immediately, I need to figure out what that threshold is. Ah, trial-and-error...

Eventually, I find that somewhere in the neighborhood of 1024 characters are needed before the Google Chrome flood gates open. The /view resource that works immediately:
  ( /view/ )
( function(){
listeners.push( this );
this({headers: { "content-type": "text/html"},
body: "<html><body>\n"})

({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})

({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"});
} )
Each one of those 12345... script lines is exactly 100 bytes long. The original <html><body> is and additional 16 bytes. 1016 + any comet output always results in an alert() message. Also of note here is that the <html><body> line is required—there was no alert() without it.

With that, it is trivial for fab.js to tell a read-only player to walk in response to live movements:
  ( /move/ )

(
function () {
var out = this;
return function listener( obj ) {
if ( !obj ) out();
else if ( obj.body ) {
broadcast(obj);
}
return listener;
};
}
)


function broadcast(obj) {
listeners.forEach(
function(listener) {
var body = '<script type="text/javascript">' + "\nconsole.debug('" + obj.body + "')\n</script>\n";
listener({body: body});

body = '<script type="text/javascript">' + "\nloc = " + obj.body + ";\nwindow.parent.me.walk_to(loc.x, loc.y);\n</script>\n";
listener({body: body});
}
);
}
And just like that, I can make an event in one browser session show up in another:



I still have quite a bit of refactoring to do on the code (and some testing might be nice), but this is a very satisfying stopping point for the night.

Day #92

No comments:

Post a Comment