🎉 JointJS has new documentation! 🥳
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!
Include joint.com.channel.js
file into your HTML:
<script src="joint.com.channel.js"></script>
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.
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, 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.
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.
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.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.