From c8ded24842f775f60d73b592fcaae2cdf1839dec Mon Sep 17 00:00:00 2001
From: Kevin Jahns <kevin.jahns@rwth-aachen.de>
Date: Sun, 26 Jul 2015 03:13:13 +0000
Subject: [PATCH] started implementing the garbage collector

---
 gulpfile.js                           |  2 +-
 src/Helper.spec.js                    |  7 +++--
 src/OperationStore.js                 | 42 +++++++++++++++++++++++++--
 src/OperationStores/IndexedDB.js      |  2 +-
 src/OperationStores/IndexedDB.spec.js |  2 +-
 src/OperationStores/Memory.js         | 18 ++++++++----
 src/Struct.js                         | 12 ++++++++
 src/Types/Array.spec.js               | 21 ++++++++++++++
 src/Types/Map.js                      | 19 +++++++-----
 src/y.js                              |  2 +-
 10 files changed, 106 insertions(+), 21 deletions(-)

diff --git a/gulpfile.js b/gulpfile.js
index 10477644..3f8e5c6d 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -133,7 +133,7 @@ gulp.task('build_jasmine_browser', function () {
     .pipe(gulp.dest('build'))
 })
 
-gulp.task('develop', ['build_jasmine_browser', 'build'], function () {
+gulp.task('develop', ['build_jasmine_browser'], function () {
   gulp.watch(files.test, ['build_jasmine_browser'])
   // gulp.watch(files.test, ["test"])
   gulp.watch(files.test, ['build'])
diff --git a/src/Helper.spec.js b/src/Helper.spec.js
index 67dafc82..21bc5918 100644
--- a/src/Helper.spec.js
+++ b/src/Helper.spec.js
@@ -99,7 +99,9 @@ async function compareAllUsers(users){//eslint-disable-line
         var d = ds[j]
         for (var i = 0; i < d.len; i++) {
           var o = yield* this.getOperation([d.id[0], d.id[1] + i])
-          expect(o.deleted).toBeTruthy()
+          if (o != null) {
+            expect(o.deleted).toBeTruthy()
+          }
         }
       }
     })
@@ -139,7 +141,8 @@ async function createUsers(self, numberOfUsers) {//eslint-disable-line
   for (var i = 0; i < numberOfUsers; i++) {
     promises.push(Y({
       db: {
-        name: 'Memory'
+        name: 'Memory',
+        gcTimeout: -1
       },
       connector: {
         name: 'Test',
diff --git a/src/OperationStore.js b/src/OperationStore.js
index 3b66e21b..d00cf3a6 100644
--- a/src/OperationStore.js
+++ b/src/OperationStore.js
@@ -39,7 +39,7 @@ class AbstractTransaction { // eslint-disable-line no-unused-vars
 }
 
 class AbstractOperationStore { // eslint-disable-line no-unused-vars
-  constructor (y) {
+  constructor (y, opts) {
     this.y = y
     // E.g. this.listenersById[id] : Array<Listener>
     this.listenersById = {}
@@ -62,6 +62,44 @@ class AbstractOperationStore { // eslint-disable-line no-unused-vars
     this.initializedTypes = {}
     this.whenUserIdSetListener = null
     this.waitingOperations = new RBTree()
+
+    this.gc1 = [] // first stage
+    this.gc2 = [] // second stage -> after that, kill it
+    this.gcTimeout = opts.gcTimeout || 5000
+    var os = this
+    function garbageCollect () {
+      os.requestTransaction(function * () {
+        for (var i in os.gc2) {
+          var oid = os.gc2[i]
+          var o = yield* this.getOperation(oid)
+          if (o.left != null) {
+            var left = yield* this.getOperation(o.left)
+            left.right = o.right
+          }
+          if (o.right != null) {
+            var right = yield* this.getOperation(o.right)
+            right.left = o.left
+          }
+          yield* this.removeOperation(o.id)
+        }
+        os.gc2 = os.gc1
+        os.gc1 = []
+        if (os.gcTimeout > 0) {
+          os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
+        }
+      })
+    }
+    this.garbageCollect = garbageCollect
+    if (this.gcTimeout > 0) {
+      garbageCollect()
+    }
+  }
+  addToGarbageCollector (op) {
+    this.gc1.push(op)
+  }
+  destroy () {
+    clearInterval(this.gcInterval)
+    this.gcInterval = null
   }
   setUserId (userId) {
     this.userId = userId
@@ -201,7 +239,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) {
+    if (t != null && !op.deleted) {
       yield* t._changed(transaction, copyObject(op))
     }
   }
diff --git a/src/OperationStores/IndexedDB.js b/src/OperationStores/IndexedDB.js
index b8e24cbc..d742fe08 100644
--- a/src/OperationStores/IndexedDB.js
+++ b/src/OperationStores/IndexedDB.js
@@ -81,7 +81,7 @@ Y.IndexedDB = (function () { // eslint-disable-line
   }
   class OperationStore extends AbstractOperationStore { // eslint-disable-line no-undef
     constructor (y, opts) {
-      super(y)
+      super(y, opts)
       if (opts == null) {
         opts = {}
       }
diff --git a/src/OperationStores/IndexedDB.spec.js b/src/OperationStores/IndexedDB.spec.js
index 159d4ede..b065f773 100644
--- a/src/OperationStores/IndexedDB.spec.js
+++ b/src/OperationStores/IndexedDB.spec.js
@@ -6,7 +6,7 @@ if (typeof window !== 'undefined') {
   describe('IndexedDB', function () {
     var ob
     beforeAll(function () {
-      ob = new Y.IndexedDB(null, {namespace: 'Test'})
+      ob = new Y.IndexedDB(null, {namespace: 'Test', gcTimeout: -1})
     })
 
     it('can add and get operation', function (done) {
diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js
index 385923c0..1127aa87 100644
--- a/src/OperationStores/Memory.js
+++ b/src/OperationStores/Memory.js
@@ -238,8 +238,8 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars
     }
   }
   class OperationStore extends AbstractOperationStore { // eslint-disable-line no-undef
-    constructor (y) {
-      super(y)
+    constructor (y, opts) {
+      super(y, opts)
       this.os = new RBTree()
       this.ss = {}
       this.waitingTransactions = []
@@ -249,10 +249,10 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars
     logTable () {
       this.os.logTable()
     }
-    requestTransaction (_makeGen) {
+    requestTransaction (_makeGen, requestNow = false) {
       if (!this.transactionInProgress) {
         this.transactionInProgress = true
-        setTimeout(() => {
+        var transact = () => {
           var makeGen = _makeGen
           while (makeGen != null) {
             var t = new Transaction(this)
@@ -268,12 +268,18 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars
             makeGen = this.waitingTransactions.shift()
           }
           this.transactionInProgress = false
-        }, 0)
+        }
+        if (!requestNow) {
+          setTimeout(transact, 0)
+        } else {
+          transact()
+        }
       } else {
         this.waitingTransactions.push(_makeGen)
       }
     }
-    * removeDatabase () { // eslint-disable-line
+    * destroy () { // eslint-disable-line
+      super.destroy()
       delete this.os
     }
   }
diff --git a/src/Struct.js b/src/Struct.js
index a21a256f..ad5f22bc 100644
--- a/src/Struct.js
+++ b/src/Struct.js
@@ -31,6 +31,18 @@ var Struct = {
       var target = yield* this.getOperation(op.target)
       if (!target.deleted) {
         target.deleted = true
+        if (target.left !== null && (yield* this.getOperation(target.left)).deleted) {
+          this.store.addToGarbageCollector(target.id)
+          target.gc = true
+        }
+        if (target.right !== null) {
+          var right = yield* this.getOperation(target.right)
+          if (right.deleted && right.gc == null) {
+            this.store.addToGarbageCollector(right.id)
+            right.gc = true
+            yield* this.setOperation(right)
+          }
+        }
         yield* this.setOperation(target)
         this.ds.delete(target.id)
         var t = this.store.initializedTypes[JSON.stringify(target.parent)]
diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js
index 9b0fbb49..7444d248 100644
--- a/src/Types/Array.spec.js
+++ b/src/Types/Array.spec.js
@@ -179,6 +179,27 @@ describe('Array Type', function () {
       await wait(50)
       done()
     })
+    it('garbage collects', async function (done) {
+      var l1, l2, l3
+      l1 = await y1.set('Array', Y.Array)
+      l1.insert(0, ['x', 'y', 'z'])
+      await flushAll()
+      yconfig1.disconnect()
+      l1.delete(0, 3)
+      l2 = await y2.get('Array')
+      await wait()
+      yconfig1.reconnect()
+      await wait()
+      l3 = await y3.get('Array')
+      await flushAll()
+      yconfig1.db.garbageCollect()
+      yconfig1.db.garbageCollect()
+      yconfig1.db.logTable()
+      expect(l1.toArray()).toEqual(l2.toArray())
+      expect(l2.toArray()).toEqual(l3.toArray())
+      expect(l2.toArray()).toEqual([])
+      done()
+    })
   })
   describe(`Random tests`, function () {
     var randomArrayTransactions = [
diff --git a/src/Types/Map.js b/src/Types/Map.js
index e7f0debf..57e4872b 100644
--- a/src/Types/Map.js
+++ b/src/Types/Map.js
@@ -34,10 +34,18 @@
             if (op.left === null) {
               if (op.opContent != null) {
                 delete this.contents[key]
-                this.opContents[key] = op.opContent
+                if (op.deleted) {
+                  delete this.opContents[key]
+                } else {
+                  this.opContents[key] = op.opContent
+                }
               } else {
                 delete this.opContents[key]
-                this.contents[key] = op.content
+                if (op.deleted) {
+                  delete this.contents[key]
+                } else {
+                  this.contents[key] = op.content
+                }
               }
               this.map[key] = op.id
               var insertEvent = {
@@ -54,11 +62,8 @@
             }
           } else if (op.struct === 'Delete') {
             if (compareIds(this.map[key], op.target)) {
-              if (this.opContents[key] != null) {
-                delete this.opContents[key]
-              } else {
-                delete this.contents[key]
-              }
+              delete this.opContents[key]
+              delete this.contents[key]
               var deleteEvent = {
                 name: key,
                 object: this,
diff --git a/src/y.js b/src/y.js
index dc86d354..9ff86034 100644
--- a/src/y.js
+++ b/src/y.js
@@ -40,7 +40,7 @@ class YConfig { // eslint-disable-line no-unused-vars
   }
   destroy () {
     this.connector.disconnect()
-    this.db.removeDatabase()
+    this.db.destroy()
     this.connector = null
     this.db = null
     this.transact = function () {