New features in Faye 1.1

This announcement is delayed by a couple of months since Faye 1.1 was actually released. This is partly because I took a 6-week break from doing open source work, and partly to allow some time to iron out teething problems with the new code. I’ve got a few bug-fix releases done across all the new modules now and things are looking pretty stable.

As well as Faye 1.1, there are new releases of:

And two brand new modules:

We’ll get to those new models in a second, but first let’s get through the new features in Faye itself.

The biggest change in 1.1 is really a bug fix, but it enables some new features. In 1.0, it was possible for the client to get into a state where if it went offline, it would flood the server with duplicate messages when it reconnected. Error recovery has long been the trickiest part of the client to get right, and 1.1 introduces a whole new layer in the stack just to deal with tracking message state. It’s called the Dispatcher, and it sits between the Client and the Transport. Its job is to accept messages from the client, send them via the transport, and deal with errors and timeouts. It tracks the state of every in-flight message to make sure that messages are retried when they fail, but duplicates are not sent if there’s currently an in-flight request carrying a message to the server.

Previously, this state was either implicit (hidden on the call stack or inside timers), or it was spread across the client and transport. Given the client can use multiple transports, that causes problems: if you send a message via long-polling and then switch to websocket, you don’t want the long-polling transport to retry the message if it fails. So, encapsulating all the state related to in-flight messages in a single class has solved a lot of error recovery problems.

It also enables some new features. When sending a message, you can use the attempts and deadline options to control the retry mechanism if it fails.

client.publish('/chat', {message: 'Hi'}, {attempts: 3});

client.publish('/chat', {message: 'Hi'}, {deadline: 15});

attempts sets the maximum number of times the dispatcher will try to send the message to the server. deadline sets the maximum time it should continue trying for; deadline: 15 means the dispatcher will only retry for up to 15 seconds after the initial publish() call.

If you want even more control than that, you can completely override the system that controls message retries, called the Scheduler. When creating a client, you can pass in a custom scheduler class to use in place of the default one. attempts and deadline are sugar for features provided by the default scheduler. For example, if you want to use exponential backoff rather than periodic retries, you can do this with a customer scheduler.

var client = new Faye.Client(url, {scheduler: CustomScheduler});

For more information on this API, see the documentation.

Next, we’ve added proxy support to Faye, faye-websocket and websocket-driver. In Faye server-side clients, you can specify a proxy to connect through like this:

var client = new faye.Client(endpointUrl, {
  proxy: 'http://username:password@proxy.example.com'
});

This will route all HTTP and WebSocket connections through proxy.example.com with the given optional username and password. If you’re using an https: proxy and need to specify TLS settings, you can do that:

var client = new faye.Client(endpointUrl, {
  proxy: {
    origin: 'http://username:password@proxy.example.com',
    tls:    {cert: fs.readFileSync('client.crt')}
  }
});

In faye-websocket, you can also specify headers that will be sent to the proxy, but not the backend server:

var ws = new WebSocket.Client('ws://www.example.com/', null, {
  proxy: {
    origin:  'http://username:password@proxy.example.com',
    headers: {'User-Agent': 'node'},
    tls:     {cert: fs.readFileSync('client.crt')}
  }
});

As for normal WebSocket data, the handling of the protocol to connect via a proxy is implemented in websocket-driver; see the docs for usage information.

As well as setting the proxy explicitly, Faye (as in the faye module, not the underlying WebSocket modules) supports the HTTP_PROXY and HTTPS_PROXY environment variables and will route all HTTP and WebSocket connections through a proxy if those are set.

There are one or two other small tweaks, for example client.disconnect() now returns a promise, the monitoring API yields the clientId for publish events, and you can access a RackAdapter used as middleware via a block:

use Faye::RackAdapter, :mount => '/bayeux', :timeout => 25 do |faye|
  faye.add_extension AnExtension.new
  client = faye.get_client
end

And finally, there’s the new modules I mentioned above:

The whole Faye stack - Faye, faye-websocket, and websocket-driver - now supports the permessage-deflate protocol extension for compressing the message stream. You can enable it in Faye like so:

var faye    = require('faye'),
    deflate = require('permessage-deflate');

var bayeux = new faye.NodeAdapter({mount: '/bayeux', timeout: 25});
bayeux.addWebsocketExtension(deflate);

If you’re using faye-websocket directly:

server.on('upgrade', function(request, socket, body) {
  var ws = new WebSocket(request, socket, body, null, {extensions: [deflate]});
});

Or, if you’re using websocket-driver:

server.on('upgrade', function(request, socket, body) {
  var driver = websocket.http(request);
  driver.addExtension(deflate);
});

Rather than build permessage-deflate support directly into the driver, I’ve introduced a new framework called websocket-extensions, that only deals with the extension-related concerns of the protocol. The intention is that extensions can be written as plugins for this framework, and any WebSocket library can use these plugins by building on top of websocket-extensions. permessage-deflate is the first standard extension for it, but the architecture means anyone can invent and implement their own extensions, in situations where your clients are outside the browser. I’ll say more about this in an upcoming post.

All the above is documented for Node and Ruby on each module’s respective page if you follow the links at the beginning of this post. As usual, this should all be a drop-in replacement for previous versions and please let me know if you find bugs in any of it.

I must also thank several people who contributed patches to 1.1, and helped identify, diagnose, fix, and test some of the bugs we’ve fixed. Adrien, Adrien Jarthon, Andrew Newdigate, David Glasser and Luigi Pinca have all been very active in helping get these modules - over 20 minor releases to date across all these modules - to where they are today and deserve a big thank-you.