Rappid - com

com.Channel

The Channel plugin is a very powerful tool that brings real-time collaboration capability to your applications. The plugin synchronises JointJS graphs between clients and server and automatically resolves merge conflicts. Real-time collaboration is only one application of this plugin. Others include push updates, easy persistence, monitoring and more.

The Channel plugin uses Operational transformations to maintain consistency across concurrent updates. This means that even though there are different updates to the same JointJS graph that happened at the same time at different sites (across clients and even server), the Channel plugin makes sure the result, after applying those updates, is exactly the same at all sites while preserving intention.

The Channel plugin runs both in browsers and NodeJS environment and requires a NodeJS server running in order to synchronize clients. The procedure to get the channel plugin up and running is quite straightforward and is described in this document. However, if you, for any reason, do not want to maintain a NodeJS application on your backend, please let us know. Thanks to the cross-domain nature of the Channel plugin, we can run the NodeJS application for you which means that the only thing you have to do is to use the client-side part of the Channel plugin. This allows you to have real-time collaboration in your applications without any backend configuration!

Install

In a browser

Include joint.com.channel.js file into your HTML:

<script src="joint.com.channel.js"></script>

On a server

Make sure you have NodeJS installed. NodeJS installation already comes with NPM (Node Package Manager) that we'll use to install all the dependencies. Go to the Rappid package root directory (where the package.json file is located) and run:

npm install

This will install all the necessary dependencies needed by the server side Channel.

Usage

In a browser

In a browser, the only thing necessary is to instantiate the joint.com.Channel constructor function and pass it a url of the server-side channel and the graph we want to be synchronising:

var graph = new joint.dia.Graph;
var channel = new joint.com.Channel({ url: 'ws://localhost:4141', graph: graph });

Note we're using the ws protocol as the Channel plugin uses WebSockets for real-time communication between server and client. If you plan to take advantage of us maintaining the NodeJS server for you, you'll get a dedicated URL that you can use in your channel and you can skip the rest of the Usage section.

On a server

On a server, we use the exact same joint.com.Channel constructor function but this time we pass it a port we'd like the WebSocket server to be listening on:

var joint = require('./index');
var Channel = require('./plugins/com/Channel/joint.com.Channel').Channel;

var graph = new joint.dia.Graph;
var channel = new Channel({ port: 4141, graph: graph });

In this example, we assume the server side script is located in the root directory of the Rappid package so that the paths used in the require()s are valid.

Rooms

The problem with the previous approach is that if you have more clients that should be divided into groups (rooms) each room sharing its own JointJS graph, you'd have to run as many servers as you have rooms each on a different port. This limits the number of rooms you can serve by the number of ports that your OS allows you to allocate. In general, this is not an ideal approach. Instead, we'd rather have just one server running that is able to maintain more graphs and route the communication between clients based on the room they're in. This is exactly what joint.com.ChannelHub does.

ChannelHub runs a WebSocket server and handles client connections. When a client wants to get connected, ChannelHub decides to which Channel it should route the requests to. In this case, the only server running is the ChannelHub. The server side Channels don't create their servers internally. Your server side script changes to something like this:

var channels = {};
var channelHub = new ChannelHub({ port: 4141 });
channelHub.route(function(req, callback) {

    var query = JSON.parse(req.query.query);
    var channel = channels[query.room] || null;

    if (!channel) {
       channel = channels[query.room] = new joint.com.Channel({ graph: new joint.dia.Graph });
    }

    // If an error occurred, call the callback function with the error as the first argument.
    // callback(new Error('Some error has occurred.'));

    // Otherwise, call the callback function with the channel as the second argument.
    callback(null, channel);
});

Notice how we instantiated the Channel. In this case, we don't pass neither a url nor a port. This defines a Channel that is not a client (no url) and it also does not create a WebSocket server internally (no port). Instead, it is the ChannelHub that handles requests and decides to which Channel it should route messages to.

Also, your client side initialization of Channel changes a little bit in order to enable rooms:

var channel = new joint.com.Channel({ url: 'ws://localhost:4141', graph: graph, query: { room: 'A' } });

We pass the query object that contains some data that help the ChannelHub decide to which channel to route the messages to. In this case, we use room: 'A' and on the server, we saw query.room which we base our decision on. This data can be arbitrary. Feel free to use any kind of decision making mechanism.

Configuration

Channel

  • url - URL of the server-side channel to which we want the client to be connected to. If this parameter is passed, the channel is considered to be a client. (Note that you can have a client running in NodeJS too.)
  • port - port number of the channel server. If this parameter is passed, the channel is considered to be a server.
  • reconnectInterval - if a client looses a connection to the server, it tries to reconnect every reconnectInterval milliseconds. The default is 10000ms = 10s. This settings makes sense only for clients.
  • healthCheckInterval - Interval in milliseconds in which a health check is performed on all the sites of a server. Each health check decreases the ttl value of a site if the site socket is disconnected. If the site socket gets connected again, its ttl returns to its original value. The default is a health check performed every 1 minute. Considering the default ttl is 60, this means that if every minute of an hour a site didn't respond (its socket was closed) then such a site is considered dead (not responding anymore) and therefore removed from the channel register. The reason for health checks is the following. Consider you have ChannelHub running your server. This ChannelHub contains couple of Channels each representing a different room. Consider there is 3 clients connected to one room simultaneously editing the same graph. One of the clients doesn't want to work anymore and shuts down his browser or closes the tab with your application. Now the channel of the room still keeps this client in its register as the client might reconnect anytime (the user closed his browser, went for a coffee and came back in ten minutes.) The healthCheckInterval and ttl allows you to configure the maximum waiting time in which the channel should still keep track of the client. This setting makes sense only for server.
  • ttl - time-to-live of a site. Explained above. The actual time to live of a site in milliseconds is ttl * healthCheckInterval. The default ttl is 60. This setting makes sense only for server.
  • query - An object that is send with the initial handshake of a client with a server. This can be an arbitrary JSON object. The query object is especially useful when working with rooms (see above for documentation on using the Channel plugin with rooms). This setting makes sense only for clients.
  • debugLevel - the verbosity of debugging logs. The default 0 meaning no debug messages are printed out. Set to a higher number if you want to get some inside on how the Channel works.

ChannelHub

  • port - port number of the channelHub server.
  • route(req) - this is actually not an option that you pass to the ChannelHub constructor function but a method that you define directly on the channelHub object. The route(req) method receives a connect request from a client and must return a channel object that is supposed to further communicate with that client. The req parameter contains the request object. The req.query.query contains the query object that the client sent during handshake (see above the Channel configuration section). Note that req.query.query is a JSON string so you should convert it to an object with JSON.parse() before accessing its inner properties. Use this object to determine what channel to return.