1. Introduction

1.1 Guide Assumptions

In order to follow this guide, you should:

1.2 Setup

In this tutorial, we will build a minimalist message board that will update itself. Users will be able to add messages to the board, and other users will see those appear are they are created.

To achieve this behaviour, we will use the sync plugin, which you can add to your application with the following command:

$ crails plugins sync install

2. ODB Transactions

The sync plugin provides features to broadcast database transactions to your clients. This chapter will focus on SQL transactions, using both the odb and sync plugins.

2.1 Server

Let's start by generating a resource:

$ crails scaffold resource -n message -p std::string/message std::string/author

2.1.1 Controller

Controllers that interact with an SQL database inherit from the Crails::Odb::Controller class template. To synchronize transactions with the client, we will use the second parameter of that template to overload the default wrapper making for SQL queries with the Crails::Odb::Sync::Connection class:

app/controllers/message.hpp
#pragma once
#include "application.hpp"
#include "app/models/message.hpp"

// Define our controller with synchronization enabled:
typedef Crails::Odb::Controller<
    ApplicationController,
    Crails::Odb::Sync::Connection
  > MessageSuperController;

class MessageController : public MessageSuperController
{
public:
  MessageController(Crails::Context&);

  void initialize() override;
  void finalize() override;

  void index();
  void show();
  void create();
  void update();
  void destroy();
private:
  std::shared_ptr<Message> find_model(Crails::Odb::id_type id);
  void require_model(Crails::Odb::id_type id);
  void find_list();

  std::shared_ptr<Message> model;
  std::vector<Message> model_list;
};

Synchronization are broadcasted through channels. You may perform all synchronizations using a single channel, or even use a channel for each of your model. In this example, we'll set up a channel for our controller:

app/controllers/message.cpp
#include "message.hpp"
#include <crails/odb/to_vector.hpp>
#include "lib/odb/application-odb.hxx"

using namespace std;

MessageController::MessageController(Crails::Context& context) : MessageSuperController(context)
{
  database.sync_transaction.set_channel(
    Crails::Sync::Channels::singleton::get()->require_unlocked_channel("/message/sync")
  );
}

...

2.1.2 Router

Now we just need to make the channel available via the router, so that clients can listen to it using WebSockets:

app/routes.cpp
#include "app/controllers/message.hpp"
#include <crails/router.hpp>
#include <crails/sync/channel_actions.hpp>

void Crails::Router::initialize()
{
  // Append routes here (do not remove this line)
  resource_actions(message, MessageController);
  match_sync_channel("/message/sync", Crails::Sync::ChannelListener);
}
ChannelListener is similar to ChannelClient, but it does not have the ability to broadcast messages: it is read-only.

2.2 Client

We'll now have the message index automatically update when a message is added or updated.

Let's start by updating the index view template. We need to make some changes so that rows can be added and updated from a script:

app/views/message/index.html
#include "app/models/message.hpp"
#include <boost/lexical_cast.hpp>

using namespace std;

vector<Message> @models;
string route = "message";
// END LINKING

<%= tag("a", {{"href", '/' + route + "/new"}}) yields %>
  New
<% end %>

<table class="table">
  <thead>
    <th>Author</th>
    <th>Content</th>
  </thead>
  <tbody>
<% for (const Message& model : models) do %>
    <!-- the [data-id] attribute will help
         our script find which rows to update -->
    <tr data-id="<%= model.get_id() %>">
      <td><%= model.get_author() %></td>
      <td><%= model.get_content() %></td>
    </tr>
<% end %>
  </tbody>
</table>

Now that the view is ready, let's write a script that will listen to our message channel and update the table when it receives notification of new transactions:

app/assets/messages.js
const socket = new WebSocket("ws://0.0.0.0:3001/message/sync");

function updateRow(row, properties) {
  row.children[0].textContent = properties.author;
  row.children[1].textContent = properties.content;
}

function createRow(list, properties) {
  const row = document.createElement("tr");

  row.insertCell(document.createElement("td"));
  row.insertCell(document.createElement("td"));
  updateRow(row, properties;)
  list.insertRow(row);
}

function forEachModel(list, callback) {
  for (let id in list.message)
    callback(id, list.message[id]);
}

socket.onmessage = function (event) {
  const transaction = JSON.parse(event.data);
  const list = document.querySelector("tbody");

  forEachModel(transaction.updates, function(id, properties) {
    const row = list.querySelector(`[data-id="${id}"]`);
    row ? updateRow(row, properties) : createRow(list, properties);
  });

  forEachModel(transaction.removals, function(id, properties) {
    const row = list.querySelector(`[data-id="${id}"]`);
    if (row)
      list.removeChild(row);
  });
}

Now this might need some explaining. As you can see in the onmessage callback, the transaction data is transmitted as JSON. Once the message received by the WebSocket is parsed, the object received will contain two properties: updates and removals. Added and edited models will appear in the former, while deleted models will appear in the former.

Within both updates and removals, models are then sorted by scope. In this example, the Message::scope constant is used, which defaults to message. This is why the forEachModel function loops over list.message.

Each scope itself is a map of all updated or deleted models, where the model id is used as key.

The updates object also provides all the model's attribute, which is how we update or create the cells in the createRow and updateRow functions.

The model properties are rendered to JSON by using the to_json method of your model (if you did not use a scaffold to create your model, don't forget to add that method). Alternatively, if there is a json template matching your model's view constant (Message::view in this exmaple), it will use this view instead.

Our script is not yet included into our index view. Let's fix that:

app/views/message/index.html
#include "lib/assets.hpp" // add the assets include
#include "app/models/message.hpp"
#include <boost/lexical_cast.hpp>

using namespace std;

vector<Message> @models;
string route = "message";
// END LINKING

...

<table class="table">
...
</table>

<%= tag("script", {{"src", Assets::application_js}}) yields %>
<% yields-end %>

And voilĂ , we're done. Build your server and go to http://localhost:3001/message to see the result of your work.

3. Task progression

The sync plugin also provides tools to follow the progress of a task remotely.

Create an application with the following command:

$ crails new -n synctask -p html
$ cd synctask

Add the sync plugin:

$ crails plugins sync install

3.1 Controller

We'll now add a simple controller which will perform an asynchronous task when called upon:

app/controller/task.hpp
#pragma once
#include "application.hpp"
#include <iostream>

class TaskController : public ApplicationController
{
public:
  TaskController(Crails::Context& context) :
    ApplicationController(context)
  {
  }

  void index()
  {
    std::thread(std::bind(run_async_task)).detach();
  }

private:
  static void run_async_task()
  {
    for (int i = 60 ; i > 0 ; --i)
    {
      std::cout << "Incrementing task: " << i << std::endl;
      sleep(1);
    }
  }
};

In the index action, we create an asynchronous task using std::thread. We will now use the Crails::Sync::Task class, which will help us broadcast the progress of the task over WebSockets.

Let's update the index action:

app/controller/task.hpp
#pragma once
#include "application.hpp"
#include <crails/sync/task.hpp> // add the header for Crails::Sync::Task
#include <iostream>

class TaskController : public ApplicationController
{
public:
  TaskController(Crails::Context& context) :
    ApplicationController(context)
  {
  }

  void index()
  {
    int step_count = 60;
    auto task = std::make_shared<Crails::Sync::Task>("task", step_count);

    std::thread(std::bind(run_async_task, task, step_count)).detach();
  }

  ...
};

We instantiate the Task object with two parameters:
- The scope "task", which is used to define the base path on which the progress will be broadcasted.
- The step count, which should match the amount of time you expect to increment the task progress.

Now, let's include the task object in the run_async_task method:

  static void run_async_task(std::shared_ptr<Crails::Sync::Task> task, int step_count)
  {
    for (int i = step_count ; i > 0 ; --i)
    {
      std::cout << "Incrementing task: " << i << std::endl;
      sleep(1);
      task->increment();
    }
  }

A simple call to task->increment() is all it takes on the server part.

3.2 Router

We now have to add routes for both our controller action, and the channel our task progress will be broadcasted to:

app/routes.cpp
#include <crails/router.hpp>
#include <crails/sync/channel_actions.hpp> // don't forget this include
#include "controllers/task.hpp"

void Crails::Router::initialize()
{
  match_action("GET", "/task", TaskController, index);
  match_sync_task("/task/:task_id");
}

The match_sync_task is a macro that creates the required route for task progress broadcasting. It's composed of the task's scope and its id as a parameter.

3.3 View

Now we'll create a view to display our task progress. Let's start by adding a Bootstrap layout:

$ crails scaffold layout --toolkit bootstrap

Then create the following view:

app/views/task/show.html
std::string @task_uri;
std::string @task_id;
// END LINKING
<h1>Task Watcher</h1>
<h3>Watching task <%= task_id %></h3>

<div class="progress">
  <div id="progress-bar" class="progress-bar" role="progressbar">
    0/0
  </div>
</div>

<script>
  const socket = new WebSocket("ws://0.0.0.0:3001/<%= task_uri %>");
  socket.onmessage = event => {
    const metadata = JSON.parse(event.data);
    const percents = metadata.progress * 100;
    const progressBar = document.querySelector("#progress-bar");

    progressBar.textContent = `${metadata.item_progress}/${metadata.item_count}`;
    progressBar.style.width = `${Math.ceil(percents)}%`;
  };
</script>

We use a bootstrap progressbar to display the task progress, and a WebSocket to receive the udpates. The update messages are in JSON, and feature a progress value that goes from 0 to 1, as well as counters for the amount of tasks done, and the total amount of task to complete.

We will now update the index action in our controller to provide the task_uri and task_uid shared variable that our view is expecting:

app/controllers/task.hpp
  void index()
  {
    int step_count = 60;
    auto task = std::make_shared<Crails::Sync::Task>("task", step_count);

    std::thread(std::bind(run_async_task, task, step_count)).detach();
    vars["layout"] = std::string("layouts/application");
    vars["task_uri"] = task->uri();
    vars["task_id"] = task->get_id();
    render("task/show");
  }

Don't forget to also set the view layout to layouts/application.

Build and launch your server, and then go to http://localhost:3001/task: you should see the task slowly update every 3 seconds or so.

3.4 Customization

3.4.1 Update broadcasting rate

The updates happen slowly due to the notification policy of the Task object. To avoid firing too many notifications, the Task object only notify clients of progresses by blocks of 5 percents by default.

Let's change our index action to use a different update rate:

  void index()
  {
    int step_count = 60;
    auto task = std::make_shared<Crails::Sync::Task>("task", step_count);

    task->set_notification_step(1);
    ...
  }

By calling set_notification_step, we tell our notifier to broadcast update everytime the progress moves by 1 percent.

3.4.2 Custom notification messages

You may also manually broadcast an update whenever you want by calling the notify method:

  static void run_async_task(std::shared_ptr<Crails::Sync::Task> task, int task_count)
  {
    ...
    task->notify();
    ...
  }

You may also add your own message to the broadcasted update:

  task->notify("Hello world !");

The client WebSocket will be able to access this message in the JSON data broadcasted, such as:

  socket.onmessage = function (event) {
    const data = JSON.parse(event.data);
    console.log("Received task update with message:", data.message);
  };