Coact wasn't launched and isn't currently under development. This post is simply here for archival purposes and general interest. We may come back to this in the future.

One of the things I am most proud and happy with in Coact so-far is the way that data updates made on the server are reflected in real time in the browser. For example, if someone renames a client, the new name will be pushed to out to all browsers who are looking at the client. It's really rather sexy and in this post I'm going to explain exactly how it works - it's a bit technical but hopefully not too bad!

Let's start with RabbitMQ

To begin, we've set up a RabbitMQ server and created a new direct exchange which will receive information every time an object is updated. When a trackable object is created, updated or destroyed we send a message (containing details of the changes) to the exchange with the object's UUID as the routing key.

This provides us a way to subscribe to the changes made on any object in our database. If we want a live stream of changes, we can just subscribe to the objects queue with the routing key of the object whos changes we want to follow.

Configuring our models

We want changes to be pushed out automatically every time an instance of a model is created, updated or deleted. To achieve this, we've created an ActiveRecord extension with a nice little DSL. Here's an example from our Project model in Coact.

class Project < ActiveRecord::Base
    publish do
    # When a new client is created, we want to notify its client so it also receives a message
    # to let it know a new project has been created.
    notify :client, :on => [:create]

    # Defines a list of attributes to follow the changes of. Any changes to these attributes will 
    # be sent to our message queue.
    attributes :name, :color, :status, :reference, :description, :project_manager_id, :client_id, :updated_at

    # Some of the attributes above are actually methods and therefore will never change and therefore
    # will be sent when any of the attributes below are changed.
    track_method :color, :attributes => [:name]
    track_method :project_manager, :attributes => [:project_manager_id]
    track_method :client, :attributes => [:client_id]

    # This defines whether the given user has permission to subscribe to receive updates for
    # this object.
    permission { |user| user.can_view_project?(self) }
  end
end

Our extension now automatically handles despatching a small JSON payload each time an object is changed. This payload includes the details of the object which changed, the type of change which occurred plus any changes to tracked attributes. It also includes an srid which stands for Source Request ID which is the ID of the HTTP request (if any) which triggered the model to be updated. Here's an example payload:

{
  "srid":"94451954-9e87-4fa6-a95a-6a8a645b6744",
  "source":{
    "id":"dd3d591a-2437-40a7-b6e2-2aebfad23189",
    "type":"Project"
  },
  "action":"update",
  "changes":{
    "name":["Old Name", "New Name"],
    "color":["#abcabc", "#efefef"]
  }
}

This payload includes everything needed by a web browser to ensure that what a user is seeing in their browser matches what is stored in the database at any moment.

Web Sockets

Naturally, we're using web sockets to allow browsers to subscribe to these object feeds. We're written our own small web socket server (loosely based on the same system we use for Viaduct WebPush. The reason we've written our own rather than using a service like WebPush is because we're planning to offer Coact as an Enterprise product and therefore dependencies on external service provides needs to be reduced everywhere possible.

When a user connects to our web socket server from their browser they send their session ID which we use to authenticate them. Once connected, they can send requests to subscribe to any object they have permission to view. For example, to subscribe to the project shown above, the browser will send a payload like this:

{
  "type":"subscribe",
  "exchange":"objects",
  "routing_key":"dd3d591a-2437-40a7-b6e2-2aebfad23189"
}

The server will then respond with an acknowledgement or an error. If the user has access to the object matching the UUID provided, it's likely the subscription will be successful. Once subscribed, any changes to this object will now be sent straight through to the browser and can be acted upon as needed.

Subscribing & Updating the DOM

We can now subscribe to objects but we need to be able to tell our javascript to do this whenever an object's data is shown on a page. Here's some example markup for a view which shows project information.

<div data-track-object="<%= @project.id %>">
  <dl>
    <dt>Name</dt>
    <dd data-track-field="name"><%= @project.name %></dd>
  </dl>
</div>

You'll see in here that we have added a data-track-object attribute containing the project's ID to the div surrounding our project information. This tells our javascript that we need to subscribe to this project's information and attributes within it should be updated.

You'll also notice a data-track-field attribute on the <dd> tag. This tells our javascript the contents of this element should be replaced with any new values for the name attribute.

This is the most basic form of update - simple text. We also have the option to use a data-track-type attribute to specify what kind of update should be made to the element in question. We currently support the following types:

  • email - sets the content of the field to new value and updates the href to include the e-mail address after prepending mailto:.
  • url - as with email but doesn't prepend the mailto:
  • borderColor - sets the object's border color to the new value
  • image - updates the src attribute with the new URL
  • html - changes the inner HTML of the element
  • livestamp - updates the value and runs it through the livestamp prettifier

That's about it.

It's quite a complex system but the results are pretty impressive. We don't think too many users will even notice this happening but Coact is all about the attention to detail and this feature was a must have for me.

Tell us how you feel about this post?