simple conflicts are now handled correctly

This commit is contained in:
Kevin Jahns 2015-07-06 16:47:49 +02:00
parent 9d0373b85b
commit bf4d5f24a8
9 changed files with 114 additions and 121 deletions

View File

@ -23,6 +23,7 @@
"GeneratorFunction": true, "GeneratorFunction": true,
"Y": true, "Y": true,
"setTimeout": true, "setTimeout": true,
"setInterval": true "setInterval": true,
"Operation": true
} }
} }

View File

@ -69,11 +69,11 @@ class Test extends AbstractConnector {
this.globalRoom = globalRoom; this.globalRoom = globalRoom;
} }
send (userId, message) { send (userId, message) {
globalRoom.buffers[userId].push([this.userId, message]); globalRoom.buffers[userId].push(JSON.parse(JSON.stringify([this.userId, message])));
} }
broadcast (message) { broadcast (message) {
for (var key in globalRoom.buffers) { 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 () { disconnect () {

View File

@ -12,8 +12,10 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars
yield* this.setOperation(op); yield* this.setOperation(op);
this.store.operationAdded(op); this.store.operationAdded(op);
return true; return true;
} else { } else if (op.id[1] < state.clock) {
return false; 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 * sid : String (converted from id via JSON.stringify
so we can use it as a property name) 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! a property before you iterate over it!
*/ */
} }
@ -56,16 +58,15 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
for (var key in ops) { for (var key in ops) {
var o = ops[key]; var o = ops[key];
var required = Y.Struct[o.struct].requiredOps(o); 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. // 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) { if (ids.length > 0) {
let listener : Listener = { let listener : Listener = {
f: f, op: op,
args: args || [],
missing: ids.length missing: ids.length
}; };
@ -81,8 +82,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
} }
} else { } else {
this.listenersByIdExecuteNow.push({ this.listenersByIdExecuteNow.push({
f: f, op: op
args: args || []
}); });
} }
@ -103,8 +103,8 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
store.listenersByIdRequestPending = false; store.listenersByIdRequestPending = false;
for (let key in exeNow) { for (let key in exeNow) {
let listener = exeNow[key]; let o = exeNow[key].op;
yield* listener.f.call(this, listener.args); yield* Struct[o.struct].execute.call(this, o);
} }
for (var sid in ls){ for (var sid in ls){
@ -115,8 +115,9 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
} else { } else {
for (let key in l) { for (let key in l) {
let listener = l[key]; let listener = l[key];
let o = listener.op;
if (--listener.missing === 0){ 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 // called by a transaction when an operation is added
operationAdded (op) { operationAdded (op) {
var sid = JSON.stringify(op.id);
var l = this.listenersById[sid];
delete this.listenersById[sid];
// notify whenOperation listeners (by id) // notify whenOperation listeners (by id)
var l = this.listenersById[JSON.stringify(op.id)];
if (l != null) { if (l != null) {
for (var key in l){ for (var key in l){
var listener = l[key]; var listener = l[key];
if (--listener.missing === 0){ if (--listener.missing === 0){
this.whenOperationsExist([], listener.f, listener.args); this.whenOperationsExist([], listener.op);
} }
} }
} }

View File

@ -2,49 +2,4 @@
/*eslint-env browser,jasmine,console */ /*eslint-env browser,jasmine,console */
describe("OperationStore", function() { 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]);
});
}); });

View File

@ -84,6 +84,7 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
for (var clock = startPos; clock <= endPos; clock++) { for (var clock = startPos; clock <= endPos; clock++) {
var op = yield* this.getOperation([user, clock]); var op = yield* this.getOperation([user, clock]);
if (op != null) { if (op != null) {
op = Struct[op.struct].encode(op);
ops.push(yield* this.makeOperationReady.call(this, startSS, 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; return ops;
} }
*makeOperationReady (ss, op) { *makeOperationReady (ss, op) {
// instead of ss, you could use currSS (a ss that increments when you add an operation)
var clock; var clock;
var o = op; var o = op;
while (true){ while (true){

View File

@ -40,10 +40,12 @@ var Struct = {
var user = this.store.y.connector.userId; var user = this.store.y.connector.userId;
var state = yield* this.getState(user); var state = yield* this.getState(user);
op.id = [user, state.clock]; 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({ this.store.y.connector.broadcast({
type: "update", type: "update",
ops: [op] ops: [Struct[op.struct].encode(op)]
}); });
} }
}, },
@ -67,16 +69,18 @@ var Struct = {
yield* Struct.Operation.create.call(this, op); yield* Struct.Operation.create.call(this, op);
if (op.left != null) { if (op.left != null) {
op.left.right = op.id; var left = yield* this.getOperation(op.left);
yield* this.setOperation(op.left); left.right = op.id;
yield* this.setOperation(left);
} }
if (op.right != null) { if (op.right != null) {
op.right.left = op.id; var right = yield* this.getOperation(op.right);
yield* this.setOperation(op.right); right.left = op.id;
yield* this.setOperation(right);
} }
var parent = yield* this.getOperation(op.parent); var parent = yield* this.getOperation(op.parent);
if (op.parentSub != null){ 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; parent.map[op.parentSub] = op.id;
yield* this.setOperation(parent); yield* this.setOperation(parent);
} }
@ -95,6 +99,22 @@ var Struct = {
} }
return op; 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){ requiredOps: function(op){
var ids = []; var ids = [];
if(op.left != null){ if(op.left != null){
@ -133,39 +153,11 @@ var Struct = {
# $this insert_position is to the left of $o (forever!) # $this insert_position is to the left of $o (forever!)
*/ */
execute: function*(op){ execute: function*(op){
var distanceToOrigin = yield* Struct.Insert.getDistanceToOrigin.call(this, op); // most cases: 0 (starts from 0) var i; // loop counter
var i = distanceToOrigin; // loop counter var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op); // most cases: 0 (starts from 0)
var o, tmp; var o;
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;
}
}
}
// find o. o is the first conflicting operation
if (op.left != null) { if (op.left != null) {
o = yield* this.getOperation(op.left); o = yield* this.getOperation(op.left);
o = yield* this.getOperation(o.right); o = yield* this.getOperation(o.right);
@ -174,18 +166,28 @@ var Struct = {
while (o.left != null){ while (o.left != null){
o = yield* this.getOperation(o.left); 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) { while (true) {
if (o != null && o.id !== op.right){ 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 // case 1
if (o.id[0] < op.id[0]) { if (o.id[0] < op.id[0]) {
op.left = o.id; op.left = o.id;
distanceToOrigin = i + 1; distanceToOrigin = i + 1;
} }
} else if ((tmp = Struct.Insert.getDistanceToOrigin(o)) < i) { } else if (oOriginDistance < i) {
// case 2 // case 2
if (i - distanceToOrigin <= tmp) { if (i - distanceToOrigin <= oOriginDistance) {
op.left = o.id; op.left = o.id;
distanceToOrigin = i + 1; distanceToOrigin = i + 1;
} }
@ -202,22 +204,41 @@ var Struct = {
// reconnect.. // reconnect..
var left = null; var left = null;
var right = 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) { if (op.left != null) {
left = yield* this.getOperation(op.left); left = yield* this.getOperation(op.left);
op.right = left.right;
left.right = op.id; left.right = op.id;
if ((yield* this.addOperation(op)) === false) { // add here
return;
}
yield* this.setOperation(left); 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) { if (op.right != null) {
right = yield* this.getOperation(op.right); right = yield* this.getOperation(op.right);
right.left = op.id; right.left = op.id;
yield* this.setOperation(right); yield* this.setOperation(right);
} }
yield* this.setOperation(op);
// notify parent // notify parent
var parent = yield* this.getOperation(op.parent);
if (op.parentSub != null) { if (op.parentSub != null) {
if (right == null) { if (left == null) {
parent.map[op.parentSub] = op.id; parent.map[op.parentSub] = op.id;
yield* this.setOperation(parent); yield* this.setOperation(parent);
} }
@ -232,7 +253,6 @@ var Struct = {
yield* this.setOperation(parent); yield* this.setOperation(parent);
} }
} }
yield* this.setOperation(op);
} }
}, },
List: { List: {
@ -242,6 +262,12 @@ var Struct = {
op.struct = "List"; op.struct = "List";
return yield* Struct.Operation.create.call(this, op); return yield* Struct.Operation.create.call(this, op);
}, },
encode: function(op){
return {
struct: "List",
id: op.id
};
},
requiredOps: function(op){ requiredOps: function(op){
var ids = []; var ids = [];
if (op.start != null) { if (op.start != null) {
@ -253,7 +279,9 @@ var Struct = {
return ids; return ids;
}, },
execute: function* (op) { execute: function* (op) {
yield* this.setOperation(op); if ((yield* this.addOperation(op)) === false) {
return;
}
}, },
ref: function* (op : Op, pos : number) : Insert { ref: function* (op : Op, pos : number) : Insert {
var o = op.start; var o = op.start;
@ -298,6 +326,12 @@ var Struct = {
op.struct = "Map"; op.struct = "Map";
return yield* Struct.Operation.create.call(this, op); return yield* Struct.Operation.create.call(this, op);
}, },
encode: function(op){
return {
struct: "Map",
id: op.id
};
},
requiredOps: function(op){ requiredOps: function(op){
var ids = []; var ids = [];
for (var end in op.map) { for (var end in op.map) {
@ -306,21 +340,18 @@ var Struct = {
return ids; return ids;
}, },
execute: function* (op) { execute: function* (op) {
yield* this.setOperation(op); if ((yield* this.addOperation(op)) === false) {
return;
}
}, },
get: function* (op, name) { get: function* (op, name) {
var res = yield* this.getOperation(op.map[name]); var res = yield* this.getOperation(op.map[name]);
return (res != null) ? res.content : void 0; return (res != null) ? res.content : void 0;
}, },
set: function* (op, name, value) { set: function* (op, name, value) {
var end = op.map[name];
if (end == null) {
end = null;
op.map[name] = end;
}
var insert = { var insert = {
left: end, left: null,
right: null, right: op.map[name] || null,
content: value, content: value,
parent: op.id, parent: op.id,
parentSub: name parentSub: name

View File

@ -75,7 +75,7 @@ describe("Yjs (basic)", function(){
}); });
var transaction = function*(root){ var transaction = function*(root){
expect(yield* root.val("stuff")).toEqual("c1"); expect(yield* root.val("stuff")).toEqual("c0");
}; };
y.connector.flushAll(); y.connector.flushAll();

4
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long