codimd/lib/ot/client.js

313 lines
11 KiB
JavaScript
Executable File

// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala
if (typeof ot === 'undefined') {
var ot = {};
}
ot.Client = (function (global) {
'use strict';
// Client constructor
function Client (revision) {
this.revision = revision; // the next expected revision number
this.setState(synchronized_); // start state
}
Client.prototype.setState = function (state) {
this.state = state;
};
// Call this method when the user changes the document.
Client.prototype.applyClient = function (operation) {
this.setState(this.state.applyClient(this, operation));
};
// Call this method with a new operation from the server
Client.prototype.applyServer = function (revision, operation) {
this.setState(this.state.applyServer(this, revision, operation));
};
Client.prototype.applyOperations = function (head, operations) {
this.setState(this.state.applyOperations(this, head, operations));
};
Client.prototype.serverAck = function (revision) {
this.setState(this.state.serverAck(this, revision));
};
Client.prototype.serverReconnect = function () {
if (typeof this.state.resend === 'function') { this.state.resend(this); }
};
// Transforms a selection from the latest known server state to the current
// client state. For example, if we get from the server the information that
// another user's cursor is at position 3, but the server hasn't yet received
// our newest operation, an insertion of 5 characters at the beginning of the
// document, the correct position of the other user's cursor in our current
// document is 8.
Client.prototype.transformSelection = function (selection) {
return this.state.transformSelection(selection);
};
// Override this method.
Client.prototype.sendOperation = function (revision, operation) {
throw new Error("sendOperation must be defined in child class");
};
// Override this method.
Client.prototype.applyOperation = function (operation) {
throw new Error("applyOperation must be defined in child class");
};
// In the 'Synchronized' state, there is no pending operation that the client
// has sent to the server.
function Synchronized () {}
Client.Synchronized = Synchronized;
Synchronized.prototype.applyClient = function (client, operation) {
// When the user makes an edit, send the operation to the server and
// switch to the 'AwaitingConfirm' state
client.sendOperation(client.revision, operation);
return new AwaitingConfirm(operation);
};
Synchronized.prototype.applyServer = function (client, revision, operation) {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// When we receive a new operation from the server, the operation can be
// simply applied to the current document
client.applyOperation(operation);
return this;
};
Synchronized.prototype.serverAck = function (client, revision) {
throw new Error("There is no pending operation.");
};
// Nothing to do because the latest server state and client state are the same.
Synchronized.prototype.transformSelection = function (x) { return x; };
// Singleton
var synchronized_ = new Synchronized();
// In the 'AwaitingConfirm' state, there's one operation the client has sent
// to the server and is still waiting for an acknowledgement.
function AwaitingConfirm (outstanding) {
// Save the pending operation
this.outstanding = outstanding;
}
Client.AwaitingConfirm = AwaitingConfirm;
AwaitingConfirm.prototype.applyClient = function (client, operation) {
// When the user makes an edit, don't send the operation immediately,
// instead switch to 'AwaitingWithBuffer' state
return new AwaitingWithBuffer(this.outstanding, operation);
};
AwaitingConfirm.prototype.applyServer = function (client, revision, operation) {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// This is another client's operation. Visualization:
//
// /\
// this.outstanding / \ operation
// / \
// \ /
// pair[1] \ / pair[0] (new outstanding)
// (can be applied \/
// to the client's
// current document)
var pair = operation.constructor.transform(this.outstanding, operation);
client.applyOperation(pair[1]);
return new AwaitingConfirm(pair[0]);
};
AwaitingConfirm.prototype.serverAck = function (client, revision) {
if (revision - client.revision > 1) {
return new Stale(this.outstanding, client, revision).getOperations();
}
client.revision = revision;
// The client's operation has been acknowledged
// => switch to synchronized state
return synchronized_;
};
AwaitingConfirm.prototype.transformSelection = function (selection) {
return selection.transform(this.outstanding);
};
AwaitingConfirm.prototype.resend = function (client) {
// The confirm didn't come because the client was disconnected.
// Now that it has reconnected, we resend the outstanding operation.
client.sendOperation(client.revision, this.outstanding);
};
// In the 'AwaitingWithBuffer' state, the client is waiting for an operation
// to be acknowledged by the server while buffering the edits the user makes
function AwaitingWithBuffer (outstanding, buffer) {
// Save the pending operation and the user's edits since then
this.outstanding = outstanding;
this.buffer = buffer;
}
Client.AwaitingWithBuffer = AwaitingWithBuffer;
AwaitingWithBuffer.prototype.applyClient = function (client, operation) {
// Compose the user's changes onto the buffer
var newBuffer = this.buffer.compose(operation);
return new AwaitingWithBuffer(this.outstanding, newBuffer);
};
AwaitingWithBuffer.prototype.applyServer = function (client, revision, operation) {
if (revision - client.revision > 1) {
throw new Error("Invalid revision.");
}
client.revision = revision;
// Operation comes from another client
//
// /\
// this.outstanding / \ operation
// / \
// /\ /
// this.buffer / \* / pair1[0] (new outstanding)
// / \/
// \ /
// pair2[1] \ / pair2[0] (new buffer)
// the transformed \/
// operation -- can
// be applied to the
// client's current
// document
//
// * pair1[1]
var transform = operation.constructor.transform;
var pair1 = transform(this.outstanding, operation);
var pair2 = transform(this.buffer, pair1[1]);
client.applyOperation(pair2[1]);
return new AwaitingWithBuffer(pair1[0], pair2[0]);
};
AwaitingWithBuffer.prototype.serverAck = function (client, revision) {
if (revision - client.revision > 1) {
return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations();
}
client.revision = revision;
// The pending operation has been acknowledged
// => send buffer
client.sendOperation(client.revision, this.buffer);
return new AwaitingConfirm(this.buffer);
};
AwaitingWithBuffer.prototype.transformSelection = function (selection) {
return selection.transform(this.outstanding).transform(this.buffer);
};
AwaitingWithBuffer.prototype.resend = function (client) {
// The confirm didn't come because the client was disconnected.
// Now that it has reconnected, we resend the outstanding operation.
client.sendOperation(client.revision, this.outstanding);
};
function Stale(acknowlaged, client, revision) {
this.acknowlaged = acknowlaged;
this.client = client;
this.revision = revision;
}
Client.Stale = Stale;
Stale.prototype.applyClient = function (client, operation) {
return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
};
Stale.prototype.applyServer = function (client, revision, operation) {
throw new Error("Ignored server-side change.");
};
Stale.prototype.applyOperations = function (client, head, operations) {
var transform = this.acknowlaged.constructor.transform;
for (var i = 0; i < operations.length; i++) {
var op = ot.TextOperation.fromJSON(operations[i]);
var pair = transform(this.acknowlaged, op);
client.applyOperation(pair[1]);
this.acknowlaged = pair[0];
}
client.revision = this.revision;
return synchronized_;
};
Stale.prototype.serverAck = function (client, revision) {
throw new Error("There is no pending operation.");
};
Stale.prototype.transformSelection = function (selection) {
return selection;
};
Stale.prototype.getOperations = function () {
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
return this;
};
function StaleWithBuffer(acknowlaged, buffer, client, revision) {
this.acknowlaged = acknowlaged;
this.buffer = buffer;
this.client = client;
this.revision = revision;
}
Client.StaleWithBuffer = StaleWithBuffer;
StaleWithBuffer.prototype.applyClient = function (client, operation) {
var buffer = this.buffer.compose(operation);
return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
};
StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
throw new Error("Ignored server-side change.");
};
StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
var transform = this.acknowlaged.constructor.transform;
for (var i = 0; i < operations.length; i++) {
var op = ot.TextOperation.fromJSON(operations[i]);
var pair1 = transform(this.acknowlaged, op);
var pair2 = transform(this.buffer, pair1[1]);
client.applyOperation(pair2[1]);
this.acknowlaged = pair1[0];
this.buffer = pair2[0];
}
client.revision = this.revision;
client.sendOperation(client.revision, this.buffer);
return new AwaitingConfirm(this.buffer);
};
StaleWithBuffer.prototype.serverAck = function (client, revision) {
throw new Error("There is no pending operation.");
};
StaleWithBuffer.prototype.transformSelection = function (selection) {
return selection;
};
StaleWithBuffer.prototype.getOperations = function () {
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
return this;
};
return Client;
}(this));
if (typeof module === 'object') {
module.exports = ot.Client;
}