diff --git a/gulpfile.js b/gulpfile.js index 0ba0c6b2..76d9fd86 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -130,8 +130,8 @@ gulp.task("build_jasmine_browser", function(){ gulp.task("develop", ["build_jasmine_browser", "build"], function(){ gulp.watch(files.test, ["build_jasmine_browser"]); - gulp.watch(files.test, ["test"]); - gulp.watch(files.test, ["build"]); + //gulp.watch(files.test, ["test"]); + //gulp.watch(files.test, ["build"]); return gulp.src("build/jasmine_browser.js") .pipe(watch("build/jasmine_browser.js")) diff --git a/src/Connectors/Test.js b/src/Connectors/Test.js index 243c33f7..ae7fde3f 100644 --- a/src/Connectors/Test.js +++ b/src/Connectors/Test.js @@ -2,7 +2,6 @@ var globalRoom = { users: {}, buffers: {}, removeUser: function(user : AbstractConnector){ - for (var i in this.users) { this.users[i].userLeft(user); } diff --git a/src/helper.spec.js b/src/Helper.spec.js similarity index 94% rename from src/helper.spec.js rename to src/Helper.spec.js index 912bbda9..d42a1aeb 100644 --- a/src/helper.spec.js +++ b/src/Helper.spec.js @@ -73,10 +73,9 @@ function compareAllUsers(users){//eslint-disable-line } function createUsers(self, numberOfUsers, done) {//eslint-disable-line - if (self.users != null) { - for (var y of self.users) { - y.destroy(); - } + //destroy old users + for (var u in globalRoom.users) {//eslint-disable-line + globalRoom.users[u].y.destroy()//eslint-disable-line } self.users = []; diff --git a/src/OperationStore.js b/src/OperationStore.js index d2c3a1c5..1eda2c51 100644 --- a/src/OperationStore.js +++ b/src/OperationStore.js @@ -9,7 +9,7 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars if (t == null) { var op = yield* this.getOperation(id); if (op != null) { - t = yield* Y[op.type].create(this.store, op); + t = yield* Y[op.type].create.call(this, this.store, op); this.store.initializedTypes[sid] = t; } } @@ -28,7 +28,7 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars state.clock++; yield* this.setState(state); this.os.add(op); - this.store.operationAdded(op); + yield* this.store.operationAdded(this, op); return true; } else if (op.id[1] < state.clock) { return false; @@ -41,7 +41,7 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars for (var i = 0; i < ops.length; i++) { var op = ops[i]; yield* Struct[op.struct].execute.call(this, op); - send.push(Struct[op.struct].encode(op)); + send.push(copyObject(Struct[op.struct].encode(op))); } this.store.y.connector.broadcast({ type: "update", @@ -161,7 +161,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars }); } // called by a transaction when an operation is added - operationAdded (op) { + *operationAdded (transaction, op) { var sid = JSON.stringify(op.id); var l = this.listenersById[sid]; delete this.listenersById[sid]; @@ -178,7 +178,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars // notify parent, if it has been initialized as a custom type var t = this.initializedTypes[JSON.stringify(op.parent)]; if (t != null) { - t._changed(op); + yield* t._changed(transaction, copyObject(op)); } } removeParentListener (id, f) { diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js index 5f921ff9..8f705fe0 100644 --- a/src/OperationStores/Memory.js +++ b/src/OperationStores/Memory.js @@ -120,7 +120,7 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars } requestTransaction (makeGen : Function) { var t = new Transaction(this); - var gen = makeGen.call(t, t.getType(["_", 0]).next().value); + var gen = makeGen.call(t); var res = gen.next(); while(!res.done){ if (res.value === "transaction") { diff --git a/src/Struct.js b/src/Struct.js index 1d2fc8a0..fd0e0a37 100644 --- a/src/Struct.js +++ b/src/Struct.js @@ -35,7 +35,7 @@ function compareIds(id1, id2) { } var Struct = { - /* + /* This Operations does _not_ have an id! { target: Id } @@ -48,12 +48,16 @@ var Struct = { return [op.target]; }, execute: function* (op) { - if ((yield* this.addOperation(op)) === false) { - return; - } var target = yield* this.getOperation(op.target); - target.deleted = true; - yield* this.setOperation(target); + if (!target.deleted) { + target.deleted = true; + yield* this.setOperation(target); + var t = this.store.initializedTypes[JSON.stringify(target.parent)]; + if (t != null) { + yield* t._changed(this, copyObject(op)); + } + } + } }, Insert: { @@ -280,14 +284,6 @@ var Struct = { } return res; }, - delete: function* (op, pos) { - var ref = yield* Struct.List.ref.call(this, op, pos); - if (ref != null) { - yield* Struct.Delete.create.call(this, { - target: ref.id - }); - } - }, map: function* (o : Op, f : Function) : Array { o = o.start; var res = []; @@ -299,31 +295,6 @@ var Struct = { o = operation.right; } return res; - }, - insert: function* (op, pos : number, contents : Array) { - var left, right; - if (pos === 0) { - left = null; - right = op.start; - } else { - var ref = yield* Struct.List.ref.call(this, op, pos - 1); - if (ref === null) { - left = op.end; - right = null; - } else { - left = ref.id; - right = ref.right; - } - } - for (var key in contents) { - var insert = { - left: left, - right: right, - content: contents[key], - parent: op.id - }; - left = (yield* Struct.Insert.create.call(this, insert)).id; - } } }, Map: { diff --git a/src/Types/Array.js b/src/Types/Array.js index bbb254d4..b08aa4d9 100644 --- a/src/Types/Array.js +++ b/src/Types/Array.js @@ -12,18 +12,28 @@ this.eventHandler = new EventHandler( ops =>{ for (var i in ops) { var op = ops[i]; - var pos; - if (op.right === null) { - pos = this.idArray.length; + if (op.struct === "Insert") { + let pos; + // we check op.left only!, + // because op.right might not be defined when this is called + if (op.left === null) { + pos = 0; + } else { + var sid = JSON.stringify(op.left); + pos = this.idArray.indexOf(sid) + 1; + if (pos <= 0) { + throw new Error("Unexpected operation!"); + } + } + this.idArray.splice(pos, 0, JSON.stringify(op.id)); + this.valArray.splice(pos, 0, op.content); + } else if (op.struct === "Delete") { + let pos = this.idArray.indexOf(JSON.stringify(op.target)); + this.idArray.splice(pos, 1); + this.valArray.splice(pos, 1); } else { - var sid = JSON.stringify(op.right); - pos = this.idArray.indexOf(sid); + throw new Error("Unexpected struct!"); } - if (pos < 0) { - throw new Error("Unexpected operation!"); - } - this.idArray.splice(pos, 0, JSON.stringify(op.id)); - this.valArray.splice(pos, 0, op.content); } }); } @@ -50,7 +60,6 @@ throw new Error("This position exceeds the range of the array!"); } var mostLeft = pos === 0 ? null : JSON.parse(this.idArray[pos - 1]); - var mostRight = pos === this.idArray.length ? null : JSON.parse(this.idArray[pos]); var ops = []; var prevId = mostLeft; @@ -58,7 +67,9 @@ var op = { left: prevId, origin: prevId, - right: mostRight, + // right: mostRight, + // NOTE: I intentionally do not define right here, because it could be deleted + // at the time of creating this operation, and is therefore not defined in idArray parent: this._model, content: contents[i], struct: "Insert", @@ -70,19 +81,58 @@ var eventHandler = this.eventHandler; eventHandler.awaitAndPrematurelyCall(ops); this.os.requestTransaction(function*(){ + // now we can set the right reference. + var mostRight; + if (mostLeft != null) { + mostRight = (yield* this.getOperation(mostLeft)).right; + } else { + mostRight = (yield* this.getOperation(ops[0].parent)).start; + } + for (var j in ops) { + ops[j].right = mostRight; + } yield* this.applyCreatedOperations(ops); - eventHandler.awaitedLastOp(ops.length); + eventHandler.awaitedLastInserts(ops.length); }); } - *delete (pos) { + delete (pos, length = 1) { + if (typeof length !== "number") { + throw new Error("pos must be a number!"); + } if (typeof pos !== "number") { throw new Error("pos must be a number!"); } - var t = yield "transaction"; - var model = yield* t.getOperation(this._model); - yield* Y.Struct.Array.delete.call(t, model, pos); + if (pos + length > this.idArray.length || pos < 0 || length < 0) { + throw new Error("The deletion range exceeds the range of the array!"); + } + var eventHandler = this.eventHandler; + var newLeft = pos > 0 ? JSON.parse(this.idArray[pos - 1]) : null; + var dels = []; + for (var i = 0; i < length; i++) { + dels.push({ + target: JSON.parse(this.idArray[pos + i]), + struct: "Delete" + }); + } + eventHandler.awaitAndPrematurelyCall(dels); + this.os.requestTransaction(function*(){ + yield* this.applyCreatedOperations(dels); + eventHandler.awaitedLastDeletes(dels.length, newLeft); + }); } - _changed (op) { + *_changed (transaction, op) { + if (op.struct === "Insert") { + var l = op.left; + var left; + while (l != null) { + left = yield* transaction.getOperation(l); + if (!left.deleted) { + break; + } + l = left.left; + } + op.left = l; + } this.eventHandler.receivedOp(op); } } diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js index 386ba150..31f62dbb 100644 --- a/src/Types/Array.spec.js +++ b/src/Types/Array.spec.js @@ -1,10 +1,10 @@ /* @flow */ /*eslint-env browser,jasmine */ -var numberOfYArrayTests = 10; +var numberOfYArrayTests = 20; describe("Array Type", function(){ - jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; beforeEach(function(done){ createUsers(this, 5, done); }); @@ -42,11 +42,64 @@ describe("Array Type", function(){ done(); }); }); + it("Basic insert&delete in array (handle three conflicts)", function(done){ + var y = this.users[0]; + var l1, l2, l3; + y.root.set("Array", Y.Array).then((array)=>{ + l1 = array; + l1.insert(0, ["x", "y", "z"]); + y.connector.flushAll(); + l1.insert(1, [0]); + return this.users[1].root.get("Array"); + }).then((array)=>{ + l2 = array; + l2.delete(0); + l2.delete(1); + return this.users[2].root.get("Array"); + }).then((array)=>{ + l3 = array; + l3.insert(1, [2]); + y.connector.flushAll(); + expect(l1.toArray()).toEqual(l2.toArray()); + expect(l2.toArray()).toEqual(l3.toArray()); + expect(l2.toArray()).toEqual([0, 2, "y"]); + compareAllUsers(this.users); + done(); + }); + }); + it("Basic insert. Then delete the whole array", function(done){ + var y = this.users[0]; + var l1, l2, l3; + y.root.set("Array", Y.Array).then((array)=>{ + l1 = array; + l1.insert(0, ["x", "y", "z"]); + y.connector.flushAll(); + l1.delete(0, 3); + return this.users[1].root.get("Array"); + }).then((array)=>{ + l2 = array; + return this.users[2].root.get("Array"); + }).then((array)=>{ + l3 = array; + y.connector.flushAll(); + expect(l1.toArray()).toEqual(l2.toArray()); + expect(l2.toArray()).toEqual(l3.toArray()); + expect(l2.toArray()).toEqual([]); + compareAllUsers(this.users); + done(); + }); + }); }); - describe("Random tests", function(){ - var randomMapTransactions = [ + describe(`${numberOfYArrayTests} Random tests`, function(){ + var randomArrayTransactions = [ function insert (array) { array.insert(getRandomNumber(array.toArray().length), [getRandomNumber()]); + }, + function _delete (array) { + var length = array.toArray().length; + if (length > 0) { + array.delete(getRandomNumber(length - 1)); + } } ]; function compareArrayValues(arrays){ @@ -82,7 +135,7 @@ describe("Array Type", function(){ expect(this.arrays.length).toEqual(this.users.length); }); it(`succeed after ${numberOfYArrayTests} actions`, function(done){ - applyRandomTransactions(this.users, this.arrays, randomMapTransactions, numberOfYArrayTests); + applyRandomTransactions(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests); setTimeout(()=>{ compareAllUsers(this.users); compareArrayValues(this.arrays); diff --git a/src/Types/Map.js b/src/Types/Map.js index 62115f53..7dd0ad93 100644 --- a/src/Types/Map.js +++ b/src/Types/Map.js @@ -18,12 +18,12 @@ class EventHandler { this.awaiting++; this.onevent(ops); } - awaitedLastOp (n) { + awaitedLastInserts (n) { var ops = this.waiting.splice(this.waiting.length - n); for (var oid = 0; oid < ops.length; oid++) { var op = ops[oid]; for (var i = this.waiting.length - 1; i >= 0; i--) { - var w = this.waiting[i]; + let w = this.waiting[i]; if (compareIds(op.left, w.id)) { // include the effect of op in w w.right = op.id; @@ -36,7 +36,25 @@ class EventHandler { } } } - + this.tryCallEvents(); + } + awaitedLastDeletes (n, newLeft) { + var ops = this.waiting.splice(this.waiting.length - n); + for (var j in ops) { + var del = ops[j]; + if (newLeft != null) { + for (var i in this.waiting) { + let w = this.waiting[i]; + // We will just care about w.left + if (compareIds(del.target, w.left)) { + del.left = newLeft; + } + } + } + } + this.tryCallEvents(); + } + tryCallEvents () { this.awaiting--; if (this.awaiting <= 0) { var events = this.waiting; @@ -44,25 +62,39 @@ class EventHandler { this.onevent(events); } } -} +} (function(){ class Map { constructor (os, model) { this._model = model.id; this.os = os; - this.map = model.map; + this.map = copyObject(model.map); this.contents = {}; this.opContents = {}; this.eventHandler = new EventHandler( ops =>{ for (var i in ops) { var op = ops[i]; - if (op.left === null) { - if (op.opContent != null) { - this.opContents[op.parentSub] = op.opContent; - } else { - this.contents[op.parentSub] = op.content; + if (op.struct === "Insert"){ + if (op.left === null) { + if (op.opContent != null) { + this.opContents[op.parentSub] = op.opContent; + } else { + this.contents[op.parentSub] = op.content; + } + this.map[op.parentSub] = op.id; } + } else if (op.struct === "Delete") { + var key = op.key; + if (compareIds(this.map[key], op.target)) { + if (this.contents[key] != null) { + delete this.contents[key]; + } else { + delete this.opContents[key]; + } + } + } else { + throw new Error("Unexpected Operation!"); } } }); @@ -82,6 +114,23 @@ class EventHandler { return def.promise; } } + delete (key) { + var right = this.map[key]; + if (right != null) { + var del = { + target: right, + struct: "Delete" + }; + var eventHandler = this.eventHandler; + var modDel = copyObject(del); + modDel.key = key; + eventHandler.awaitAndPrematurelyCall([modDel]); + this.os.requestTransaction(function*(){ + yield* this.applyCreatedOperations([del]); + eventHandler.awaitedLastDeletes(1); + }); + } + } set (key, value) { // set property. // if property is a type, return a promise @@ -114,7 +163,7 @@ class EventHandler { this.os.requestTransaction(function*(){ yield* this.applyCreatedOperations([insert]); - eventHandler.awaitedLastOp(1); + eventHandler.awaitedLastInserts(1); }); def.resolve(value); } @@ -126,7 +175,10 @@ class EventHandler { var model = yield* t.getOperation(this._model); yield* Y.Struct.Map.delete.call(t, model, key); }*/ - _changed (op) { + *_changed (transaction, op) { + if (op.struct === "Delete") { + op.key = (yield* transaction.getOperation(op.target)).parentSub; + } this.eventHandler.receivedOp(op); } } diff --git a/src/Types/Map.spec.js b/src/Types/Map.spec.js index acc9e4ae..585e5d58 100644 --- a/src/Types/Map.spec.js +++ b/src/Types/Map.spec.js @@ -1,7 +1,7 @@ /* @flow */ /*eslint-env browser,jasmine */ -var numberOfYMapTests = 5; +var numberOfYMapTests = 70; describe("Map Type", function(){ jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; @@ -77,6 +77,25 @@ describe("Map Type", function(){ done(); }, 50); }); + it("Basic get&set&delete of Map property (handle conflict)", function(done){ + var y = this.users[0]; + y.connector.flushAll(); + y.root.set("stuff", "c0"); + y.root.delete("stuff"); + + this.users[1].root.set("stuff", "c1"); + + y.connector.flushAll(); + + setTimeout( () => { + for (var key in this.users) { + var u = this.users[key]; + expect(u.root.get("stuff")).toBeUndefined(); + compareAllUsers(this.users); + } + done(); + }, 50); + }); it("Basic get&set of Map property (handle three conflicts)", function(done){ var y = this.users[0]; this.users[0].root.set("stuff", "c0"); @@ -94,13 +113,36 @@ describe("Map Type", function(){ done(); }, 50); }); + it("Basic get&set&delete of Map property (handle three conflicts)", function(done){ + var y = this.users[0]; + this.users[0].root.set("stuff", "c0"); + this.users[1].root.set("stuff", "c1"); + this.users[2].root.set("stuff", "c2"); + this.users[3].root.set("stuff", "c3"); + y.connector.flushAll(); + this.users[0].root.set("stuff", "deleteme"); + this.users[0].root.delete("stuff"); + this.users[1].root.set("stuff", "c1"); + this.users[2].root.set("stuff", "c2"); + this.users[3].root.set("stuff", "c3"); + y.connector.flushAll(); + + setTimeout( () => { + for (var key in this.users) { + var u = this.users[key]; + expect(u.root.get("stuff")).toBeUndefined(); + } + compareAllUsers(this.users); + done(); + }, 50); + }); }); - describe("Random tests", function(){ + describe(`${numberOfYMapTests} Random tests`, function(){ var randomMapTransactions = [ function set (map) { map.set("somekey", getRandomNumber()); }, - function* delete_ (map) { + function delete_ (map) { map.delete("somekey"); } ];