JS-Free Live Chat

I built a real-time web-based chat without a line of client-side code. No JavaScript, no Flash, no (ew) Java applet. 100% HTML. It even works in Internet Explorer 4, emulated with SheepShaver.

Because I remember the canonical node.js example being a chat server, I had to call it No.JS (no-dot-JS, got it? Haha. Sorry.)

No (dot) JS

To be fair, the backend could have been built in JavaScript using Node, instead of Python using Brubeck. It would make for the dandy perversity of having exclusively server-side JavaScript.

All the code for it is on GitHub, so all you Node-hackers should go ahead and build a JavaScript edition…


Why’d I do this, and how does it work?

I’m building a real-time browser-based multiplayer game, using the same templates (Mustache) on the client and server-side. Instead of messing around with WebSockets, I used AJAX long polling to push updates from the server to the client. Since the server could render all the views, I set up a little Accept header goodness so that browsers with JavaScript turned off could still play… but the problem of pushing the state remained.

There is the peculiarly irritating <meta http-equiv="refresh" ...> tag, embedded in my consciousness from some late-90s “dynamic” interfaces. Every few seconds, the page would flash to refresh with possibly new content. Maddeningly, you would be thrown back to the top of an identical (or very slightly modified) page.

Throwing the meta refresh inside a <noscript /> would make the game kind-of work, while not irritating the majority of JavaScript-enabled users. The refresh still wouldn’t be live, though. And the more frequent it was, the more irritating it would be.

So why not just long-poll the whole page?

Browsers wait for a response before wiping the contents of a window. The prior page works the same while a new page is loading. The server can dictate exactly when the page should reload.

The stakes are higher than when long polling an AJAX request. If the server doesn’t get back to the client with a response eventually, the browser doesn’t merely log to the error console — it replaces the page with a message that the server is down. Responding with identical content at least twice a minute prevents this.

To put it all together, the Mustache template would look like this in the header:

<!-- Only fuss with this for those without JavaScript. -->
<noscript>
    <!-- The content of the refresh header is generated by the handler. --> 
    <meta http-equiv="refresh" content={{refresh}} />
</noscript>

And this in the body:

{{#content}}
    <!-- Exciting rapidly changing content! --> 
{{/content}}

The function to generate content, using Redis, would look like:

def get_page_content(old_id):
    # If an old ID was specified, and it's equal to the current ID,
    # subscribe to updates on the 'updates' channel of our Redis database.
    # Then listen for an update.  Whatever triggers the update should also
    # modify the ID.
    if old_id and old_id == db.get('id'):
        pubsub = db.pubsub()
        pubsub.subscribe('updates')
        pubsub.listen().next() 
    return {
        # Populate the refresh header in the template with the new
        # ID.  The `0;` means that the browser will immediately
        # try to reload the page -- but will hang, because the
        # reload is for the new ID. 
        'refresh': "0; url=?id=%s" % db.get('id'),
        # This would be a separate function, but you get the point.
        'content': json.loads(db.get('content')) 
    }

The handler, using Brubeck with gevent, would look like:

class PageHandler(MustacheRendering):

    # The route is specified elsewhere, but this method will be called
    # when there is a vanilla HTTP GET for the page.
    def get(self):
        # Pull out the last loaded page ID.  Will be `None` if none was
        # specified.
        old_id = self.get_argument('id')
        try:
            # See `get_page_content` above.
            context = gevent.timeout.with_timeout(30, get_page_content, old_id)
            return self.render_template('page', **context)
        except gevent.timeout.Timeout:
            # The timeout above was for thirty seconds.  If nothing changes
            # after that time, the client is simply redirected back to the
            # original page with no ID argument at all.  This will cause
            # an immediate reload.
            #
            # We do this instead of redirecting to `"?id=%s" % old_id` 
            # because that would cause another hang, and possibly another
            # 3xx response if we hit this timeout again.  If nothing
            # changes for a few minutes, which is likely, then the repeated
            # redirects would wipe the page with a `too many redirects`
            # error.  This avoids that eventuality. :)
            return self.redirect('?')

Neato!

No.JS is pulled from the game’s chat component. Chat is tricky, because a refresh wipes forms. You’re typing, someone says something… and everything you were about to say goes to the bitbucket. Keeping the chat messages inside an <iframe /> solves this problem. I switched it out for a regular frame in No.JS to support older browsers.

Comments