simple conflicts are now handled correctly
This commit is contained in:
		
							parent
							
								
									9d0373b85b
								
							
						
					
					
						commit
						bf4d5f24a8
					
				@ -23,6 +23,7 @@
 | 
			
		||||
    "GeneratorFunction": true,
 | 
			
		||||
    "Y": true,
 | 
			
		||||
    "setTimeout": true,
 | 
			
		||||
    "setInterval": true
 | 
			
		||||
    "setInterval": true,
 | 
			
		||||
    "Operation": true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -69,11 +69,11 @@ class Test extends AbstractConnector {
 | 
			
		||||
    this.globalRoom = globalRoom;
 | 
			
		||||
  }
 | 
			
		||||
  send (userId, message) {
 | 
			
		||||
    globalRoom.buffers[userId].push([this.userId, message]);
 | 
			
		||||
    globalRoom.buffers[userId].push(JSON.parse(JSON.stringify([this.userId, message])));
 | 
			
		||||
  }
 | 
			
		||||
  broadcast (message) {
 | 
			
		||||
    for (var key in globalRoom.buffers) {
 | 
			
		||||
      globalRoom.buffers[key].push([this.userId, message]);
 | 
			
		||||
      globalRoom.buffers[key].push(JSON.parse(JSON.stringify([this.userId, message])));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  disconnect () {
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,10 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars
 | 
			
		||||
      yield* this.setOperation(op);
 | 
			
		||||
      this.store.operationAdded(op);
 | 
			
		||||
      return true;
 | 
			
		||||
    } else {
 | 
			
		||||
    } else if (op.id[1] < state.clock) {
 | 
			
		||||
      return false;
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new Error("Operations must arrive in order!");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -45,7 +47,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
 | 
			
		||||
       * sid : String (converted from id via JSON.stringify
 | 
			
		||||
                       so we can use it as a property name)
 | 
			
		||||
 | 
			
		||||
      Always remember to first overwrite over
 | 
			
		||||
      Always remember to first overwrite
 | 
			
		||||
      a property before you iterate over it!
 | 
			
		||||
    */
 | 
			
		||||
  }
 | 
			
		||||
@ -56,16 +58,15 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
 | 
			
		||||
    for (var key in ops) {
 | 
			
		||||
      var o = ops[key];
 | 
			
		||||
      var required = Y.Struct[o.struct].requiredOps(o);
 | 
			
		||||
      this.whenOperationsExist(required, Y.Struct[o.struct].execute, o);
 | 
			
		||||
      this.whenOperationsExist(required, o);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // f is called as soon as every operation requested is available.
 | 
			
		||||
  // op is executed as soon as every operation requested is available.
 | 
			
		||||
  // Note that Transaction can (and should) buffer requests.
 | 
			
		||||
  whenOperationsExist (ids : Array<Id>, f : GeneratorFunction, args : Array<any>) {
 | 
			
		||||
  whenOperationsExist (ids : Array<Id>, op : Operation) {
 | 
			
		||||
    if (ids.length > 0) {
 | 
			
		||||
      let listener : Listener = {
 | 
			
		||||
        f: f,
 | 
			
		||||
        args: args || [],
 | 
			
		||||
        op: op,
 | 
			
		||||
        missing: ids.length
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
@ -81,8 +82,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      this.listenersByIdExecuteNow.push({
 | 
			
		||||
        f: f,
 | 
			
		||||
        args: args || []
 | 
			
		||||
        op: op
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -103,8 +103,8 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
 | 
			
		||||
      store.listenersByIdRequestPending = false;
 | 
			
		||||
 | 
			
		||||
      for (let key in exeNow) {
 | 
			
		||||
        let listener = exeNow[key];
 | 
			
		||||
        yield* listener.f.call(this, listener.args);
 | 
			
		||||
        let o = exeNow[key].op;
 | 
			
		||||
        yield* Struct[o.struct].execute.call(this, o);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (var sid in ls){
 | 
			
		||||
@ -115,8 +115,9 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
 | 
			
		||||
        } else {
 | 
			
		||||
          for (let key in l) {
 | 
			
		||||
            let listener = l[key];
 | 
			
		||||
            let o = listener.op;
 | 
			
		||||
            if (--listener.missing === 0){
 | 
			
		||||
              yield* listener.f.call(this, listener.args);
 | 
			
		||||
              yield* Struct[o.struct].execute.call(this, o);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
@ -125,13 +126,16 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
 | 
			
		||||
  }
 | 
			
		||||
  // called by a transaction when an operation is added
 | 
			
		||||
  operationAdded (op) {
 | 
			
		||||
    var sid = JSON.stringify(op.id);
 | 
			
		||||
    var l = this.listenersById[sid];
 | 
			
		||||
    delete this.listenersById[sid];
 | 
			
		||||
 | 
			
		||||
    // notify whenOperation listeners (by id)
 | 
			
		||||
    var l = this.listenersById[JSON.stringify(op.id)];
 | 
			
		||||
    if (l != null) {
 | 
			
		||||
      for (var key in l){
 | 
			
		||||
        var listener = l[key];
 | 
			
		||||
        if (--listener.missing === 0){
 | 
			
		||||
          this.whenOperationsExist([], listener.f, listener.args);
 | 
			
		||||
          this.whenOperationsExist([], listener.op);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -2,49 +2,4 @@
 | 
			
		||||
/*eslint-env browser,jasmine,console */
 | 
			
		||||
 | 
			
		||||
describe("OperationStore", function() {
 | 
			
		||||
 | 
			
		||||
  class OperationStore extends AbstractOperationStore {
 | 
			
		||||
    constructor (){
 | 
			
		||||
      super();
 | 
			
		||||
    }
 | 
			
		||||
    requestTransaction (makeGen) {
 | 
			
		||||
      var gen = makeGen.apply({
 | 
			
		||||
        getOperation: function*(){
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      function handle(res : any){
 | 
			
		||||
        if (res.done){
 | 
			
		||||
          return;
 | 
			
		||||
        } else {
 | 
			
		||||
          handle(gen.next(res.value));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      handle(gen.next());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var os = new OperationStore();
 | 
			
		||||
 | 
			
		||||
  it("calls when operation added", function(done) {
 | 
			
		||||
    var id = ["u1", 1];
 | 
			
		||||
    os.whenOperationsExist([id], function*(){
 | 
			
		||||
      expect(true).toEqual(true);
 | 
			
		||||
      done();
 | 
			
		||||
    });
 | 
			
		||||
    os.operationAdded({id: id});
 | 
			
		||||
  });
 | 
			
		||||
  it("calls when no requirements", function(done) {
 | 
			
		||||
    os.whenOperationsExist([], function*(){
 | 
			
		||||
      expect(true).toEqual(true);
 | 
			
		||||
      done();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  it("calls when no requirements with arguments", function(done) {
 | 
			
		||||
    os.whenOperationsExist([], function*(arg){
 | 
			
		||||
      expect(arg).toBeTruthy();
 | 
			
		||||
      done();
 | 
			
		||||
    }, [true]);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -84,6 +84,7 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
 | 
			
		||||
        for (var clock = startPos; clock <= endPos; clock++) {
 | 
			
		||||
          var op = yield* this.getOperation([user, clock]);
 | 
			
		||||
          if (op != null) {
 | 
			
		||||
            op = Struct[op.struct].encode(op);
 | 
			
		||||
            ops.push(yield* this.makeOperationReady.call(this, startSS, op));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
@ -91,6 +92,7 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
 | 
			
		||||
      return ops;
 | 
			
		||||
    }
 | 
			
		||||
    *makeOperationReady (ss, op) {
 | 
			
		||||
      // instead of ss, you could use currSS (a ss that increments when you add an operation)
 | 
			
		||||
      var clock;
 | 
			
		||||
      var o = op;
 | 
			
		||||
      while (true){
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										141
									
								
								src/Struct.js
									
									
									
									
									
								
							
							
						
						
									
										141
									
								
								src/Struct.js
									
									
									
									
									
								
							@ -40,10 +40,12 @@ var Struct = {
 | 
			
		||||
      var user = this.store.y.connector.userId;
 | 
			
		||||
      var state = yield* this.getState(user);
 | 
			
		||||
      op.id = [user, state.clock];
 | 
			
		||||
      yield* this.addOperation(op);
 | 
			
		||||
      if ((yield* this.addOperation(op)) === false) {
 | 
			
		||||
        throw new Error("This is highly unexpected :(");
 | 
			
		||||
      }
 | 
			
		||||
      this.store.y.connector.broadcast({
 | 
			
		||||
        type: "update",
 | 
			
		||||
        ops: [op]
 | 
			
		||||
        ops: [Struct[op.struct].encode(op)]
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
@ -67,16 +69,18 @@ var Struct = {
 | 
			
		||||
      yield* Struct.Operation.create.call(this, op);
 | 
			
		||||
 | 
			
		||||
      if (op.left != null) {
 | 
			
		||||
        op.left.right = op.id;
 | 
			
		||||
        yield* this.setOperation(op.left);
 | 
			
		||||
        var left = yield* this.getOperation(op.left);
 | 
			
		||||
        left.right = op.id;
 | 
			
		||||
        yield* this.setOperation(left);
 | 
			
		||||
      }
 | 
			
		||||
      if (op.right != null) {
 | 
			
		||||
        op.right.left = op.id;
 | 
			
		||||
        yield* this.setOperation(op.right);
 | 
			
		||||
        var right = yield* this.getOperation(op.right);
 | 
			
		||||
        right.left = op.id;
 | 
			
		||||
        yield* this.setOperation(right);
 | 
			
		||||
      }
 | 
			
		||||
      var parent = yield* this.getOperation(op.parent);
 | 
			
		||||
      if (op.parentSub != null){
 | 
			
		||||
        if (compareIds(parent.map[op.parentSub], op.left)) {
 | 
			
		||||
        if (compareIds(parent.map[op.parentSub], op.right)) {
 | 
			
		||||
          parent.map[op.parentSub] = op.id;
 | 
			
		||||
          yield* this.setOperation(parent);
 | 
			
		||||
        }
 | 
			
		||||
@ -95,6 +99,22 @@ var Struct = {
 | 
			
		||||
      }
 | 
			
		||||
      return op;
 | 
			
		||||
    },
 | 
			
		||||
    encode: function(op){
 | 
			
		||||
      /*var e = {
 | 
			
		||||
        id: op.id,
 | 
			
		||||
        left: op.left,
 | 
			
		||||
        right: op.right,
 | 
			
		||||
        origin: op.origin,
 | 
			
		||||
        parent: op.parent,
 | 
			
		||||
        content: op.content,
 | 
			
		||||
        struct: "Insert"
 | 
			
		||||
      };
 | 
			
		||||
      if (op.parentSub != null){
 | 
			
		||||
        e.parentSub = op.parentSub;
 | 
			
		||||
      }
 | 
			
		||||
      return e;*/
 | 
			
		||||
      return op;
 | 
			
		||||
    },
 | 
			
		||||
    requiredOps: function(op){
 | 
			
		||||
      var ids = [];
 | 
			
		||||
      if(op.left != null){
 | 
			
		||||
@ -133,39 +153,11 @@ var Struct = {
 | 
			
		||||
    #         $this insert_position is to the left of $o (forever!)
 | 
			
		||||
    */
 | 
			
		||||
    execute: function*(op){
 | 
			
		||||
      var distanceToOrigin = yield* Struct.Insert.getDistanceToOrigin.call(this, op); // most cases: 0 (starts from 0)
 | 
			
		||||
      var i = distanceToOrigin; // loop counter
 | 
			
		||||
      var o, tmp;
 | 
			
		||||
      if (op.right == null && op.left == null) {
 | 
			
		||||
       var p = yield* this.getOperation(op.parent);
 | 
			
		||||
       if (op.parentSub != null) {
 | 
			
		||||
         tmp = p.map[op.parentSub];
 | 
			
		||||
         if (!compareIds(tmp, op.id)) {
 | 
			
		||||
           op.right = tmp;
 | 
			
		||||
         }
 | 
			
		||||
         if (op.right == null) {
 | 
			
		||||
           // this is the first ins in parent
 | 
			
		||||
           p.map[op.parentSub] = op.id;
 | 
			
		||||
           yield* this.setOperation(p);
 | 
			
		||||
           yield* this.setOperation(op);
 | 
			
		||||
           return;
 | 
			
		||||
         }
 | 
			
		||||
       } else {
 | 
			
		||||
         tmp = p.start;
 | 
			
		||||
         if (!compareIds(tmp, op.id)) {
 | 
			
		||||
           op.left = tmp;
 | 
			
		||||
         }
 | 
			
		||||
         if (op.left == null) {
 | 
			
		||||
           // this is the first ins in parent
 | 
			
		||||
           p.start = op.id;
 | 
			
		||||
           p.end = op.id;
 | 
			
		||||
           yield* this.setOperation(p);
 | 
			
		||||
           yield* this.setOperation(op);
 | 
			
		||||
           return;
 | 
			
		||||
         }
 | 
			
		||||
       }
 | 
			
		||||
      }
 | 
			
		||||
      var i; // loop counter
 | 
			
		||||
      var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op); // most cases: 0 (starts from 0)
 | 
			
		||||
      var o;
 | 
			
		||||
 | 
			
		||||
      // find o. o is the first conflicting operation
 | 
			
		||||
      if (op.left != null) {
 | 
			
		||||
        o = yield* this.getOperation(op.left);
 | 
			
		||||
        o = yield* this.getOperation(o.right);
 | 
			
		||||
@ -174,18 +166,28 @@ var Struct = {
 | 
			
		||||
        while (o.left != null){
 | 
			
		||||
          o = yield* this.getOperation(o.left);
 | 
			
		||||
        }
 | 
			
		||||
      } else { // left & right are null
 | 
			
		||||
        var p = yield* this.getOperation(op.parent);
 | 
			
		||||
        if (op.parentSub != null) {
 | 
			
		||||
          o = yield* this.getOperation(p.map[op.parentSub]);
 | 
			
		||||
        } else {
 | 
			
		||||
          o = yield* this.getOperation(p.start);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // handle conflicts
 | 
			
		||||
      while (true) {
 | 
			
		||||
        if (o != null && o.id !== op.right){
 | 
			
		||||
          if (Struct.Insert.getDistanceToOrigin(o) === i) {
 | 
			
		||||
          var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o);
 | 
			
		||||
          if (oOriginDistance === i) {
 | 
			
		||||
            // case 1
 | 
			
		||||
            if (o.id[0] < op.id[0]) {
 | 
			
		||||
              op.left = o.id;
 | 
			
		||||
              distanceToOrigin = i + 1;
 | 
			
		||||
            }
 | 
			
		||||
          } else if ((tmp = Struct.Insert.getDistanceToOrigin(o)) < i) {
 | 
			
		||||
          } else if (oOriginDistance < i) {
 | 
			
		||||
            // case 2
 | 
			
		||||
            if (i - distanceToOrigin <= tmp) {
 | 
			
		||||
            if (i - distanceToOrigin <= oOriginDistance) {
 | 
			
		||||
              op.left = o.id;
 | 
			
		||||
              distanceToOrigin = i + 1;
 | 
			
		||||
            }
 | 
			
		||||
@ -202,22 +204,41 @@ var Struct = {
 | 
			
		||||
      // reconnect..
 | 
			
		||||
      var left = null;
 | 
			
		||||
      var right = null;
 | 
			
		||||
      var parent = yield* this.getOperation(op.parent);
 | 
			
		||||
 | 
			
		||||
      // NOTE: You you have to call addOperation before you set any other operation!
 | 
			
		||||
 | 
			
		||||
      // reconnect left and set right of op
 | 
			
		||||
      if (op.left != null) {
 | 
			
		||||
        left = yield* this.getOperation(op.left);
 | 
			
		||||
        op.right = left.right;
 | 
			
		||||
        left.right = op.id;
 | 
			
		||||
        if ((yield* this.addOperation(op)) === false) { // add here
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        yield* this.setOperation(left);
 | 
			
		||||
      } else {
 | 
			
		||||
        if ((yield* this.addOperation(op)) === false) { // or here
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        // only set right, if possible
 | 
			
		||||
        if (op.parentSub != null) {
 | 
			
		||||
          var sub = parent[op.parentSub];
 | 
			
		||||
          op.right = sub != null ? sub : null;
 | 
			
		||||
        } else {
 | 
			
		||||
          op.right = parent.start;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // reconnect right
 | 
			
		||||
      if (op.right != null) {
 | 
			
		||||
        right = yield* this.getOperation(op.right);
 | 
			
		||||
        right.left = op.id;
 | 
			
		||||
        yield* this.setOperation(right);
 | 
			
		||||
      }
 | 
			
		||||
      yield* this.setOperation(op);
 | 
			
		||||
 | 
			
		||||
      // notify parent
 | 
			
		||||
      var parent = yield* this.getOperation(op.parent);
 | 
			
		||||
      if (op.parentSub != null) {
 | 
			
		||||
        if (right == null) {
 | 
			
		||||
        if (left == null) {
 | 
			
		||||
          parent.map[op.parentSub] = op.id;
 | 
			
		||||
          yield* this.setOperation(parent);
 | 
			
		||||
        }
 | 
			
		||||
@ -232,7 +253,6 @@ var Struct = {
 | 
			
		||||
          yield* this.setOperation(parent);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      yield* this.setOperation(op);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  List: {
 | 
			
		||||
@ -242,6 +262,12 @@ var Struct = {
 | 
			
		||||
      op.struct = "List";
 | 
			
		||||
      return yield* Struct.Operation.create.call(this, op);
 | 
			
		||||
    },
 | 
			
		||||
    encode: function(op){
 | 
			
		||||
      return {
 | 
			
		||||
        struct: "List",
 | 
			
		||||
        id: op.id
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    requiredOps: function(op){
 | 
			
		||||
      var ids = [];
 | 
			
		||||
      if (op.start != null) {
 | 
			
		||||
@ -253,7 +279,9 @@ var Struct = {
 | 
			
		||||
      return ids;
 | 
			
		||||
    },
 | 
			
		||||
    execute: function* (op) {
 | 
			
		||||
      yield* this.setOperation(op);
 | 
			
		||||
      if ((yield* this.addOperation(op)) === false) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    ref: function* (op : Op, pos : number) : Insert {
 | 
			
		||||
      var o = op.start;
 | 
			
		||||
@ -298,6 +326,12 @@ var Struct = {
 | 
			
		||||
      op.struct = "Map";
 | 
			
		||||
      return yield* Struct.Operation.create.call(this, op);
 | 
			
		||||
    },
 | 
			
		||||
    encode: function(op){
 | 
			
		||||
      return {
 | 
			
		||||
        struct: "Map",
 | 
			
		||||
        id: op.id
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    requiredOps: function(op){
 | 
			
		||||
      var ids = [];
 | 
			
		||||
      for (var end in op.map) {
 | 
			
		||||
@ -306,21 +340,18 @@ var Struct = {
 | 
			
		||||
      return ids;
 | 
			
		||||
    },
 | 
			
		||||
    execute: function* (op) {
 | 
			
		||||
      yield* this.setOperation(op);
 | 
			
		||||
      if ((yield* this.addOperation(op)) === false) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    get: function* (op, name) {
 | 
			
		||||
      var res = yield* this.getOperation(op.map[name]);
 | 
			
		||||
      return (res != null) ? res.content : void 0;
 | 
			
		||||
    },
 | 
			
		||||
    set: function* (op, name, value) {
 | 
			
		||||
      var end = op.map[name];
 | 
			
		||||
      if (end == null) {
 | 
			
		||||
        end = null;
 | 
			
		||||
        op.map[name] = end;
 | 
			
		||||
      }
 | 
			
		||||
      var insert = {
 | 
			
		||||
        left: end,
 | 
			
		||||
        right: null,
 | 
			
		||||
        left: null,
 | 
			
		||||
        right: op.map[name] || null,
 | 
			
		||||
        content: value,
 | 
			
		||||
        parent: op.id,
 | 
			
		||||
        parentSub: name
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ describe("Yjs (basic)", function(){
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    var transaction = function*(root){
 | 
			
		||||
      expect(yield* root.val("stuff")).toEqual("c1");
 | 
			
		||||
      expect(yield* root.val("stuff")).toEqual("c0");
 | 
			
		||||
    };
 | 
			
		||||
    y.connector.flushAll();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user