From 9c4074e3e3be92073f7e1f1860c109ac5537b49c Mon Sep 17 00:00:00 2001
From: Kevin Jahns <kevin.jahns@rwth-aachen.de>
Date: Sun, 11 Oct 2015 02:55:46 +0200
Subject: [PATCH] fixed late join issues when gc is turned off

---
 gulpfile.js                   |  2 +-
 src/Connector.js              | 46 ++++++++++--------
 src/Helper.spec.js            | 10 ++--
 src/OperationStores/Memory.js | 91 +++++++++++++++++++----------------
 src/Types/Array.spec.js       |  2 +-
 5 files changed, 85 insertions(+), 66 deletions(-)

diff --git a/gulpfile.js b/gulpfile.js
index 897ead70..2efc75fe 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -120,7 +120,7 @@ gulp.task('build:test', function () {
   if (!options.regenerator) {
     babelOptions.blacklist = 'regenerator'
   }
-  gulp.src('src/**/*.js')
+  return gulp.src('src/**/*.js')
     .pipe(sourcemaps.init())
     .pipe(babel(babelOptions))
     .pipe(sourcemaps.write())
diff --git a/src/Connector.js b/src/Connector.js
index 6f3242d5..91eb3f6a 100644
--- a/src/Connector.js
+++ b/src/Connector.js
@@ -30,6 +30,7 @@ class AbstractConnector {
     this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
     this.debug = opts.debug === true
     this.broadcastedHB = false
+    this.syncStep2 = Promise.resolve()
   }
   reconnect () {
   }
@@ -184,27 +185,34 @@ class AbstractConnector {
       let conn = this
       var broadcastHB = !this.broadcastedHB
       this.broadcastedHB = true
-      this.y.db.requestTransaction(function * () {
-        yield* this.applyDeleteSet(m.deleteSet)
-        this.store.apply(m.os)
-      })
-      this.y.db.requestTransaction(function * () {
-        var ops = yield* this.getOperations(m.stateSet)
-        if (ops.length > 0) {
-          m = {
-            type: 'update',
-            ops: ops
-          }
-          if (!broadcastHB || true) { // TODO: consider to broadcast here..
-            conn.send(sender, m)
-          } else {
-            // broadcast only once!
-            conn.broadcast(m)
-          }
-        }
+      var db = this.y.db
+      this.syncStep2 = new Promise(function (resolve) {
+        db.requestTransaction(function * () {
+          yield* this.applyDeleteSet(m.deleteSet)
+          this.store.apply(m.os)
+          db.requestTransaction(function * () {
+            var ops = yield* this.getOperations(m.stateSet)
+            if (ops.length > 0) {
+              m = {
+                type: 'update',
+                ops: ops
+              }
+              if (!broadcastHB) { // TODO: consider to broadcast here..
+                conn.send(sender, m)
+              } else {
+                // broadcast only once!
+                conn.broadcast(m)
+              }
+            }
+            resolve()
+          })
+        })
       })
     } else if (m.type === 'sync done') {
-      this._setSyncedWith(sender)
+      var self = this
+      this.syncStep2.then(function () {
+        self._setSyncedWith(sender)
+      })
     } else if (m.type === 'update') {
       if (this.forwardToSyncingClients) {
         for (var client of this.syncingClients) {
diff --git a/src/Helper.spec.js b/src/Helper.spec.js
index 58b7b0e7..9adf37e9 100644
--- a/src/Helper.spec.js
+++ b/src/Helper.spec.js
@@ -18,7 +18,7 @@ g.g = g
 
 g.YConcurrency_TestingMode = true
 
-jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000
 
 g.describeManyTimes = function describeManyTimes (times, name, f) {
   for (var i = 0; i < times; i++) {
@@ -31,12 +31,12 @@ g.describeManyTimes = function describeManyTimes (times, name, f) {
 */
 function wait (t) {
   if (t == null) {
-    t = 5
+    t = 10
   }
   return new Promise(function (resolve) {
     setTimeout(function () {
       resolve()
-    }, t)
+    }, t * 2)
   })
 }
 g.wait = wait
@@ -110,10 +110,12 @@ g.applyRandomTransactions = async(function * applyRandomTransactions (users, obj
 
 g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
   return yield wait(100)// TODO!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+  /*
   for (var i in users) {
     yield users[i].db.garbageCollect()
     yield users[i].db.garbageCollect()
   }
+  */
 })
 
 g.compareAllUsers = async(function * compareAllUsers (users) {
@@ -239,7 +241,7 @@ function async (makeGenerator) {
     try {
       return handle(generator.next())
     } catch (ex) {
-      generator.throw(ex) // TODO: check this out
+      generator.throw(ex)
       // return Promise.reject(ex)
     }
   }
diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js
index 72d5374f..ba6ba971 100644
--- a/src/OperationStores/Memory.js
+++ b/src/OperationStores/Memory.js
@@ -4,9 +4,6 @@
 class DeleteStore extends Y.utils.RBTree {
   constructor () {
     super()
-    // TODO: debugggg
-    this.mem = [];
-    this.memDS = [];
   }
   isDeleted (id) {
     var n = this.findNodeWithUpperBound(id)
@@ -18,9 +15,7 @@ class DeleteStore extends Y.utils.RBTree {
     returns the delete node
   */
   markGarbageCollected (id) {
-    this.mem.push({"gc": id});
     var n = this.markDeleted(id)
-    this.mem.pop()
     if (!n.val.gc) {
       if (n.val.id[1] < id[1]) {
         // un-extend left
@@ -65,7 +60,6 @@ class DeleteStore extends Y.utils.RBTree {
     returns the delete node
   */
   markDeleted (id) {
-    this.mem.push({"del": id});
     var n = this.findNodeWithUpperBound(id)
     if (n != null && n.val.id[0] === id[0]) {
       if (n.val.id[1] <= id[1] && id[1] < n.val.id[1] + n.val.len) {
@@ -125,8 +119,6 @@ Y.Memory = (function () {
       this.ss = store.ss
       this.os = store.os
       this.ds = store.ds
-
-      this.memDS = store.ds.memDS; // TODO: remove
     }
     * checkDeleteStoreForState (state) {
       var n = this.ds.findNodeWithUpperBound([state.user, state.clock])
@@ -149,11 +141,6 @@ Y.Memory = (function () {
         }
       }
 
-      var memAction = {
-        before: yield* this.getDeleteSet(),
-        applied: JSON.parse(JSON.stringify(ds))
-      };
-
       for (var user in ds) {
         var dv = ds[user]
         var pos = 0
@@ -215,8 +202,6 @@ Y.Memory = (function () {
           yield* this.deleteOperation(id)
         }
       }
-      memAction.after = yield* this.getDeleteSet();
-      this.memDS.push(memAction);
     }
     * isDeleted (id) {
       return this.ds.isDeleted(id)
@@ -294,43 +279,67 @@ Y.Memory = (function () {
       var res = []
       for (var op of ops) {
         res.push(yield* this.makeOperationReady(startSS, op))
-        /*
-        var state = startSS[op.id[0]] || 0
-        if ((state === op.id[1]) || true) {
-          startSS[op.id[0]] = op.id[1] + 1
-        } else {
-          throw new Error('Unexpected operation!')
-        }
-        */
       }
       return res
     }
-    * makeOperationReady (ss, op) {
+    /*
+      Here, we make op executable for the receiving user.
+
+      Notes:
+        startSS: denotes to the SV that the remote user sent
+        currSS:  denotes to the state vector that the user should have if he
+                 applies all already sent operations (increases is each step)
+
+      We face several problems:
+      * Execute op as is won't work because ops depend on each other
+       -> find a way so that they do not anymore
+      * When changing left, must not go more to the left than the origin
+      * When changing right, you have to consider that other ops may have op
+        as their origin, this means that you must not set one of these ops
+        as the new right (interdependencies of ops)
+      * can't just go to the right until you find the first known operation,
+        With currSS
+          -> interdependency of ops is a problem
+        With startSS
+          -> leads to inconsistencies when two users join at the same time.
+             Then the position depends on the order of execution -> error!
+
+        Solution:
+        -> re-create originial situation
+          -> set op.left = op.origin (which never changes)
+          -> set op.right
+               to the first operation that is known (according to startSS)
+               or to the first operation that has an origin that is not to the
+               right of op.
+          -> Enforces unique execution order -> happy user
+
+        Improvements: TODO
+          * Could set left to origin, or the first known operation
+            (startSS or currSS.. ?)
+            -> Could be necessary when I turn GC again.
+            -> Is a bad(ish) idea because it requires more computation
+    */
+    * makeOperationReady (startSS, op) {
       op = Y.Struct[op.struct].encode(op)
-      // instead of ss, you could use currSS (a ss that increments when you add an operation)
       op = Y.utils.copyObject(op)
       var o = op
-
+      var ids = [op.id]
+      // search for the new op.right
+      // it is either the first known op (according to startSS)
+      // or the o that has no origin to the right of op
+      // (this is why we use the ids array)
       while (o.right != null) {
-        // while unknown, go to the right
-        if (o.right[1] < (ss[o.right[0]] || 0)) { // && !Y.utils.compareIds(op.id, o.origin)
+        var right = yield* this.getOperation(o.right)
+        if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) {
+          return Y.utils.compareIds(id, right.origin)
+        })) {
           break
         }
-        o = yield* this.getOperation(o.right)
+        ids.push(o.right)
+        o = right
       }
-      // new right is known according to the ss
       op.right = o.right
-      /*
-      while (o.left != null) {
-        // while unknown, go to the right
-        if (o.left[1] < (ss[o.left[0]] || 0)) {
-          break
-        }
-        o = yield* this.getOperation(o.left)
-      }
-      // new left is known according to the ss
-      op.left = o.left
-      */
+      op.left = op.origin
       return op
     }
   }
diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js
index d078d613..87265bd4 100644
--- a/src/Types/Array.spec.js
+++ b/src/Types/Array.spec.js
@@ -2,7 +2,7 @@
 /* eslint-env browser,jasmine */
 
 var numberOfYArrayTests = 10
-var repeatArrayTests = 1000
+var repeatArrayTests = 1
 
 describe('Array Type', function () {
   var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll
-- 
GitLab