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