313 lines
11 KiB
JavaScript
313 lines
11 KiB
JavaScript
|
// 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;
|
||
|
}
|
||
|
|