Dojo WebSocket with AMD

By on November 5, 2012 12:23 pm

Dojo has an 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 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:

require(["dojox/socket"], function (Socket) {
	// Create socket instance
	var socket = new Socket("/comet");
});

The socket module will then connect to the origin server using WebSocket, or HTTP as a fallback. 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 deprecated 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 event 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 = new 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 = new 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 the get() method in dojo/io/script 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:

require(["dojox/socket", "dojox/socket/Reconnect"], 
	function (Socket, Reconnect) {
	// Create socket instance
	var socket = new Reconnect(new Socket("/comet"));
});

Using Dojo WebSocket with Object Stores

One of the other big enhancements in Dojo is the Dojo object store API (which supercedes the Dojo Data API), based on the HTML5 IndexedDB object store API. Dojo 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:

require([
	"dojo/store/JsonRest",
	"dojo/store/Observable"
], function(JsonRest, Observable){
	var store = Observable(new JsonRest({data:myData}));
});

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:

require([
	"dojox/socket"
], function(Socket){
	var socket = 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 = new Socket("/cometd");
function send(data){
  return socket.send(json.stringify(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
     }
  ])
  socket.on("message", function(data){
    // wait for the response so we can connect with the provided client id
    data = json.parse(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:

require([
	"dojo/request", "dojox/socket"
], function(request, Socket){
	var
		args = {},
		ws = typeof WebSocket != "undefined",
		url =  ws ? "/socket.io/websocket" : "/socket.io/xhr-polling";
		
	var socket = new Socket(args = {
		url:url,
		headers:{
			"Content-Type":"application/x-www-urlencoded"
		},
		transport: function(args, message){
			args.data = message; // use URL-encoding to send the message instead of a raw body
			request.post(url, args);
		}
	});
	var sessionId;
	socket.on("message", function(){
		if (!sessionId){
			sessionId = message;
			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):

require([
	"dojo/json", "dojox/socket"
], function(json, Socket){
	var args, socket = new Socket(args = {
		url: "/csp/handshake"
	});
	function send(data){
		return socket.send(json.stringify(data));
	}
	var sessionId = Math.random().toString().substring(2);
	socket.on("connect", function(){
		send({session:sessionId});
		socket.on("message", 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 = new Socket({
	url:"/comet",
	headers: {
		"Accept": "application/json",
		"Content-Type": "application/json",
		"Client-Id": Math.random()
	}});  
function send(data){
	return socket.send(json.stringify(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 features in Dojo.

Comments

  • Rob Moran

    Great post!

    I would love to see what can be done with Dojo and NodeJS (both running Dojo on the server-side and integrating it client-side with existing frameworks such as socketstream).
    Could you consider this topic for a future article?

    Cheers,

    Rob

  • @Rob, agreed, it’s on our blog todo list!

  • Hi,

    Absolutely +1 for a guide on how to use this with nodeJS as a backend.

    I personally needed something done — I wanted something _really_ lightweight and built on stuff I knew. I looked into dojox/socket, and well, I had to give up as I couldn’t quite figure out what to do.

    I ended up doing this:

    https://github.com/mercmobily/hotplate/blob/master/hotplate/node_modules/hotMessages/client/messages.js

    The interesting thing is the backend:

    https://github.com/mercmobily/hotplate/blob/master/hotplate/node_modules/hotMessages/lib/hotMessages.js

    Basically, when an application starts, the client will run:

    messages.register( workspaceId )

    This will create this.tabId in ‘messages’ (a variable local to the running tab). So, if you open up 10 tabs in your browser, you end up with 10 instances of ‘messages’ each one with a tabId.

    Then, I use aspect to make sure that every single store request includes a tabId it its request:

    // This is run for each store in my application
    aspect.before( store, ‘put’, function( object, options ){
    object._tabId = messages.tabId;
    return [ object, options ];
    });

    Each call needs to tell the server the tabId because in pretty much all cases I don’t want messages to echo back to the client who sent it — or triggered it on the server side (by modifying a store).

    As a result, in my application right now if you change your username, within 10 seconds other tabs you have open will get the notification from the server (the tab where you changed it, though, won’t get the notification). ALSO other users logged into the same workspace _also_ will get the notification (your username will change in their screen as their store will be notified).

    It’s quite fancy, and it took a lot of work. But it works! I just wish I could do this with Websocket, so that the change would be _seriously_ instantaneous :D

    Thank you!

    Merc.

  • dylanks

    Merc,

    Kitson Kelly has been working on a tutorial… I expect it to be ready/live in the very near future.

    -Dylan