Easy Streams with Rails 4 ActionController::Live

If you ever have to deal with an application where it's important that the view different users are seeing, at the same time, is up-to-date real time, then you've probably implemented a couple of different solutions such as refreshing the page (or parts of it), using long-poll ajax or a mix and match of implementations.

Having to deal with this recently, I was very interested in the offerings by the new ActionController::Live class in Rails 4. I'm going to go through a short tutorial on how to use this functionality and address some of the caveats.

Requirements:
  • Rails 4 (and a non-buffering web server (Thin, Rainbows!, Puma etc.., Apache w/Passenger))
  • Some Javascript
  • Users on a modern browser
Implementation:
  • We plan to use the EventSource Javascript model to handle event receiving and processing. EventSource is supported by modern browsers, like Firefox, Chrome, Opera and Safari. Internet Explorer is supposed to support it with IE10, but I have not confirmed it. Although, you can grab the source for EventSource.js and probably hack something together.

  • EventSource uses a protocol for message processing with fields: id, event, and data. Following the protocol allows the EventSource Javascript library to pick up where it left off, sending back the last ID automagically, in case of the client disconnecting.

  • Let's get started by creating a new controller, mixing in the new Live ActionController. Live ActionController will enable all actions of the controller to stream. We will also add index method to respond to requests and stream back data.

  • In this contrived example, we are going to write an action that will respond to clients and let each client know about the other clients..

class NewStreamingController < ActionController::Base  
   include ActionController::Live

   def index
      # .. store current accessing user

      # Set the response header to keep client open
      response.headers['Content-Type'] = 'text/event-stream'

      # .. list of users who are current streaming the list
      list_of_current_streamers = Users.streamers

      # loop infinitely, users can just close the browser
      begin
         loop do
            # .. iterate over the list and send the list of users.
            names = list_of_current_users.collect(&:name)
            # build the package to send over the stream using 
            # Server-Sent Events protocol format.
            response.stream.write "id: 0\n"
            response.stream.write "event: update\n"
            # two new lines marks the end of the data for this event.
            response.stream.write "data: #{JSON.dump(names)}\n\n"

            # decided to only send it ever 2 seconds.
            sleep 2
         end
       rescue IOError
          # client disconnected.
          # .. update database streamers to remove disconnected client
       ensure
          # clean up the stream by closing it.
          response.stream.close
       end
   end
end
  • Now that we have our server streamer written, we need to put together a client to consume the steamer.
  • We will use jQuery for this client, but you can use whatever you'd like.
jQuery(document).ready(function(){  
   var source = new EventSource("/streams");
   source.addEventListener('update', function(e){
      // update a div, reload a section of the page
   });
   // you can add different event listeners to
   // process some logic based on the event push
   // from the server to do something unique based
   // users leaving, joining the application or other
   // kinds of events not releated to users.
});

With just a little implementation such as this one, clients using your application can be aware of what is going on with other users of the application in real-time, without having to work out how the clients will get the updates from the server with polling or refreshing the page.

This very simple example can be expanded in endless ways, both on the server and the client. The client can be written to behave differently, depending on what kind of event it receives. Updates can behave one way, while alerts, errors, or any other event behaves in other ways.

The main caveat with streaming is that is dependent on a non-buffering web server and, more importantly, supported implementation of Javascript on the client side. As browsers work ever so slowly towards standardized implementations of streaming, hopefully that particular caveat will become less and less of a concern.