Real-time data pushed directly to the browser is a powerful tool when designing an application. Bridge provides real-time access to data changes from the warehouse with HarbingerJS. Applications can also generate and consume real-time messages just for their own use, unrelated to the warehouse. A specific use case is demonstrated in part 9 of the Rails tutorial.

Prerequisites

HarbingerJS is included in the Rails Extensions gem. The gem provides both the HarbingerJS library to the asset rails pipeline as well as a Rails helper method to handle passing serverside information into the JavaScript library.

Warning: You must perform the HarbingerJS setup to use this code!

Additionally you'll need to include some specific JavaScript files in your asset pipeline to use these features. An example excerpt of an application.js:

//= require harbinger-js/core
//= require harbinger-js/cometd/cometd.js
//= require harbinger-js/cometd/jquery.cometd.js
//= require harbinger-js/amqp-listener.js

Connecting

HarbingerJS uses cometd as a communication/transport layer to an AMQP messaging bus. The first step is to connect to cometd. cometd works by querying the server with a handshake request. This handshake determines the protocol(s) supported by the server and subsequently used by the cometd JavaScript libraries. After a successful handshake the library attempts to connect. cometd (the Bayeux protocol specifically) has an architecture for connection and channel management, but HarbingerJS conceals this detail with abstractions. Establishling a connection to real-time data feeds is this simple:

$(document).ready(function() {
  $.harbingerjs.amqp.setup({url: $.cometURL});
});

You should now be able to see a cometd connection to the server with your browser developer tools.

Warning: The $.cometURL will only be set after following the directions in harbinger-rails-extensions setup

Binding

Binding is how you recieve messages from AMQP exchanges into a browser. The exchange for clinical data from the warehouse is name audit and it provides a stream of all database transactions. The routing key format is tablename.operation_type.database_row_id, where tablename is the name of the table in the database, operation_type is the type of operation performed (insert,update,delete), and id is the primary key of the affected row in the tablename.

The payload consists of the pre- and post-values for that transaction (before and after the message that caused the change).

Binding requires two arguments: the exchange name and the routing key matcher. Further details about the routing key matcher can be found in the RabbitMQ documentation on topic exchanges.

To bind to an exchange, use $.harbingerjs.amqp.bind (after completing the setup, as above). This example demonstrates connecting and binding to get all audit messages:

$(document).ready(function() {
  $.harbingerjs.amqp.setup({url: $.cometURL});
  $.harbingerjs.amqp.bind("audit","#",
                          function(bindMessage) { ... }, //An optional onBind callback
                          function(unBoundMessage) { ... }); //An optional onUnbound callback

There is also an unbind method which takes an exchange, routing key matcher, and an unbound callback. To unbind from a bound exchange, specify the identical exchange and routing key matcher used for binding:

$.harbingerjs.amqp.unbind("audit","#",
                          function(unBoundMessage) { /* ... callback logic ... */ });

Warning - When reading cometd documentation, you will encounter similar concepts and terms (binding, channels, etc), but this documentation is exclusively referencing the Bridge AMQP service, not cometd.

Callbacks and Listeners

Callbacks are the way to integrate JavaScript functions with real-time data. They could do anything from making an AJAX call to redrawing a graph. Callbacks are added to events within the harbingerjs.amqp library by the addCallback method. It arguments of the type of event and a function to call. The callback types are connect, disconnect, handshake, message, bound, and unbound. Typically, you only use connect and disconnect directly: other forms of callbacks are for messages and bindings. Callback and listener methods can be called as many times as you like, just like the jQuery bind method.

Disconnect and Connect Callbacks

This example demonstrates how and why connect and disconnect are used. All callbacks pass in the message associated with the event as the first argument. It is possible that cometd could disconnect and it is important to give the client feedback when this happens (typically it is because the user credentials have expired and they need to log in again). To accomplish this, add a callback to disconnect:

//... Your document.ready etc
  $.harbingerjs.amqp.addCallback('disconnect',function(message) {
    // You can run this as many times as you want to add a callback for when/if
    // a disconnect happens (i.e., you are no longer getting messages)
    // This can be used to alert a user that or reload the page
    console.log("Real-time data is off.");
  });
//...

Providing feedback about connection status is very useful, especially in conjunction with disconnect. Here is an example that adds a connect callback:

  $.harbingerjs.amqp.addCallback('connect',function(message) {
    // Here is where you can put callbacks around connecting
    // Warning: being connected and being bound are not the same thing!
    console.log("Connected.");
  });

Message Callback (Listener)

The message event type is how an application gets access to real-time messages. Much of the time you will want to use the routing key or exchange of the message to determine which callback should run. There is a message-specific callback method addListener in the library that accomplishes this: addListener takes a callback function (which is passed the message object) and an optional routing key matcher and exchange. This allows you to bind all the messages and applications needs, then build callbacks for each specific message type. Here is an example of addListener:

// Here is a callback that will be run any time you receive a message
$.harbingerjs.amqp.addListener(function(message) { /* ... */ });

// Here is a callback that will fire only when the routing key matcher matches the
// routing key of the incoming message.
$.harbingerjs.amqp.addListener(function(message) { /* ... */ },"my.routing_key.*.matcher");

// Here is a callback that will fire only when the message comes from the audit exchange
$.harbingerjs.amqp.addListener(function(message) { /* ... */ },"#","audit");

Similar to addCallback method, addListener can be called multiple times and all of the given callbacks will be run when they meet the specified conditions.

Bind and Unbind

When an application is driven entirely by real-time messaging, there is often a need to be ensure the application receives all the expected messages before proceeding. In this instance connect is not the ideal event to determine if the application is getting messages. Instead you want to use the bind and unbind events. You can do this in two ways: using addCallback for bind and unbind or adding bind and unbind callbacks when running bind. For the first approach pass a binding object and the callback function to the addCallback method:

var bindingObject = {'exchange':'audit',
                     'routing_key':'#',
                     'fun': function(message) { /* ... my callback ... */ }};
$.harbingerjs.amqp.addCallback('bind',bindingObject);

// The same can be done with the 'unbind' event type
$.harbingerjs.amqp.addCallback('unbind',bindingObject);

For the second, add a bind and unbind callback at the time the bind method is run:

$.harbingerjs.amqp.bind("my_exchange","my.routing.key.matcher",
                        function(bindMessage) { ... }, //An optional onBind callback
                        function(unBoundMessage) { ... }); //An optional onUnbound callback

// There is an unbind method that has a similar structure, the difference being that this
// method does not take a binding callback as you are intentionally sending the unbind
// message to the server.
$.harbingerjs.amqp.unbind("my_exchange","my.routing.key.matcher",
                          function(unBoundMessage) { ... }); //An optional onUnbound callback

Full Example

$(document).ready(function() {
  $.harbingerjs.amqp.addCallback('disconnect',function(message) {
    console.log("Real-time data is off.");
  });

  $.harbingerjs.amqp.addCallback('connect',function(message) {
    console.log("Connected.");
  });

  $.harbingerjs.amqp.setup({url: $.cometURL});

  $.harbingerjs.amqp.addListener(function(message) {}, optional_routing_key_matcher, optional_exchange);
  $.harbingerjs.amqp.bind("my_exchange","my.routing.key.matcher",
                          function(bindMessage) { ... }, //An optional onBind callback
                          function(unBoundMessage) { ... }); //An optional onUnbound callback
});