faye-websocket 0.3: EventSource support, and two more Ruby servers

The latest iteration of faye-websocket has just been released for Ruby and Node, probably the last release before I get back to making progress on Faye itself. It contains two major new features: EventSource support, and support for the Rainbows and Goliath web servers for the Ruby version.

EventSource is a server-push protocol that’s supported by many modern browsers. On the client side, you open a connection and listen for messages:

var es = new EventSource('http://example.com/events');
es.onmessage = function(event) {
  // process event.data

This sends a GET request with Content-Type: text/event-stream to the server and holds the connection open, and the server can then send messages to the client via a streaming HTTP response. It’s a one-way connection so the client cannot send messages to the server over this connection; it must use separate HTTP requests. However, EventSource uses a much simpler protocol than WebSocket, and looks more like ‘normal’ HTTP so it has less trouble getting through proxies.

On the server side, supporting this requires a lot of the same code as WebSocket, and I might use it in Faye later on, so I decided to add support for it in faye-websocket. In your Rack app, you can now easily handle EventSource connections using an API similar to WebSocket:

require 'faye/websocket'

App = lambda do |env|
  if Faye::EventSource.eventsource?(env)
    es = Faye::EventSource.new(env)

    # Periodically send messages
    loop = EM.add_periodic_timer(1) { es.send('Hello') }

    es.onclose = lambda do |event|
      es = nil

    # Async Rack response

    # Normal HTTP
    [200, {'Content-Type' => 'text/plain'}, ['Hello']]

Just like WebSocket, EventSource is designed as a convenient wrapper around the Rack environment and underlying TCP connection that deals with the wire protocol and connection details for you. It tries not to make any assumptions or force constraints on your application design. There are a lot of WebSocket libraries around whose interfaces look more like Rails controllers; black-box Rack components or full-stack servers that hide the socket object and force you respond to WebSockets on one endpoint, and normal HTTP on another. Faye needs to be able to speak many different transport protocols over a single endpoint, which is why this library is designed to be usable inside any Rack adapter while leaving routing decisions up to you.

The benefit of interacting with the sockets as first-class objects is that you can pass them to other parts of your application, which can deal with them as a simple abstraction. For example, if your application just needs to push data to the client, as many WebSocket apps do, you can maintain a socket pool full of objects that respond to send(). When the application wants to push data, it selects the right connection and calls send() on it, without worrying whether it’s a WebSocket or EventSource connection. This gives you some flexibility around which transport your client uses.

The Node API to this is very similar and both are fully documented on GitHub: Ruby docs, Node docs.

The final big change is that the Ruby version now works under a broader range of web servers; it now supports Rack apps running under Thin, Rainbows and Goliath. Hopefully, providing a portable socket implementation that’s easy to drop into any Rack app will open up possibilities for more portable async app frameworks, decoupling the application from the network transport just as Rack did for HTTP. faye-websocket extracted its initial Thin and Rainbows adapters from the Cramp project, and there’s a chance Cramp can now remove its WebSocket code and rely on a simple abstraction for binding the app framework to the web.

As usual, download from Rubygems and npm, and get on the Faye mailing list if you have problems.

If you’ve enjoyed this article, you might enjoy my recently published book JavaScript Testing Recipes. It’s full of simple techniques for writing modular, maintainable JavaScript apps in the browser and on the server.