Hopefully in Part 1 I wrote about a means of writing and deploying a long-polling server in Ruby. Of course that’s useless by itself, so here we’ll hook it up to a browser client via Ajax.

We’ll touch on

CORS (Cross-Origin Resource Sharing)

Unfortunately you can’t just fire an Ajax request at polling.myapp.com and go. Actually it’s not unfortunate, because it’s an important part of browser security known as the Same Origin Policy . For our purposes, it states that our script loaded from myapp.com can’t make an Ajax request to any other domain.

Obviously that’s quite limiting, so the Sort of People Who Think Up These Kinds of Things thought up CORS. Here’s an article about it and here’s another one .

In short, if your client is IE 8+, Firefox 3.5+, Safari 4+, or a recent Chromium derivative, then you can use CORS without any extra work on the client side. There’s a little server side work, but we already covered that. Remember under the ‘My App’ section of Part 1 where I added the Access-Control-Allow-Origin header? That’s it. Just set that header to a ‘myapp.com’ to allow access. To allow access from anywhere, just use *. (Unfortunately *.myapp.com isn’t supported, which irks me.)

Javascript and browser issues

Using jQuery, I’m putting some example Javascript below. Theoretically it’s quite straightforward, but there’s always that one thing that ruins your day. (Yes, it’s what you’re expecting.) While version 8+ of Microsoft’s “browser-like program” claims to support CORS, it does not do so through the XMLHttpRequest object like every other browser on the freaking planet. They had to go and create a whole new object just for use with cross-domain requests. Why? I suspect someone couldn’t find a puppy to kick that morning.

With that unpleasantness in mind, here are some codes:

// Assume we're in a sane, modern browser
$(function() {
  try {
    // Test the waters with a "ping" to the root of the polling app
    $.ajax({
      url: 'polling.myapp.com',
      type: 'GET',
      dataType: 'json',
      success: function(data, textStatus, jXHR) {
        // Ping went through, initiate long polling
        if ( data.ack && data.ack == 'huzzah!' ) long_poll_walls(walls)
        // Ping failed, fall back to simple polling every 20 sec
        else setInterval(function() { simple_poll_walls(walls) }, 20000)
      },
      error: function(jXHR, textStatus, errorThrown) {
        // Ping failed, fall back to simple polling every 20 sec
        setInterval(function() { simple_poll_walls(walls) }, 20000)
      }
    })
  // Guess we aren't
  } catch (e) {
    // It's Microsoft's browser-like program!
    if ( jQuery.browser.msie && window.XDomainRequest ) ie_long_poll_walls(walls)
    // Fall back to simple polling every 20 sec
    else setInterval(function() { simple_poll_walls(walls) }, 20000)
  }
})

// For normal browsers
function long_poll_walls(walls) {
  d = {last_post_id: {}, session_id: 'some token to prove who you are'}
  // Record the last post under each wall.
  $(walls).each(function() {
    var wall_id = $(this).attr('data-id')
    d.last_post_id[wall_id] = $('.post:last', this).attr('data-id') || 0
  })
  $.ajax({
    url: 'polling.myapp.com',
    async: true,
    timeout: 180000, // 3 minutes
    type: 'POST', // Using POST because it makes the polling app a little harder to abuse
    data: d,
    dataType: 'json',
    success: function(data, textStatus, jXHR) {
      var success = wall_polling_callback(walls, data, textStatus, jXHR)
      if ( success ) long_poll_walls(walls)
      else setTimeout(function() { long_poll_walls(walls) }, 5500)
    },
    error: function(jXHR, textStatus, errorThrown) { long_poll_walls(walls) } // Assume it just timed out and continue
  })
}

// For IE
function ie_long_poll_walls(walls) {
  // Build params to send with request
  var params = ''
  $(walls).each(function() {
    var wall_id = $(this).attr('data-id')
    var last_post_id = $('.post:last', this).attr('data-id') || 0
    params += 'last_post_id['+wall_id+']='+last_post_id+'&'
  })
  params += 'session_id=your happy session token'

  // Set up request
  var xdr = new XDomainRequest()
  xdr.open('post', 'polling.myapp.com')
  xdr.onload = function() {
    var success = wall_polling_callback(walls, JSON.parse(this.responseText), 'success', xdr)
    if ( success ) ie_long_poll_walls(walls)
    else setTimeout(function() { ie_long_poll_walls(walls) }, 5500)
  }
  xdr.send(params)
}

Short-polling fallback

Older browsers without CORS support will need a simpler short polling scheme, every n seconds. For simplicity’s sake, I just make this a part of my Rails app.

function simple_poll_walls(walls) {
  // Just a normal Ajax request to your app with callbacks.
  // You can figure it out.
}

Why write your own?

There are several good Ruby long-polling servers out there (e.g. Goliath) so why would you want to write your own? For starters there’s a small sense of pride. But mostly, it’s the best way to understand what’s going on. Sure, Goliath may be superior to my attempt in many respects. But really, isn’t this more fun?