Dojo WebSocket

Dojo 1.6 introduces a new API for Comet-style real-time communication based on the WebSocket API. WebSocket provides a bi-directional connection to servers that is ideal for pushing messages from a server to a client in real-time. Dojo's new dojox.socket module provides access to this API with automated fallback to HTTP-based long-polling for browsers (or servers) that do not support the new WebSocket API. This allows you start using this API with Dojo now.

The dojox.socket module is designed to be simple, lightweight, and protocol agnostic. In the past Dojo has provided protocol specific modules like CometD and RestChannels, but there are numerous other Comet protocols out there, and dojox.socket provides the flexibility to work with virtually any of them, with a simple foundational interface. The dojox.socket module simply passes strings over the HTTP or WebSocket connection, making it compatible with any system.

The simplest way to start a dojox.socket is to simply call it with a URL path:

var socket = dojox.socket("/comet");

We can now listen for message events from the server:

socket.on("message", function(event){
  var data = event.data;
  // do something with the data from the server
});

Here we use the socket.on() event registration method (inspired by socket.io and NodeJS's registration method) to listen to "message events" and retrieve data when they occur. This method is also aliased to the Dojo style socket.connect().

We can also use send() to send data to the server. If you have just started the connection, you should wait for the open to ensure the connection is ready to send data:

socket.on("open", function(event){
  socket.send("hi server");
});

Finally, we can listen for the connection being closed by the server or network by listening for the "close" event. And we can initiate the close of a connection from the client by calling socket.close().

The dojox.socket method can also be called with standard Dojo IO arguments to initiate the communication with the server. This makes it easy to provide any necessary headers for the requests. For example:

var socket = dojox.socket({
	url:"/comet",
	headers: {
		"Accept": "application/json",
		"Content-Type": "application/json"
	}});

This will automatically translate the relative URL path to a WebSocket URL (using ws:// scheme) or an HTTP URL depending on the browser capability.

For some applications, the server may only support HTTP/long-polling (without real WebSocket support). We can also explicitly create a long-poll based connection:

var socket = dojox.socket.LongPoll({
	url:"/comet",
	headers: {
		"Accept": "application/json",
		"Content-Type": "application/json"
	}});

We can also provide alternate transports in the socket arguments object. This would allow us to use dojo.io.script.get to connect to a server. However, a more robust solution is to use the dojox.io.xhrPlugins for cross-domain long-polling, which will work properly with dojox.socket.

Auto-Reconnect

In addition to dojox.socket, we have also added a dojox.socket.Reconnect module. This wraps a socket, adding auto-reconnection support. When a socket is closed by network or server problems, this module will automatically attempt to reconnect to the server on a periodic basis, with a back-off algorithm to minimize resource consumption. We can upgrade a socket to auto-reconnect by this simple code fragment:

socket = dojox.socket.Reconnect(socket);

Using Dojo WebSocket with Object Stores

One of the other big enhancements in Dojo 1.6 is the new Dojo object store API (supercedes the Dojo Data API), based on the HTML5 IndexedDB object store API. Dojo 1.6 comes with several store wrappers, and the Observable store provides notification events that work very well with Comet driven updates. Observable is a store wrapper. To use it, we first create a store, and then wrap it with Observable:

define("my-module", function(require){
  var JsonRest = require("dojo/store/JsonRest");
  var Observable = require("dojo/store/Observable");
  var store = new JsonRest({data:myData});
  store = Observable(store);
});

This store will now provide an observe() method on query results that widgets can use to react to changes in the data. We can notify the store of changes from the server by calling the notify() method on the store:

socket.on("message", function(event){
  var existingId = event.data.id;
  var object = event.data.object;
  store.notify(object, existingId);
});

We can signal a new object by calling store.notify() and omitting the id, and a deleted object by omitting the object (undefined). A changed/updated object should include both.

Handling Long-Polling from your Server

Long-polling style connection emulation can require some care on the server-side. For many applications, the server may have sufficient information from request cookies (or other ambient data) to determine what messages to send the browser. However, other applications may vary on what information should be sent to the browser during the life of the application. Different topics may be subscribed to and unsubscribed from. In these situations, the server may need to correlate different HTTP requests with a single connection and its associated state. While there are numerous protocols, one could do this very easily be defining a unique connection and adding that as a header for the socket (the headers are added to each request in the long-poll cycles). For example, we could do:

var socket = dojox.socket.LongPoll({
	url:"/comet",
	headers: {
		"Accept": "application/json",
		"Content-Type": "application/json",
		"Client-Id": Math.random()
	}});

In addition, dojox.socket includes a Pragma: long-poll to indicate the first request in a series of long-poll requests to help a server ensure that the connection setup and timeout is properly handled.

We can easily use dojox.socket with other protocols as well:

CometD

To initiate a Comet connection with a CometD server, we can do a CometD handshake, connection, and subscription:

var socket = dojox.socket("/cometd");
function send(data){
  return socket.send(dojo.toJson(data));
}
socket.on("connect", function(){
  // send a handshake
  send([
    {
       "channel": "/meta/handshake",
       "version": "1.0",
       "minimumVersion": "1.0beta",
       "supportedConnectionTypes": ["long-polling"] // or ["callback-polling"] for x-domain
     }
  ]).then(function(data){
    // wait for the response so we can connect with the provided client id
    data = dojo.fromJson(data);
    if(data.error){
      throw new Error(error);
    }
    // get the client id for all future messages
    clientId = data.clientId;
    // send a connect message
    send([
      {
         "channel": "/meta/connect",
         "clientId": clientId,
         "connectionType": "long-polling"
       },
       {  // also send a subscription message
         "channel": "/meta/subscribe",
         "clientId": clientId,
         "subscription": "/foo/**"
       }
    ]);
    socket.on("message", function(){
      // handle messages from the server
    });
  });
});

Socket.IO

Socket.IO provides a lower-level interface like dojox.socket, providing simple text-based message passing. Here is an example of how to connect to a Socket.IO server:

var args, ws = typeof WebSocket != "undefined";
var socket = dojox.socket(args = {
  url: ws ? "/socket.io/websocket" : "/socket.io/xhr-polling",
  headers:{
    "Content-Type":"application/x-www-urlencoded"
  },
  transport: function(args, message){
    args.content = message; // use URL-encoding to send the message instead of a raw body
    dojo.xhrPost(args);
  };
});
var sessionId;
socket.on("message", function(){
  if (!sessionId){
    sessionId = message;
    args.url += '/' + sessionId;
  }else if(message.substr(0, 3) == '~h~'){
    // a heartbeat
  }
});

Comet Session Protocol

And here is an example of connecting to a Comet Session Protocol server (the following example was tested with Orbited, but could work with Hookbox, APE, and others):

var args, socket = dojox.socket(args = {
  url: "/csp/handshake"
});
function send(data){
  return socket.send(dojo.toJson(data));
}
var sessionId = Math.random().toString().substring(2);
socket.on("connect", function(){
  send({session:sessionId}).then(function(){
    args.url = "/csp/comet";
    send({session:sessionId});
  });
});

Tunguska

Tunguska provides a Comet-based interface for subscribing to data changes. This is a very simple protocol which allows us to communicate with a Tunguska server:

var socket = dojox.socket({
	url:"/comet",
	headers: {
		"Accept": "application/json",
		"Content-Type": "application/json",
		"Client-Id": Math.random()
	}});
function send(data){
  return socket.send(dojo.toJson(data));
}
socket.on("connect", function(){
  // now subscribe to all changes for MyTable
  send([{"to":"/MyTable/*", "method":"subscribe"}]);
});

Conclusion

Dojo's socket API is a flexible simple module for connecting to a variety of servers and building powerful, efficient real-time applications without constraints. This adds to the array of awesome new features and improvements in Dojo 1.6.