Real time Collaboration - audreyt/socialcalc GitHub Wiki

Real-time Collaboration

The next example we'll explore is multi-user, real-time editing on a shared spreadsheet.

This may seem complicated at first, but thanks to SocialCalc's modular design, all it takes is for each on-line user to broadcast their commands to other participiants.

To distinguish between locally-issued commands and remote commands, we add an isRemote parameter to the ScheduleSheetCommands method:

SocialCalc.ScheduleSheetCommands = function(sheet, cmdstr, saveundo, isRemote) {
   if (SocialCalc.Callbacks.broadcast && !isRemote) {
       SocialCalc.Callbacks.broadcast('execute', { cmdstr: cmdstr, saveundo: saveundo });
   }
   // ...original ScheduleSheetCommands code here...
}

Now all we need to do is to define a suitable SocialCalc.Callbacks.broadcast callback function. Once it's in place, the same commands will be executed on all users connected to the same spreadsheet.

When this feature was first implemented for the OLPC by SEETA's Sugar Labs in 2009, the broadcast function was built with XPCOM calls into D-Bus/Telepathy, the standard transport for OLPC/Sugar networks:

That worked reasonably well, enabling XO instances in the same Sugar network to collaborate on a common SocialCalc spreadsheet. However, it is both specific to the Mozilla/XPCOM browser platform, as well as to the D-Bus/Telepathy messaging platform.

Cross-browser Transport

To make this work across browsers and operating systems, we use the Web::Hippie framework, a high-level abstraction of JSON-over-WebSocket with convenient jQuery bindings, with MXHR (multipart XMLHttpRequest) as the fallback transport mechanism if WebSocket is not available.

For browsers with Adobe Flash plugin installed but without native WebSocket support, we use the web_socket.js project's Flash emulation of WebSocket, which is often faster and more reliable than MXHR.

The operation flow looks like this:

The client-side SocialCalc.Callbacks.broadcast function is defined as:

var hpipe = new Hippie.Pipe();

SocialCalc.Callbacks.broadcast = function(type, data) {
    hpipe.send({ type: type, data: data });
};

$(hpipe).bind("message.execute", function (e, d) {
    SocialCalc.CurrentSpreadsheetControlObject.context.sheetobj.ScheduleSheetCommands(
        d.data.cmdstr,
        d.data.saveundo,
        true // isRemote = true
    );
    break;
});

Although this works quite well, there are still two remaining issues to resolve.

Conflict Resolution

The first one is a race-condition in the order of commands executed: If users A and B simultaneously perform an operation affecting the same cells, then receive and execute commands broadcast from the other user, they will end up in different states:

We can resolve this with SocialCalc's built-in undo/redo mechanism, as shown in the diagram here:

  • When a client broadcasts a command, it adds the command to a pending queue.

  • When a client receives a command, it checks the remote command against the pending queue:

    • If the pending queue is empty, then the command is simply executed as a remote action.

    • If it matches a command in the pending queue, then the local command is removed from the queue.

    • Otherwise, the client checks if there's any queued commands that conflicts with the received command:

      • If there are conflicting commands, the client first Undo those commands and mark them for later Redo.

      • After undoing the conflicting commands (if any), the remote command is executed as usual.

  • When a marked-for-redo command is received from the server, the client will execute it again, then remove it from the queue.

Remote Cursors

Even with race conditions resolved, it is still suboptimal to accidentally overwrite the cell another user is currently editing. A simple improvement is for each client to broadcast its cursor position to other users, so everyone can see which cells are being worked on.

To implement this idea, we add another broadcast handler to the MoveECellCallback event:

editor.MoveECellCallback.broadcast = function(e) {
    hpipe.send({ type: 'ecell', data: e.ecell.coord });
};

$(hpipe).bind("message.ecell", function (e, d) {
    var cr = SocialCalc.coordToCr(d.data);
    var cell = SocialCalc.GetEditorCellElement(editor, cr.row, cr.col);
    // ...decorate cell with styles specific to the remote user(s) on it...
});

To mark cell focus in spreadsheets, it's common to use colored borders. However, a cell may already define its own border property, and since border is mono-colored, it can only represent one cursor on the same cell.

Therefore, on browsers with CSS3 support, we use the box-shadow property to represent peer cursors:

/* Two cursors on the same cell */
box-shadow: inset 0 0 0 4px red, inset 0 0 0 2px green;

With four people editing on the same spreadsheet, the screen would look like this:

>>> Lessons Learned

⚠️ **GitHub.com Fallback** ⚠️