diff --git a/.eslintrc b/.eslintrc index b0791eea..340ba6a7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -28,6 +28,7 @@ "Operation": true, "getRandom": true, "RBTree": true, - "compareIds": true + "compareIds": true, + "EventHandler": true } } diff --git a/src/OperationStore.js b/src/OperationStore.js index 1190c7c1..d2c3a1c5 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.id); + t = yield* Y[op.type].create(this.store, op); this.store.initializedTypes[sid] = t; } } @@ -36,6 +36,18 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars throw new Error("Operations must arrive in order!"); } } + *applyCreatedOperations (ops) { + var send = []; + 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)); + } + this.store.y.connector.broadcast({ + type: "update", + ops: send + }); + } } type Listener = { @@ -70,6 +82,13 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars } setUserId (userId) { this.userId = userId; + this.opClock = 0; + } + getNextOpId () { + if (this.userId == null) { + throw new Error("OperationStore not yet initialized!"); + } + return [this.userId, this.opClock++]; } apply (ops) { for (var key in ops) { diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js index 353c8103..5f921ff9 100644 --- a/src/OperationStores/Memory.js +++ b/src/OperationStores/Memory.js @@ -14,13 +14,12 @@ function copyObject (o) { } type StateVector = Array; -type OperationSet = Object; // os[Id] = op type StateSet = Object; Y.Memory = (function(){ //eslint-disable-line no-unused-vars class Transaction extends AbstractTransaction { //eslint-disable-line ss: StateSet; - os: OperationSet; + os: RBTree; store: OperationStore; constructor (store : OperationStore) { diff --git a/src/Struct.js b/src/Struct.js index 64c9e990..1d2fc8a0 100644 --- a/src/Struct.js +++ b/src/Struct.js @@ -35,28 +35,12 @@ function compareIds(id1, id2) { } var Struct = { - Operation: { //eslint-disable-line no-unused-vars - create: function*(op : Op) : Struct.Operation { - var user = this.store.y.connector.userId; - var state = yield* this.getState(user); - op.id = [user, state.clock]; - yield* Struct[op.struct].execute.call(this, op); - - this.store.y.connector.broadcast({ - type: "update", - ops: [Struct[op.struct].encode(op)] - }); - return op; - } - }, + /* + { + target: Id + } + */ Delete: { - create: function* (op) { - if (op.target == null) { - throw new Error("You must define a delete target!"); - } - op.struct = "Delete"; - return yield* Struct.Operation.create.call(this, op); - }, encode: function (op) { return op; }, @@ -77,20 +61,12 @@ var Struct = { content: any, left: Id, right: Id, + origin: id, parent: Id, - parentSub: string (optional) + parentSub: string (optional), + id: this.os.getNextOpId() } */ - create: function* ( op: Op ) : Insert { - if ( op.left === undefined - || op.right === undefined - || op.parent === undefined ) { - throw new Error("You must define left, right, and parent!"); - } - op.origin = op.left; - op.struct = "Insert"; - return yield* Struct.Operation.create.call(this, op); - }, encode: function(op){ /*var e = { id: op.id, @@ -248,12 +224,15 @@ var Struct = { } }, List: { - create: function* ( op : Op){ - op.start = null; - op.end = null; - op.struct = "List"; - return yield* Struct.Operation.create.call(this, op); - }, + /* + { + start: null, + end: null, + struct: "List", + type: "", + id: this.os.getNextOpId() + } + */ encode: function(op){ return { struct: "List", @@ -315,7 +294,7 @@ var Struct = { while ( o != null) { var operation = yield* this.getOperation(o); if (!operation.deleted) { - res.push(f(operation.content)); + res.push(f(operation)); } o = operation.right; } @@ -350,19 +329,18 @@ var Struct = { Map: { /* { - // empty + map: {}, + struct: "Map", + type: "", + id: this.os.getNextOpId() } */ - create: function* ( op : Op ){ - op.map = {}; - op.struct = "Map"; - return yield* Struct.Operation.create.call(this, op); - }, encode: function(op){ return { struct: "Map", type: op.type, - id: op.id + id: op.id, + map: {} // overwrite map!! }; }, requiredOps: function(){ diff --git a/src/Types/Array.js b/src/Types/Array.js new file mode 100644 index 00000000..bbb254d4 --- /dev/null +++ b/src/Types/Array.js @@ -0,0 +1,109 @@ + + +(function(){ + + class YArray { + constructor (os, _model, idArray, valArray) { + this.os = os; + this._model = _model; + // Array of all the operation id's + this.idArray = idArray; + this.valArray = valArray; + this.eventHandler = new EventHandler( ops =>{ + for (var i in ops) { + var op = ops[i]; + var pos; + if (op.right === null) { + pos = this.idArray.length; + } else { + var sid = JSON.stringify(op.right); + pos = this.idArray.indexOf(sid); + } + if (pos < 0) { + throw new Error("Unexpected operation!"); + } + this.idArray.splice(pos, 0, JSON.stringify(op.id)); + this.valArray.splice(pos, 0, op.content); + } + }); + } + get (pos) { + if (pos == null || typeof pos !== "number") { + throw new Error("pos must be a number!"); + } + return this.valArray[pos]; + } + toArray() { + return this.valArray.slice(); + } + insert (pos, contents) { + if (typeof pos !== "number") { + throw new Error("pos must be a number!"); + } + if (!(contents instanceof Array)) { + throw new Error("contents must be an Array of objects!"); + } + if (contents.length === 0) { + return; + } + if (pos > this.idArray.length || pos < 0) { + 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; + for (var i = 0; i < contents.length; i++) { + var op = { + left: prevId, + origin: prevId, + right: mostRight, + parent: this._model, + content: contents[i], + struct: "Insert", + id: this.os.getNextOpId() + }; + ops.push(op); + prevId = op.id; + } + var eventHandler = this.eventHandler; + eventHandler.awaitAndPrematurelyCall(ops); + this.os.requestTransaction(function*(){ + yield* this.applyCreatedOperations(ops); + eventHandler.awaitedLastOp(ops.length); + }); + } + *delete (pos) { + 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); + } + _changed (op) { + this.eventHandler.receivedOp(op); + } + } + + Y.Array = function* _YArray(){ + var model = { + start: null, + end: null, + struct: "List", + type: "Array", + id: this.store.getNextOpId() + }; + yield* this.applyCreatedOperations([model]); + return yield* this.createType(model); + }; + Y.Array.create = function* YArrayCreate(os, model){ + var valArray = []; + var idArray = yield* Y.Struct.List.map.call(this, model, function(c){ + valArray.push(c.content); + return JSON.stringify(c.id); + }); + return new YArray(os, model.id, idArray, valArray); + }; +})(); diff --git a/src/Types/List.js b/src/Types/List.js deleted file mode 100644 index 990d100a..00000000 --- a/src/Types/List.js +++ /dev/null @@ -1,48 +0,0 @@ - - -(function(){ - - class List { - constructor (_model) { - this._model = _model; - } - *val (pos) { - var t = yield "transaction"; - var model = yield* t.getOperation(this._model); - if (pos != null) { - var o = yield* Y.Struct.List.ref.call(t, model, pos); - return o ? o.content : null; - } else { - return yield* Y.Struct.List.map.call(t, model, function(c){return c; }); - } - } - *insert (pos, contents) { - if (typeof pos !== "number") { - throw new Error("pos must be a number!"); - } - if (!(contents instanceof Array)) { - throw new Error("contents must be an Array of objects!"); - } - var t = yield "transaction"; - var model = yield* t.getOperation(this._model); - yield* Y.Struct.List.insert.call(t, model, pos, contents); - } - *delete (pos) { - 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.List.delete.call(t, model, pos); - } - _changed () { - } - } - - Y.List = function* YList(){ - var t = yield "transaction"; - var model = yield* Y.Struct.List.create.call(t, {type: "List"}); - return t.createType(model); - }; - Y.List.Create = List; -})(); diff --git a/src/Types/Map.js b/src/Types/Map.js index 47482692..62115f53 100644 --- a/src/Types/Map.js +++ b/src/Types/Map.js @@ -14,25 +14,29 @@ class EventHandler { this.waiting.push(copyObject(op)); } } - awaitAndPrematurelyCall (op) { + awaitAndPrematurelyCall (ops) { this.awaiting++; - this.onevent([op]); + this.onevent(ops); } - awaitedLastOp () { - var op = this.waiting.pop(); - for (var i = this.waiting.length - 1; i >= 0; i--) { - var w = this.waiting[i]; - if (compareIds(op.left, w.id)) { - // include the effect of op in w - w.right = op.id; - // exclude the effect of w in op - op.left = w.left; - } else if (compareIds(op.right, w.id)) { - // similar.. - w.left = op.id; - op.right = w.right; + awaitedLastOp (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]; + if (compareIds(op.left, w.id)) { + // include the effect of op in w + w.right = op.id; + // exclude the effect of w in op + op.left = w.left; + } else if (compareIds(op.right, w.id)) { + // similar.. + w.left = op.id; + op.right = w.right; + } } } + this.awaiting--; if (this.awaiting <= 0) { var events = this.waiting; @@ -87,8 +91,10 @@ class EventHandler { var insert = { left: null, right: right, + origin: null, parent: this._model, - parentSub: key + parentSub: key, + struct: "Insert" }; var def = Promise.defer(); if ( value != null && value.constructor === GeneratorFunction) { @@ -96,17 +102,19 @@ class EventHandler { this.os.requestTransaction(function*(){ var type = yield* value.call(this); insert.opContent = type._model; - yield* Struct.Insert.create.call(this, insert); + insert.id = this.store.getNextOpId(); + yield* this.applyCreatedOperations([insert]); def.resolve(type); }); } else { insert.content = value; + insert.id = this.os.getNextOpId(); var eventHandler = this.eventHandler; - eventHandler.awaitAndPrematurelyCall(insert); + eventHandler.awaitAndPrematurelyCall([insert]); this.os.requestTransaction(function*(){ - yield* Struct.Insert.create.call(this, insert); - eventHandler.awaitedLastOp(); + yield* this.applyCreatedOperations([insert]); + eventHandler.awaitedLastOp(1); }); def.resolve(value); } @@ -124,7 +132,13 @@ class EventHandler { } Y.Map = function* YMap(){ - var model = yield* Y.Struct.Map.create.call(this, {type: "Map"}); + var model = { + map: {}, + struct: "Map", + type: "Map", + id: this.store.getNextOpId() + }; + yield* this.applyCreatedOperations([model]); return yield* this.createType(model); }; Y.Map.create = function* YMapCreate(os, model){ diff --git a/src/y.spec.js b/src/y.spec.js index ef2796e7..06381c55 100644 --- a/src/y.spec.js +++ b/src/y.spec.js @@ -21,9 +21,9 @@ function getRandomNumber(n) { return Math.floor(Math.random() * n); } -var numberOfYMapTests = 30; +var numberOfYMapTests = 5; -function applyRandomTransactions (users, transactions, numberOfTransactions) { +function applyRandomTransactions (users, objects, transactions, numberOfTransactions) { function randomTransaction (root) { var f = getRandom(transactions); f(root); @@ -34,7 +34,7 @@ function applyRandomTransactions (users, transactions, numberOfTransactions) { // 10% chance to flush users[0].connector.flushOne(); } else { - randomTransaction(getRandom(users).root); + randomTransaction(getRandom(objects)); } } } @@ -70,7 +70,7 @@ function compareAllUsers(users){ } describe("Yjs", function(){ - jasmine.DEFAULT_TIMEOUT_INTERVAL = 500; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; beforeEach(function(done){ if (this.users != null) { for (var y of this.users) { @@ -121,6 +121,16 @@ describe("Yjs", function(){ done(); }); }); + it("Map can set custom types (Array)", function(done){ + var y = this.users[0].root; + y.set("Array", Y.Array).then(function(array) { + array.insert(0, [1, 2, 3]); + return y.get("Array"); + }).then(function(array){ + expect(array.toArray()).toEqual([1, 2, 3]); + done(); + }); + }); it("Basic get&set of Map property (converge via update)", function(done){ var u = this.users[0]; u.connector.flushAll(); @@ -172,6 +182,28 @@ describe("Yjs", function(){ done(); }, 50); }); + it("Basic insert 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; + y.connector.flushAll(); + l1.insert(0, [0]); + return this.users[1].root.get("Array"); + }).then((array)=>{ + l2 = array; + l2.insert(0, [1]); + return this.users[2].root.get("Array"); + }).then((array)=>{ + l3 = array; + l3.insert(0, [2]); + y.connector.flushAll(); + expect(l1.toArray()).toEqual(l2.toArray()); + expect(l2.toArray()).toEqual(l3.toArray()); + compareAllUsers(this.users); + done(); + }); + }); }); describe("Map random tests", function(){ var randomMapTransactions = [ @@ -182,10 +214,10 @@ describe("Yjs", function(){ map.delete("somekey"); } ]; - function compareMapValues(users){ + function compareMapValues(maps){ var firstMap; - for (var u of users) { - var val = u.root.get(); + for (var map of maps) { + var val = map.get(); if (firstMap == null) { firstMap = val; } else { @@ -193,79 +225,80 @@ describe("Yjs", function(){ } } } - it(`succeed after ${numberOfYMapTests} actions with flush before transactions`, function(done){ + beforeEach(function(done){ + this.users[0].root.set("Map", Y.Map); this.users[0].connector.flushAll(); - applyRandomTransactions(this.users, randomMapTransactions, numberOfYMapTests); - setTimeout(()=>{ - compareAllUsers(this.users); - compareMapValues(this.users); + + var then = Promise.resolve(); + var maps = []; + for (var u of this.users) { + then = then.then(function(){ //eslint-disable-line + return u.root.get("Map"); + }).then(function(map){//eslint-disable-line + maps.push(map); + }); + } + this.maps = maps; + then.then(function(){ done(); - }, 500); + }); }); - it(`succeed after ${numberOfYMapTests} actions without flush before transactions`, function(done){ - applyRandomTransactions(this.users, randomMapTransactions, numberOfYMapTests); + it(`succeed after ${numberOfYMapTests} actions`, function(done){ + applyRandomTransactions(this.users, this.maps, randomMapTransactions, numberOfYMapTests); setTimeout(()=>{ compareAllUsers(this.users); - compareMapValues(this.users); + compareMapValues(this.maps); done(); }, 500); }); }); -/* - - var numberOfYListTests = 100; - describe("List random tests", function(){ - var randomListTests = [function* insert (root) { - var list = yield* root.get("list"); - yield* list.insert(Math.floor(Math.random() * 10), [getRandomNumber()]); - }, function* delete_(root) { - var list = yield* root.get("list"); - yield* list.delete(Math.floor(Math.random() * 10)); - }]; - beforeEach(function(){ - this.users[0].transact(function*(root){ - var list = yield* Y.List(); - yield* root.set("list", list); - }); - this.users[0].connector.flushAll(); - }); - - it(`succeeds after ${numberOfYListTests} actions`, function(){ - applyRandomTransactions(this.users, randomListTests, numberOfYListTests); - compareAllUsers(this.users); - var userList; - this.users[0].transact(function*(root){ - var list = yield* root.get("list"); - if (userList == null) { - userList = yield* list.get(); + var numberOfYArrayTests = 10; + describe("Array random tests", function(){ + var randomMapTransactions = [ + function insert (array) { + array.insert(getRandomNumber(array.toArray().length), [getRandomNumber()]); + } + ]; + function compareArrayValues(arrays){ + var firstArray; + for (var l of arrays) { + var val = l.toArray(); + if (firstArray == null) { + firstArray = val; } else { - expect(userList).toEqual(yield* list.get()); - expect(userList.length > 0).toBeTruthy(); + expect(val).toEqual(firstArray); } - }); - }); - }); + } + } + beforeEach(function(done){ + this.users[0].root.set("Array", Y.Array); + this.users[0].connector.flushAll(); - describe("Map debug tests", function(){ - beforeEach(function(){ - this.u1 = this.users[0]; - this.u2 = this.users[1]; - this.u3 = this.users[2]; - }); - it("concurrent insertions #1", function(){ - this.u1.transact(function*(root){ - var op = { - content: 1, - left: null, - right: null, - parent: root._model, - parentSub: "a" - }; - Struct.Insert.create.call(this, op); + var then = Promise.resolve(); + var arrays = []; + for (var u of this.users) { + then = then.then(function(){ //eslint-disable-line + return u.root.get("Array"); + }).then(function(array){//eslint-disable-line + arrays.push(array); + }); + } + this.arrays = arrays; + then.then(function(){ + done(); }); - compareAllUsers(this.users); + }); + it("arrays.length equals users.length", function(){ + expect(this.arrays.length).toEqual(this.users.length); + }); + it(`succeed after ${numberOfYArrayTests} actions`, function(done){ + applyRandomTransactions(this.users, this.arrays, randomMapTransactions, numberOfYArrayTests); + setTimeout(()=>{ + compareAllUsers(this.users); + compareArrayValues(this.arrays); + done(); + }, 500); }); }); - */ });