diff --git a/.eslintrc b/.eslintrc index f1f99bd4..d1b1705f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -37,6 +37,7 @@ "CustomType": true, "window": true, "document": true, - "smaller": true + "smaller": true, + "wait": true } } diff --git a/gulpfile.js b/gulpfile.js index bbf2445d..dafa90b9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -95,7 +95,9 @@ gulp.task("build", function () { .pipe(babel({ loose: "all", modules: "ignore", - blacklist: ["regenerator"] + optional: ["es7.asyncFunctions"], + blacklist: ["regenerator"], + experimental: true })) .pipe(sourcemaps.write()) .pipe(gulp.dest(".")); @@ -114,7 +116,9 @@ gulp.task("test", function () { .pipe(concat("jasmine")) .pipe(babel({ loose: "all", - modules: "ignore" + optional: ["es7.asyncFunctions"], + modules: "ignore", + experimental: true })) .pipe(uglify()) .pipe(sourcemaps.write()) @@ -132,7 +136,9 @@ gulp.task("build_jasmine_browser", function(){ .pipe(babel({ loose: "all", modules: "ignore", - blacklist: ["regenerator"] + optional: ["es7.asyncFunctions"], + blacklist: "regenerator", + experimental: true })) .pipe(sourcemaps.write()) .pipe(gulp.dest("build")); diff --git a/src/Connector.js b/src/Connector.js index 832f580a..fffe84f7 100644 --- a/src/Connector.js +++ b/src/Connector.js @@ -81,22 +81,22 @@ class AbstractConnector { //eslint-disable-line no-unused-vars // returns false, if there is no sync target // true otherwise findNextSyncTarget () { - if (this.currentSyncTarget != null && this.connections[this.currentSyncTarget].isSynced === false) { - throw new Error("The current sync has not finished!"); + if (this.currentSyncTarget != null) { + return; // "The current sync has not finished!" } var syncUser = null; for (var uid in this.connections) { - syncUser = this.connections[uid]; - if (!syncUser.isSynced) { + if (!this.connections[uid].isSynced) { + syncUser = uid; break; } } if (syncUser != null){ var conn = this; + this.currentSyncTarget = syncUser; this.y.db.requestTransaction(function*(){ - conn.currentSyncTarget = uid; - conn.send(uid, { + conn.send(syncUser, { type: "sync step 1", stateVector: yield* this.getStateVector() }); @@ -110,7 +110,6 @@ class AbstractConnector { //eslint-disable-line no-unused-vars } this.whenSyncedListeners = null; } - return false; } send (uid, message) { if (this.debug) { @@ -149,24 +148,38 @@ class AbstractConnector { //eslint-disable-line no-unused-vars type: "sync done" }); }, conn.syncingClientDuration); + } else { + conn.send(sender, { + type: "sync done" + }); } }); } else if (m.type === "sync step 2") { this.y.db.apply(m.os); let conn = this; + var broadcastHB = !this.broadcastedHB; + this.broadcastedHB = true; this.y.db.requestTransaction(function*(){ var ops = yield* this.getOperations(m.stateVector); - conn.broadcastedHB = true; if (ops.length > 0) { - conn.broadcast({ + m = { type: "update", ops: ops - }); + }; + if (!broadcastHB) { + conn.send(sender, m); + } else { + // broadcast only once! + conn.broadcast(m); + } } }); } else if (m.type === "sync done") { this.connections[sender].isSynced = true; - this.findNextSyncTarget(); + if (sender === this.currentSyncTarget) { + this.currentSyncTarget = null; + this.findNextSyncTarget(); + } } else if (m.type === "update") { if (this.forwardToSyncingClients) { for (var client of this.syncingClients) { diff --git a/src/Connectors/Test.js b/src/Connectors/Test.js index 5a41348e..9152e8ae 100644 --- a/src/Connectors/Test.js +++ b/src/Connectors/Test.js @@ -73,10 +73,26 @@ class Test extends AbstractConnector { } } flushAll () { - var c = true; - while (c) { - c = flushOne(); + var def = Promise.defer(); + // flushes may result in more created operations, + // flush until there is nothing more to flush + function nextFlush() { + var c = flushOne(); + if (c) { + while(flushOne()) { + //nop + } + wait().then(nextFlush); + } else { + wait().then(function(){ + def.resolve(); + }); + } } + // in the case that there are + // still actions that want to be performed + wait(0).then(nextFlush); + return def.promise; } flushOne() { flushOne(); diff --git a/src/Helper.spec.js b/src/Helper.spec.js index d42a1aeb..560ec628 100644 --- a/src/Helper.spec.js +++ b/src/Helper.spec.js @@ -5,6 +5,13 @@ This is "just" a compilation of functions that help to test this library! ***/ +function wait(t = 0) {//eslint-disable-line + var def = Promise.defer(); + setTimeout(function(){ + def.resolve(); + }, t); + return def.promise; +} // returns a random element of o // works on Object, and Array @@ -26,7 +33,7 @@ function getRandomNumber(n) {//eslint-disable-line return Math.floor(Math.random() * n); } -function applyRandomTransactions (users, objects, transactions, numberOfTransactions) {//eslint-disable-line +async function applyRandomTransactions (users, objects, transactions, numberOfTransactions) {//eslint-disable-line function randomTransaction (root) { var f = getRandom(transactions); f(root); @@ -39,40 +46,45 @@ function applyRandomTransactions (users, objects, transactions, numberOfTransact } else { randomTransaction(getRandom(objects)); } + wait(); } } -function compareAllUsers(users){//eslint-disable-line +async function compareAllUsers(users){//eslint-disable-line var s1, s2; + var db1 = []; function* t1(){ s1 = yield* this.getStateSet(); } function* t2(){ s2 = yield* this.getStateSet(); } - users[0].connector.flushAll(); - for (var uid = 0; uid + 1 < users.length; uid++) { - var u1 = users[uid]; - var u2 = users[uid + 1]; - u1.db.requestTransaction(t1); - u2.db.requestTransaction(t2); - expect(s1).toEqual(s2); - var db1 = []; - var db2 = []; - u1.db.os.iterate(null, null, function(o){//eslint-disable-line - db1.push(o); - }); - u2.db.os.iterate(null, null, function(o){//eslint-disable-line - db2.push(o); - }); - - for (var key in db1) { - expect(db1[key]).toEqual(db2[key]); + await users[0].connector.flushAll(); + for (var uid = 0; uid < users.length; uid++) { + if (s1 == null) { + var u = users[uid]; + u.db.requestTransaction(t1); + await wait(); + u.db.os.iterate(null, null, function(o){//eslint-disable-line + db1.push(o); + }); + } else { + var u2 = users[uid]; + u2.db.requestTransaction(t2); + await wait(); + expect(s1).toEqual(s2); + var count = 0; + u2.db.os.iterate(null, null, function(o){//eslint-disable-line + expect(db1[count++]).toEqual(o); + }); } } } -function createUsers(self, numberOfUsers, done) {//eslint-disable-line +async function createUsers(self, numberOfUsers) {//eslint-disable-line + if (globalRoom.users[0] != null) {//eslint-disable-line + await globalRoom.users[0].flushAll();//eslint-disable-line + } //destroy old users for (var u in globalRoom.users) {//eslint-disable-line globalRoom.users[u].y.destroy()//eslint-disable-line @@ -91,8 +103,5 @@ function createUsers(self, numberOfUsers, done) {//eslint-disable-line } })); } - Promise.all(promises).then( users => { - self.users = users; - done(); - }); + self.users = await Promise.all(promises); } diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js index 2038848a..93c6546b 100644 --- a/src/OperationStores/Memory.js +++ b/src/OperationStores/Memory.js @@ -126,7 +126,7 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars requestTransaction (_makeGen : Function) { if (!this.transactionInProgress) { this.transactionInProgress = true; - window.setTimeout(() => { + setTimeout(() => { var makeGen = _makeGen; while (makeGen != null) { var t = new Transaction(this); diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js index 5c0d845e..b7cf1334 100644 --- a/src/Types/Array.spec.js +++ b/src/Types/Array.spec.js @@ -1,116 +1,98 @@ /* @flow */ /*eslint-env browser,jasmine */ -var numberOfYArrayTests = 20; +var numberOfYArrayTests = 80; describe("Array Type", function(){ - jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; - beforeEach(function(done){ - createUsers(this, 5, done); + var y1, y2, y3, flushAll; + + jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000; + beforeEach(async function(done){ + await createUsers(this, 5); + y1 = this.users[0].root; + y2 = this.users[1].root; + y3 = this.users[2].root; + flushAll = this.users[0].connector.flushAll; + done(); + }); + afterEach(async function(done) { + await compareAllUsers(this.users); + done(); }); describe("Basic tests", function(){ - it("insert three elements", 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("insert three elements, try re-get property", async function(done){ + var array = await y1.set("Array", Y.Array); + array.insert(0, [1, 2, 3]); + array = await y1.get("Array"); // re-get property + expect(array.toArray()).toEqual([1, 2, 3]); + done(); }); - it("Basic insert in array (handle three conflicts)", function(done){ - var y = this.users[0]; + it("Basic insert in array (handle three conflicts)", async function(done){ 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(); - }); + await y1.set("Array", Y.Array); + await flushAll(); + (l1 = await y1.get("Array")).insert(0, [0]); + (l2 = await y2.get("Array")).insert(0, [1]); + (l3 = await y3.get("Array")).insert(0, [2]); + await flushAll(); + expect(l1.toArray()).toEqual(l2.toArray()); + expect(l2.toArray()).toEqual(l3.toArray()); + done(); }); - it("Basic insert&delete in array (handle three conflicts)", function(done){ - var y = this.users[0]; + it("Basic insert&delete in array (handle three conflicts)", async function(done){ 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(); - }); + l1 = await y1.set("Array", Y.Array); + l1.insert(0, ["x", "y", "z"]); + await flushAll(); + l1.insert(1, [0]); + l2 = await y2.get("Array"); + l2.delete(0); + l2.delete(1); + l3 = await y3.get("Array"); + l3.insert(1, [2]); + await flushAll(); + expect(l1.toArray()).toEqual(l2.toArray()); + expect(l2.toArray()).toEqual(l3.toArray()); + expect(l2.toArray()).toEqual([0, 2, "y"]); + done(); }); - it("Basic insert. Then delete the whole array", function(done){ - var y = this.users[0]; + it("Basic insert. Then delete the whole array", async function(done){ 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(); - }); + l1 = await y1.set("Array", Y.Array); + l1.insert(0, ["x", "y", "z"]); + await flushAll(); + l1.delete(0, 3); + l2 = await y2.get("Array"); + l3 = await y3.get("Array"); + await flushAll(); + expect(l1.toArray()).toEqual(l2.toArray()); + expect(l2.toArray()).toEqual(l3.toArray()); + expect(l2.toArray()).toEqual([]); + done(); }); - it("throw insert & delete events", function(done){ - this.users[0].root.set("array", Y.Array).then(function(array){ - var event; - array.observe(function(e){ - event = e; - }); - array.insert(0, [0]); - expect(event).toEqual([{ - type: "insert", - object: array, - index: 0, - length: 1 - }]); - array.delete(0); - expect(event).toEqual([{ - type: "delete", - object: array, - index: 0, - length: 1 - }]); - done(); + it("throw insert & delete events", async function(done){ + var array = await this.users[0].root.set("array", Y.Array); + var event; + array.observe(function(e){ + event = e; }); + array.insert(0, [0]); + expect(event).toEqual([{ + type: "insert", + object: array, + index: 0, + length: 1 + }]); + array.delete(0); + expect(event).toEqual([{ + type: "delete", + object: array, + index: 0, + length: 1 + }]); + await wait(50); + done(); }); }); describe(`${numberOfYArrayTests} Random tests`, function(){ @@ -136,34 +118,26 @@ describe("Array Type", function(){ } } } - beforeEach(function(done){ - this.users[0].root.set("Array", Y.Array); - this.users[0].connector.flushAll(); + beforeEach(async function(done){ + await this.users[0].root.set("Array", Y.Array); + await flushAll(); - 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); - }); + var promises = []; + for (var u = 0; u < this.users.length; u++) { + promises.push(this.users[u].root.get("Array")); } - this.arrays = arrays; - then.then(function(){ - done(); - }); + this.arrays = await Promise.all(promises); + done(); }); - it("arrays.length equals users.length", function(){ + it("arrays.length equals users.length", async function(done){ expect(this.arrays.length).toEqual(this.users.length); + done(); }); - it(`succeed after ${numberOfYArrayTests} actions`, function(done){ - applyRandomTransactions(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests); - setTimeout(()=>{ - compareAllUsers(this.users); - compareArrayValues(this.arrays); - done(); - }, 500); + it(`succeed after ${numberOfYArrayTests} actions`, async function(done){ + await applyRandomTransactions(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests); + await flushAll(); + await compareArrayValues(this.arrays); + done(); }); }); });