simple conflicts are now handled correctly
This commit is contained in:
parent
9d0373b85b
commit
bf4d5f24a8
@ -23,6 +23,7 @@
|
|||||||
"GeneratorFunction": true,
|
"GeneratorFunction": true,
|
||||||
"Y": true,
|
"Y": true,
|
||||||
"setTimeout": true,
|
"setTimeout": true,
|
||||||
"setInterval": true
|
"setInterval": true,
|
||||||
|
"Operation": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 () {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -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){
|
||||||
|
141
src/Struct.js
141
src/Struct.js
@ -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
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user