GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Chat Example App Using Server-Sent Events

Rails 4 can stream data to the browser with ActionController::Live using server-sent events (SSEs). I was curious how server sent events worked so I decided to use them to implement a simple chat application. Tiny-chat is a chat app I built using Goliath, Redis Pub/Sub and of course server-sent events.

Subscribing to Events

require 'goliath'
require 'redis'

class Subscribe < Goliath::API
  def response(env)
    EM.synchrony do
      @redis = Redis.new(Options::redis)
        channel = env["REQUEST_PATH"].sub(/^\/subscribe\//, '')

        # We pass the subscribe method a block which describes what to
        # do when we receive an event.
        # This block writes the message formatted as a server sent event
        # to the HTTP stream.
        @redis.subscribe(channel) do |on|
          on.message do |channel, message|
            @message = message
            env.stream_send(payload)
          end
        end
      end
    end
    streaming_response(200, { 'Content-Type' => "text/event-stream" })
   end
  end

  def on_close(env)
    @redis.disconnect
  end

  def payload
    "id: #{Time.now}\n" +
    "data: #{@message}" +
    "\r\n\n"
  end
 end
end

When tiny-chat connects to the server it sends a GET request to /subscribe/everyone where everyone is the name of the channel and with the “Accept” header set to text/event-stream. The streaming middleware (above) receives this request and subscribes to a redis Pub/Sub channel. Since Goliath is non-blocking multiple clients can be listening for events without tying up a Heroku dyno. The payload of a server sent event looks like this:

id: 1361572294
data: {"sender":"mason", "message":"hi"}

Receiving messages

class Receive < Goliath::API
  @@redis = Redis.new(Options::redis)
  def response(env)
    channel = env["REQUEST_PATH"][1..-1]
    message = Rack::Utils.escape_html(params["message"])
    @@redis.publish(channel, {sender: params["sender"], message: message}.to_json)
    [ 200, { }, [ ] ]
  end
end

In the tiny-chat app, the client POSTs to /everyone with the message and the name of the sender encoded in JSON. The Receive middleware takes this request and publishes it to the appropriate redis Pub/Sub channel.

The Client

var source = new EventSource('/subscribe/everyone');
source.addEventListener('message', function(event) {
  message = JSON.parse(event.data);
  $('.messages').append(
    "
"+message.sender+"
"+ "
"+message.message+"
"); });

The client is a single page of static HTML, CSS and Javascript. First we instantiate an EventSource object. The EventSource object is built into the latest Firefox, Safari and Chrome. There are also polyfills available which bring support to other browsers including Internet Explorer.

Next, we add an event listener for the “message” event. “message” is the default event but you could also define your own custom events and bind event listeners to those. For example if the SSE payload was:

event: userJoined
data: {"username": "mason"}

You could bind to the custom event like this:

source.addEventListener('userJoined', function(event) {
  username = JSON.parse(event.data).username;
  alert(user + " joined the room!");
});

To send messages we do a regular ajax POST, prevent the default behavior of the form and reset the message field to be empty.

$('form').submit(function(e){
  e.preventDefault();
  $.post($('.room option:selected').val(),
  {
    sender: $('.name').text(),
    message: $('form .message').val()
  })
  $('form .message').val('')
});

That’s it! We now have a fully functional chat application. Tiny-chat is setup to run on Heroku with the Redis To Go plugin. There is a demo running on Heroku. And the source is available on GitHub. Try forking it and adding multiple chat rooms!

Some useful links: