From 29f3f3f7224c84281fdc7ca617b990f645c80e37 Mon Sep 17 00:00:00 2001
From: Kevin Jahns <kevin.jahns@rwth-aachen.de>
Date: Sun, 18 Oct 2015 03:07:34 +0200
Subject: [PATCH] added offline editing demo :star2:

---
 Examples/OfflineEditing/index.html | 12 ++++
 Examples/OfflineEditing/index.js   | 50 +++++++++++++++++
 src/Database.js                    | 89 ++++++++++++++++++------------
 src/Database.spec.js               |  2 +-
 src/Databases/IndexedDB.js         | 23 ++++++++
 src/Struct.js                      |  2 +-
 src/Transaction.js                 | 11 +++-
 src/Types/Array.js                 | 18 +++---
 src/Types/Array.spec.js            |  2 +-
 src/Types/Map.spec.js              |  2 +-
 src/y.js                           |  2 +-
 11 files changed, 165 insertions(+), 48 deletions(-)
 create mode 100644 Examples/OfflineEditing/index.html
 create mode 100644 Examples/OfflineEditing/index.js

diff --git a/Examples/OfflineEditing/index.html b/Examples/OfflineEditing/index.html
new file mode 100644
index 00000000..9bfd4525
--- /dev/null
+++ b/Examples/OfflineEditing/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+  <button id="button">Disconnect</button>
+  <h1 id="contenteditable" contentEditable></h1>
+  <textarea style="width:80%;" rows=40 id="textfield"></textarea>
+
+  <script src="../../node_modules/simplewebrtc/simplewebrtc.bundle.js"></script>
+  <script src="../../y.js"></script>
+  <script src="./index.js"></script>
+</body>
+</html>
diff --git a/Examples/OfflineEditing/index.js b/Examples/OfflineEditing/index.js
new file mode 100644
index 00000000..78a347b7
--- /dev/null
+++ b/Examples/OfflineEditing/index.js
@@ -0,0 +1,50 @@
+/* global Y */
+
+// create a shared object. This function call will return a promise!
+Y({
+  db: {
+    name: 'IndexedDB',
+    namespace: 'offlineEditingDemo'
+  },
+  connector: {
+    name: 'WebRTC',
+    room: 'offlineEditingDemo',
+    debug: true
+  }
+}).then(function (yconfig) {
+  // yconfig holds all the information about the shared object
+  window.yconfig = yconfig
+  // yconfig.root holds the shared element
+  window.y = yconfig.root
+
+  // now we bind the textarea and the contenteditable h1 element
+  // to a shared element
+  var textarea = document.getElementById('textfield')
+  var contenteditable = document.getElementById('contenteditable')
+  yconfig.root.observePath(['text'], function (text) {
+    // every time the 'text' property of the yconfig.root changes,
+    // this function is called. Then we bind it to the html elements
+    if (text != null) {
+      // when the text property is deleted, text may be undefined!
+      // This is why we have to check if text exists..
+      text.bind(textarea)
+      text.bind(contenteditable)
+    }
+  })
+  // create a shared TextBind
+  var textpromise = yconfig.root.get('text')
+  if (textpromise == null) {
+    yconfig.root.set('text', Y.TextBind)
+  }
+  // We also provide a button for disconnecting/reconnecting the shared element
+  var button = document.querySelector('#button')
+  button.onclick = function () {
+    if (button.innerText === 'Disconnect') {
+      yconfig.disconnect()
+      button.innerText = 'Reconnect'
+    } else {
+      yconfig.reconnect()
+      button.innerText = 'Disconnect'
+    }
+  }
+})
diff --git a/src/Database.js b/src/Database.js
index cc6df6be..c5e3e194 100644
--- a/src/Database.js
+++ b/src/Database.js
@@ -36,6 +36,8 @@ class AbstractDatabase {
     // wont be kept in memory.
     this.initializedTypes = {}
     this.whenUserIdSetListener = null
+    this.waitingTransactions = []
+    this.transactionInProgress = false
     if (typeof YConcurrency_TestingMode !== 'undefined') {
       this.executeOrder = []
     }
@@ -46,7 +48,7 @@ class AbstractDatabase {
     function garbageCollect () {
       return new Promise((resolve) => {
         os.requestTransaction(function * () {
-          if (os.y.connector.isSynced) {
+          if (os.y.connector != null && os.y.connector.isSynced) {
             for (var i in os.gc2) {
               var oid = os.gc2[i]
               yield* this.garbageCollectOperation(oid)
@@ -65,8 +67,6 @@ class AbstractDatabase {
     if (this.gcTimeout > 0) {
       garbageCollect()
     }
-    this.waitingTransactions = []
-    this.transactionInProgress = false
   }
   addToDebug () {
     if (typeof YConcurrency_TestingMode !== 'undefined') {
@@ -252,47 +252,68 @@ class AbstractDatabase {
     this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
     if (op.struct === 'Delete') {
       yield* Y.Struct.Delete.execute.call(this, op)
+      yield* this.store.operationAdded(this, op)
     } else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) {
       yield* Y.Struct[op.struct].execute.call(this, op)
       yield* this.addOperation(op)
       yield* this.store.operationAdded(this, op)
-
-      // Delete if DS says this is actually deleted
-      if (yield* this.isDeleted(op.id)) {
-        yield* Y.Struct['Delete'].execute.call(this, {struct: 'Delete', target: op.id})
-      }
     }
   }
   // called by a transaction when an operation is added
   * operationAdded (transaction, op) {
-    // increase SS
-    var o = op
-    var state = yield* transaction.getState(op.id[0])
-    while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) {
-      // either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
-      state.clock++
-      yield* transaction.checkDeleteStoreForState(state)
-      o = yield* transaction.os.findNext(o.id)
-    }
-    yield* transaction.setState(state)
-
-    // notify whenOperation listeners (by id)
-    var sid = JSON.stringify(op.id)
-    var l = this.listenersById[sid]
-    delete this.listenersById[sid]
-
-    if (l != null) {
-      for (var key in l) {
-        var listener = l[key]
-        if (--listener.missing === 0) {
-          this.whenOperationsExist([], listener.op)
+    if (op.struct === 'Delete') {
+      var target = yield* transaction.getOperation(op.target)
+      if (target != null) {
+        var type = transaction.store.initializedTypes[JSON.stringify(target.parent)]
+        if (type != null) {
+          yield* type._changed(transaction, {
+            struct: 'Delete',
+            target: op.target
+          })
+        }
+      }
+    } else {
+      // increase SS
+      var o = op
+      var state = yield* transaction.getState(op.id[0])
+      while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) {
+        // either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
+        state.clock++
+        yield* transaction.checkDeleteStoreForState(state)
+        o = yield* transaction.os.findNext(o.id)
+      }
+      yield* transaction.setState(state)
+
+      // notify whenOperation listeners (by id)
+      var sid = JSON.stringify(op.id)
+      var l = this.listenersById[sid]
+      delete this.listenersById[sid]
+
+      if (l != null) {
+        for (var key in l) {
+          var listener = l[key]
+          if (--listener.missing === 0) {
+            this.whenOperationsExist([], listener.op)
+          }
+        }
+      }
+      var t = this.initializedTypes[JSON.stringify(op.parent)]
+      // notify parent, if it has been initialized as a custom type
+      if (t != null) {
+        yield* t._changed(transaction, Y.utils.copyObject(op))
+      }
+
+      // Delete if DS says this is actually deleted
+      if (!op.deleted && (yield* transaction.isDeleted(op.id))) {
+        var delop = {
+          struct: 'Delete',
+          target: op.id
+        }
+        yield* Y.Struct['Delete'].execute.call(transaction, delop)
+        if (t != null) {
+          yield* t._changed(transaction, delop)
         }
       }
-    }
-    // notify parent, if it has been initialized as a custom type
-    var t = this.initializedTypes[JSON.stringify(op.parent)]
-    if (t != null && !op.deleted) {
-      yield* t._changed(transaction, Y.utils.copyObject(op))
     }
   }
   getNextRequest () {
diff --git a/src/Database.spec.js b/src/Database.spec.js
index 7a64e313..2305bf21 100644
--- a/src/Database.spec.js
+++ b/src/Database.spec.js
@@ -1,7 +1,7 @@
 /* global Y, async, databases */
 /* eslint-env browser,jasmine,console */
 
-for (var database of databases) {
+for (let database of databases) {
   describe(`Database (${database})`, function () {
     var store
     describe('DeleteStore', function () {
diff --git a/src/Databases/IndexedDB.js b/src/Databases/IndexedDB.js
index 87331765..16823321 100644
--- a/src/Databases/IndexedDB.js
+++ b/src/Databases/IndexedDB.js
@@ -83,6 +83,29 @@ Y.IndexedDB = (function () {
           yield this.ss.store.clear()
         })
       }
+      var operationsToAdd = []
+      window.addEventListener('storage', function (event) {
+        if (event.key === '__YJS__' + store.namespace) {
+          operationsToAdd.push(event.newValue)
+          if (operationsToAdd.length === 1) {
+            store.requestTransaction(function * () {
+              var add = operationsToAdd
+              operationsToAdd = []
+              for (var i in add) {
+                // don't call the localStorage event twice..
+                var op = yield* this.getOperation(JSON.parse(add[i]).id)
+                yield* this.store.operationAdded(this, op, true)
+              }
+            })
+          }
+        }
+      }, false)
+    }
+    * operationAdded (transaction, op, noAdd) {
+      yield* super.operationAdded(transaction, op)
+      if (!noAdd) {
+        window.localStorage['__YJS__' + this.namespace] = JSON.stringify(op)
+      }
     }
     transact (makeGen) {
       var transaction = this.db != null ? new Transaction(this) : null
diff --git a/src/Struct.js b/src/Struct.js
index 9f625417..fb1f72e1 100644
--- a/src/Struct.js
+++ b/src/Struct.js
@@ -36,7 +36,7 @@ var Struct = {
       return [] // [op.target]
     },
     execute: function * (op) {
-      yield* this.deleteOperation(op.target)
+      return yield* this.deleteOperation(op.target)
     }
   },
   Insert: {
diff --git a/src/Transaction.js b/src/Transaction.js
index def2ef66..8084777c 100644
--- a/src/Transaction.js
+++ b/src/Transaction.js
@@ -132,6 +132,7 @@ class Transaction {
   */
   * deleteOperation (targetId, preventCallType) {
     var target = yield* this.getOperation(targetId)
+    var callType = false
 
     if (target == null || !target.deleted) {
       yield* this.markDeleted(targetId)
@@ -139,8 +140,10 @@ class Transaction {
 
     if (target != null && target.gc == null) {
       if (!target.deleted) {
+        callType = true
         // set deleted & notify type
         target.deleted = true
+        /*
         if (!preventCallType) {
           var type = this.store.initializedTypes[JSON.stringify(target.parent)]
           if (type != null) {
@@ -150,6 +153,7 @@ class Transaction {
             })
           }
         }
+        */
         // delete containing lists
         if (target.start != null) {
           // TODO: don't do it like this .. -.-
@@ -187,6 +191,7 @@ class Transaction {
       ) {
         yield* this.setOperation(right)
       }
+      return callType
     }
   }
   /*
@@ -468,7 +473,11 @@ class Transaction {
       var del = deletions[i]
       var id = [del[0], del[1]]
       // always try to delete..
-      yield* this.deleteOperation(id)
+      var addOperation = yield* this.deleteOperation(id)
+      if (addOperation) {
+        // TODO:.. really .. here? You could prevent calling all these functions in operationAdded
+        yield* this.store.operationAdded(this, {struct: 'Delete', target: id})
+      }
       if (del[2]) {
         // gc
         yield* this.garbageCollectOperation(id)
diff --git a/src/Types/Array.js b/src/Types/Array.js
index f2059343..4445fcf1 100644
--- a/src/Types/Array.js
+++ b/src/Types/Array.js
@@ -37,14 +37,16 @@
             })
           } else if (op.struct === 'Delete') {
             let pos = this.idArray.indexOf(JSON.stringify(op.target))
-            this.idArray.splice(pos, 1)
-            this.valArray.splice(pos, 1)
-            userEvents.push({
-              type: 'delete',
-              object: this,
-              index: pos,
-              length: 1
-            })
+            if (pos >= 0) {
+              this.idArray.splice(pos, 1)
+              this.valArray.splice(pos, 1)
+              userEvents.push({
+                type: 'delete',
+                object: this,
+                index: pos,
+                length: 1
+              })
+            }
           } else {
             throw new Error('Unexpected struct!')
           }
diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js
index 21fc75ea..bdb3d4fb 100644
--- a/src/Types/Array.spec.js
+++ b/src/Types/Array.spec.js
@@ -4,7 +4,7 @@
 var numberOfYArrayTests = 50
 var repeatArrayTests = 2
 
-for (var database of databases) {
+for (let database of databases) {
   describe(`Array Type (DB: ${database})`, function () {
     var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll
 
diff --git a/src/Types/Map.spec.js b/src/Types/Map.spec.js
index 4fd8e59a..a338fbf9 100644
--- a/src/Types/Map.spec.js
+++ b/src/Types/Map.spec.js
@@ -4,7 +4,7 @@
 var numberOfYMapTests = 40
 var repeatMapTeasts = 2
 
-for (var database of databases) {
+for (let database of databases) {
   describe(`Map Type (DB: ${database})`, function () {
     var y1, y2, y3, y4, flushAll
 
diff --git a/src/y.js b/src/y.js
index f6bb96bf..5f5266d1 100644
--- a/src/y.js
+++ b/src/y.js
@@ -23,7 +23,7 @@ class YConfig {
         type: 'Map',
         map: {}
       }
-      yield* this.addOperation(model)
+      yield* this.store.tryExecute.call(this, model)
       var root = yield* this.getType(model.id)
       this.store.y.root = root
       callback()