canop
Transport-agnostic operation-rich intention-preserving client-server
JSON synchronization protocol.
Work in progress. Status: alpha.
Currently only supports string values.
A simple client-server example is available,
using adapters for WebSocket and
CodeMirror.
Exploratory API description:
var canop = require('canop');
var server = new canop.Server({ data: {some: 'data'} });
var client = new canop.Client({ send: function(message) {} });
server.addClient({
send: function(message) { client.receive(message); },
onReceive: function(receive) { client.send = receive; },
});
client.emit('syncing');
client.get(['some']);
client.add(['some'], 0, 'modified ');
client.move([], 'some', ['final']);
client.on('signal', function(event) { event.clientId, event.data });
client.signal({ name: 'Grace', focus: ['some'], sel: [[9,9]] });
client.clientCount
var changeListener = function(event) {};
client.on('change', changeListener);
server.on('change', changeListener);
client.on('localChange', changeListener);
client.on('update', function(updated) {}, {path: ['some']});
client.on('localUpdate', function(updated) {}, {path: ['some']});
client.removeListener('change', changeListener);
client.undo()
client.redo()
client.on('synced', function() {});
client.on('syncing', function() {});
client.on('unsyncable', function() {});
canop.changePosition(position, posChanges, bestGuess);
var actionType = client.registerAction(
canop.type.list | canop.type.string,
function action(object, params) {},
function reverse(change) {});
client.act([actionType, path, …params]);
client.act([client.action.set, ['final'], 'the end.']);
client.actAtomically([
[client.action.stringAdd, ['some'], 0, 'modified'],
[client.action.objectMove, [], 'some', ['final']],
]);
Pros
- Operational Transformations minimizes the number of UI operations at the
cost of tremendous implementation complexity that rises exponentially with the
number of operations it supports.
- CRDTs tend to use a lot of memory, and require tricky garbage collection
to avoid bloat. Canop does not suffer from memory bloat. Canonical operations
are immutable, and so, can be substituted for the equivalent string.
Furthermore, CRDT reads require more computation for complex structures. CRDTs
are, however, great for peer-to-peer synchronization.
- Rebase-sync requires local changes to be rebased by changes that the
server has accepted, similar to our design. However, it denies changes that
are not rebased, causing the potential for long-term divergence if changes
happen faster than a client-server round-trip, and preventing the "every
individual key press appears instantly" experience that has become concurrent
live editing's signature. Canop operations are immediately rebased and
accepted by the server.
Limitations
Out of the box, Canop does not support peer-to-peer editing. If the
probability of a central server crashing is at odds with your availability
requirements, that is not a worry, as you can easily add a server fallback
mechanism. On the other hand, if the frequency of edits goes beyond what a
single server can handle, the algorithm can be tweaked to be peer-to-peer (at
the expense of intention preservation). A description of that algorithm will be
available in the paper. You may contribute a patch that implements this
algorithm.
Atomic transactions will usually keep their meaning thanks
to Canop's intention preservation system. However, it is not guaranteed. For
instance, assuming we store the money of two bank accounts in a list. We encode
a transaction between them with two operations, add and remove: [20, 50]
① →
[15, 50]
② → [15, 55]
. We encode a swap between then with two operations
that set their values: [20, 50]
③ → [20, 20]
④ → [50, 20]
. If those
compound operations happen concurrently, they can converge to the following
sequence: [20, 50]
① → [15, 50]
③ → [15, 20]
② → [15, 25]
④ → [50, 25]
. Semantically, it should converge to [55, 15]
, which is clearly not the
case; one account did not receive its money, and the other received money that
was not even in the system.
Use client.actAtomically()
to send operations that are part of an atomic
transaction in bulk, ensuring that no operation will be executed in the middle, and
that the data will never be read within operations.
Alternatively, you may add a custom atomic operation (once the primitives for
that are implemented).
Contributing
git clone https://github.com/espadrine/canop.git
cd canop
make
TODO
- Textarea adapter
- Customizable UI sync debouncing
- JSON-compatible protocol
- Array index rebasing
- Autosave of operations to disk
- Allow creating user-defined operations