From 8dbd2c4696f2c1d48d7bf34be26cb59d18a98eb9 Mon Sep 17 00:00:00 2001
From: Kevin Jahns <kevin.jahns@protonmail.com>
Date: Thu, 4 Apr 2019 13:50:00 +0200
Subject: [PATCH] restructure EventHandler

---
 src/structs/ItemFormat.js     |   2 +-
 src/structs/ItemJSON.js       |   2 +-
 src/structs/ItemString.js     |   2 +-
 src/structs/ItemType.js       |   4 +-
 src/types/AbstractType.js     |  41 +++++---
 src/types/YXmlHook.js         |   2 +-
 src/utils/EventHandler.js     | 120 +++++++++++-----------
 src/utils/UndoManager.js      |   1 +
 src/utils/Y.js                | 184 +++++++++++++++++-----------------
 src/utils/YEvent.js           |   2 +-
 src/utils/relativePosition.js |   6 +-
 11 files changed, 187 insertions(+), 179 deletions(-)

diff --git a/src/structs/ItemFormat.js b/src/structs/ItemFormat.js
index 0eea68dc..943c27c5 100644
--- a/src/structs/ItemFormat.js
+++ b/src/structs/ItemFormat.js
@@ -33,7 +33,7 @@ export class ItemFormat extends AbstractItem {
    * @param {ID} id
    * @param {AbstractItem | null} left
    * @param {AbstractItem | null} right
-   * @param {AbstractType<any> parent
+   * @param {AbstractType<any>} parent
    * @param {string | null} parentSub
    */
   copy (id, left, right, parent, parentSub) {
diff --git a/src/structs/ItemJSON.js b/src/structs/ItemJSON.js
index 3de53299..9271b966 100644
--- a/src/structs/ItemJSON.js
+++ b/src/structs/ItemJSON.js
@@ -34,7 +34,7 @@ export class ItemJSON extends AbstractItem {
    * @param {ID} id
    * @param {AbstractItem | null} left
    * @param {AbstractItem | null} right
-   * @param {AbstractType<any> parent
+   * @param {AbstractType<any>} parent
    * @param {string | null} parentSub
    */
   copy (id, left, right, parent, parentSub) {
diff --git a/src/structs/ItemString.js b/src/structs/ItemString.js
index b443f440..e0088efe 100644
--- a/src/structs/ItemString.js
+++ b/src/structs/ItemString.js
@@ -33,7 +33,7 @@ export class ItemString extends AbstractItem {
    * @param {ID} id
    * @param {AbstractItem | null} left
    * @param {AbstractItem | null} right
-   * @param {AbstractType<any> parent
+   * @param {AbstractType<any>} parent
    * @param {string | null} parentSub
    */
   copy (id, left, right, parent, parentSub) {
diff --git a/src/structs/ItemType.js b/src/structs/ItemType.js
index 32e75155..c065c615 100644
--- a/src/structs/ItemType.js
+++ b/src/structs/ItemType.js
@@ -67,7 +67,7 @@ export class ItemType extends AbstractItem {
    * @param {ID} id
    * @param {AbstractItem | null} left
    * @param {AbstractItem | null} right
-   * @param {AbstractType<any> parent
+   * @param {AbstractType<any>} parent
    * @param {string | null} parentSub
    * @return {AbstractItem} TODO, returns itemtype
    */
@@ -150,7 +150,7 @@ export class ItemTypeRef extends AbstractItemRef {
     super(decoder, id, info)
     const typeRef = decoding.readVarUint(decoder)
     /**
-     * @type {AbstractType<any>
+     * @type {AbstractType<any>}
      */
     this.type = typeRefs[typeRef](decoder)
   }
diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js
index fd75f153..27924220 100644
--- a/src/types/AbstractType.js
+++ b/src/types/AbstractType.js
@@ -3,8 +3,8 @@
  */
 
 import { Y } from '../utils/Y.js' // eslint-disable-line
-import { EventHandler } from '../utils/EventHandler.js'
-import { YEvent } from '../utils/YEvent.js'
+import * as eventHandler from '../utils/EventHandler.js'
+import { YEvent } from '../utils/YEvent.js' // eslint-disable-line
 import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
 import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
 import { Encoder } from 'lib0/encoding.js' // eslint-disable-line
@@ -16,6 +16,7 @@ import { ItemBinary } from '../structs/ItemBinary.js'
 import { ID, createID } from '../utils/ID.js' // eslint-disable-line
 import { getItemCleanStart, getItemCleanEnd } from '../utils/StructStore.js'
 import * as iterator from 'lib0/iterator.js'
+import * as error from 'lib0/error.js'
 
 /**
  * @template EventType
@@ -43,8 +44,16 @@ export class AbstractType {
      */
     this._y = null
     this._length = 0
-    this._eventHandler = new EventHandler()
-    this._deepEventHandler = new EventHandler()
+    /**
+     * Event handlers
+     * @type {eventHandler.EventHandler<EventType,Transaction>}
+     */
+    this._eH = eventHandler.create()
+    /**
+     * Deep event handlers
+     * @type {eventHandler.EventHandler<Array<YEvent>,Transaction>}
+     */
+    this._dEH = eventHandler.create()
   }
 
   /**
@@ -89,14 +98,16 @@ export class AbstractType {
   }
 
   /**
-   * Creates YEvent and calls observers.
+   * Creates YEvent and calls _callEventHandler.
+   * Must be implemented by each type.
+   * @todo Rename to _createEvent
    * @private
    *
    * @param {Transaction} transaction
    * @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
    */
   _callObserver (transaction, parentSubs) {
-    this._callEventHandler(transaction, new YEvent(this, transaction))
+    throw error.methodUnimplemented()
   }
 
   /**
@@ -108,8 +119,8 @@ export class AbstractType {
    * @param {any} event
    */
   _callEventHandler (transaction, event) {
+    eventHandler.callEventListeners(this._eH, [event, transaction])
     const changedParentTypes = transaction.changedParentTypes
-    this._eventHandler.callEventListeners(transaction, event)
     /**
      * @type {AbstractType<EventType>}
      */
@@ -127,37 +138,37 @@ export class AbstractType {
   /**
    * Observe all events that are created on this type.
    *
-   * @param {function(EventType):void} f Observer function
+   * @param {function(EventType, Transaction):void} f Observer function
    */
   observe (f) {
-    this._eventHandler.addEventListener(f)
+    eventHandler.addEventListener(this._eH, f)
   }
 
   /**
    * Observe all events that are created by this type and its children.
    *
-   * @param {function(Array<YEvent>):void} f Observer function
+   * @param {function(Array<YEvent>,Transaction):void} f Observer function
    */
   observeDeep (f) {
-    this._deepEventHandler.addEventListener(f)
+    eventHandler.addEventListener(this._dEH, f)
   }
 
   /**
    * Unregister an observer function.
    *
-   * @param {Function} f Observer function
+   * @param {function(EventType,Transaction):void} f Observer function
    */
   unobserve (f) {
-    this._eventHandler.removeEventListener(f)
+    eventHandler.removeEventListener(this._eH, f)
   }
 
   /**
    * Unregister an observer function.
    *
-   * @param {Function} f Observer function
+   * @param {function(Array<YEvent>,Transaction):void} f Observer function
    */
   unobserveDeep (f) {
-    this._deepEventHandler.removeEventListener(f)
+    eventHandler.removeEventListener(this._dEH, f)
   }
 
   /**
diff --git a/src/types/YXmlHook.js b/src/types/YXmlHook.js
index 75b38e15..95a89f1d 100644
--- a/src/types/YXmlHook.js
+++ b/src/types/YXmlHook.js
@@ -9,7 +9,7 @@ import * as decoding from 'lib0/decoding.js'
 /**
  * You can manage binding to a custom type with YXmlHook.
  *
- * @public
+ * @extends {YMap<any>}
  */
 export class YXmlHook extends YMap {
   /**
diff --git a/src/utils/EventHandler.js b/src/utils/EventHandler.js
index 11a11759..f6e89d85 100644
--- a/src/utils/EventHandler.js
+++ b/src/utils/EventHandler.js
@@ -1,70 +1,66 @@
-/**
- * @module utils
- */
+import { Transaction } from './Transaction.js' // eslint-disable-line
+import { YEvent } from './YEvent.js' // eslint-disable-line
+import * as f from 'lib0/function.js'
 
 /**
  * General event handler implementation.
+ *
+ * @template ARG0, ARG1
  */
 export class EventHandler {
   constructor () {
-    this.eventListeners = []
-  }
-
-  /**
-   * To prevent memory leaks, call this method when the eventListeners won't be
-   * used anymore.
-   */
-  destroy () {
-    this.eventListeners = null
-  }
-
-  /**
-   * Adds an event listener that is called when
-   * {@link EventHandler#callEventListeners} is called.
-   *
-   * @param {Function} f The event handler.
-   */
-  addEventListener (f) {
-    this.eventListeners.push(f)
-  }
-
-  /**
-   * Removes an event listener.
-   *
-   * @param {Function} f The event handler that was added with
-   *                     {@link EventHandler#addEventListener}
-   */
-  removeEventListener (f) {
-    this.eventListeners = this.eventListeners.filter(g => f !== g)
-  }
-
-  /**
-   * Removes all event listeners.
-   */
-  removeAllEventListeners () {
-    this.eventListeners = []
-  }
-
-  /**
-   * Call all event listeners that were added via
-   * {@link EventHandler#addEventListener}.
-   *
-   * @param {Transaction} transaction The transaction object
-   * @param {YEvent} event An event object that describes the change on a type.
-   */
-  callEventListeners (transaction, event) {
-    for (var i = 0; i < this.eventListeners.length; i++) {
-      try {
-        const f = this.eventListeners[i]
-        f(event, transaction)
-      } catch (e) {
-        /*
-          Your observer threw an error. This error was caught so that Yjs
-          can ensure data consistency! In order to debug this error you
-          have to check "Pause On Caught Exceptions" in developer tools.
-        */
-        console.error(e)
-      }
-    }
+    /**
+     * @type {Array<function(ARG0, ARG1):void>}
+     */
+    this.l = []
   }
 }
+
+/**
+ * @template ARG0,ARG1
+ * @returns {EventHandler<ARG0,ARG1>}
+ */
+export const create = () => new EventHandler()
+
+/**
+ * Adds an event listener that is called when
+ * {@link EventHandler#callEventListeners} is called.
+ *
+ * @template ARG0,ARG1
+ * @param {EventHandler<ARG0,ARG1>} eventHandler
+ * @param {function(ARG0,ARG1):void} f The event handler.
+ */
+export const addEventListener = (eventHandler, f) =>
+  eventHandler.l.push(f)
+
+/**
+ * Removes an event listener.
+ *
+ * @template ARG0,ARG1
+ * @param {EventHandler<ARG0,ARG1>} eventHandler
+ * @param {function(ARG0,ARG1):void} f The event handler that was added with
+ *                     {@link EventHandler#addEventListener}
+ */
+export const removeEventListener = (eventHandler, f) => {
+  eventHandler.l = eventHandler.l.filter(g => f !== g)
+}
+
+/**
+ * Removes all event listeners.
+ * @template ARG0,ARG1
+ * @param {EventHandler<ARG0,ARG1>} eventHandler
+ */
+export const removeAllEventListeners = eventHandler => {
+  eventHandler.l.length = 0
+}
+
+/**
+ * Call all event listeners that were added via
+ * {@link EventHandler#addEventListener}.
+ *
+ * @template ARG0,ARG1
+ * @param {EventHandler<ARG0,ARG1>} eventHandler
+ * @param {[ARG0,ARG1]} args
+ */
+export const callEventListeners = (eventHandler, args) =>
+  f.callAll(eventHandler.l, args)
diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js
index 7b03a5ab..1e1f8b8a 100644
--- a/src/utils/UndoManager.js
+++ b/src/utils/UndoManager.js
@@ -1,3 +1,4 @@
+// @ts-nocheck
 
 import * as ID from './ID.js'
 import { isParentOf } from './isParentOf.js'
diff --git a/src/utils/Y.js b/src/utils/Y.js
index 829edad5..615f4914 100644
--- a/src/utils/Y.js
+++ b/src/utils/Y.js
@@ -15,6 +15,7 @@ import { YText } from '../types/YText.js'
 import { YMap } from '../types/YMap.js'
 import { YXmlFragment } from '../types/YXmlElement.js'
 import { YEvent } from './YEvent.js' // eslint-disable-line
+import * as eventHandler from './EventHandler.js'
 
 /**
  * A Yjs instance handles the state of shared data.
@@ -74,108 +75,107 @@ export class Y extends Observable {
     }
     try {
       f(this._transaction)
-    } catch (e) {
-      console.error(e)
-    }
-    if (initialCall) {
-      const transaction = this._transaction
-      this._transaction = null
-      // only call event listeners / observers if anything changed
-      const transactionChangedContent = transaction.changedParentTypes.size !== 0
-      if (transactionChangedContent) {
-        this.emit('beforeObserverCalls', [this, this._transaction])
-        // emit change events on changed types
-        transaction.changed.forEach((subs, itemtype) => {
-          itemtype._callObserver(transaction, subs)
-        })
-        transaction.changedParentTypes.forEach((events, type) => {
-          events = events
-            .filter(event =>
-              event.target._item === null || !event.target._item.deleted
-            )
-          events
-            .forEach(event => {
-              event.currentTarget = type
-            })
-          // we don't have to check for events.length
-          // because there is no way events is empty..
-          type._deepEventHandler.callEventListeners(transaction, events)
-        })
-        // when all changes & events are processed, emit afterTransaction event
-        this.emit('afterTransaction', [this, transaction])
-        // transaction cleanup
-        const store = transaction.y.store
-        const ds = transaction.deleteSet
-        // replace deleted items with ItemDeleted / GC
-        sortAndMergeDeleteSet(ds)
-        /**
-         * @type {Set<ItemDeleted|GC>}
-         */
-        const replacedItems = new Set()
-        for (const [client, deleteItems] of ds.clients) {
+    } finally {
+      if (initialCall) {
+        const transaction = this._transaction
+        this._transaction = null
+        // only call event listeners / observers if anything changed
+        const transactionChangedContent = transaction.changedParentTypes.size !== 0
+        if (transactionChangedContent) {
+          this.emit('beforeObserverCalls', [this, this._transaction])
+          // emit change events on changed types
+          transaction.changed.forEach((subs, itemtype) => {
+            itemtype._callObserver(transaction, subs)
+          })
+          transaction.changedParentTypes.forEach((events, type) => {
+            events = events
+              .filter(event =>
+                event.target._item === null || !event.target._item.deleted
+              )
+            events
+              .forEach(event => {
+                event.currentTarget = type
+              })
+            // we don't need to check for events.length
+            // because we know it has at least one element
+            eventHandler.callEventListeners(type._dEH, [events, transaction])
+          })
+          // when all changes & events are processed, emit afterTransaction event
+          this.emit('afterTransaction', [this, transaction])
+          // transaction cleanup
+          const store = transaction.y.store
+          const ds = transaction.deleteSet
+          // replace deleted items with ItemDeleted / GC
+          sortAndMergeDeleteSet(ds)
           /**
-           * @type {Array<AbstractStruct>}
+           * @type {Set<ItemDeleted|GC>}
            */
-          // @ts-ignore
-          const structs = store.clients.get(client)
-          for (let di = 0; di < deleteItems.length; di++) {
-            const deleteItem = deleteItems[di]
-            for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) {
-              const struct = structs[si]
-              if (deleteItem.clock + deleteItem.len < struct.id.clock) {
-                break
-              }
-              if (struct.deleted && struct instanceof AbstractItem) {
-                // check if we can GC
-                replacedItems.add(struct.gc(this))
+          const replacedItems = new Set()
+          for (const [client, deleteItems] of ds.clients) {
+            /**
+             * @type {Array<AbstractStruct>}
+             */
+            // @ts-ignore
+            const structs = store.clients.get(client)
+            for (let di = 0; di < deleteItems.length; di++) {
+              const deleteItem = deleteItems[di]
+              for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) {
+                const struct = structs[si]
+                if (deleteItem.clock + deleteItem.len < struct.id.clock) {
+                  break
+                }
+                if (struct.deleted && struct instanceof AbstractItem) {
+                  // check if we can GC
+                  replacedItems.add(struct.gc(this))
+                }
               }
             }
           }
-        }
-        /**
-         * @param {Array<AbstractStruct>} structs
-         * @param {number} pos
-         */
-        const tryToMergeWithLeft = (structs, pos) => {
-          const left = structs[pos - 1]
-          const right = structs[pos]
-          if (left.deleted === right.deleted && left.constructor === right.constructor) {
-            if (left.mergeWith(right)) {
-              structs.splice(pos, 1)
+          /**
+           * @param {Array<AbstractStruct>} structs
+           * @param {number} pos
+           */
+          const tryToMergeWithLeft = (structs, pos) => {
+            const left = structs[pos - 1]
+            const right = structs[pos]
+            if (left.deleted === right.deleted && left.constructor === right.constructor) {
+              if (left.mergeWith(right)) {
+                structs.splice(pos, 1)
+              }
             }
           }
-        }
-        // on all affected store.clients props, try to merge
-        for (const [client, clock] of transaction.stateUpdates) {
-          /**
-           * @type {Array<AbstractStruct>}
-           */
-          // @ts-ignore
-          const structs = store.clients.get(client)
-          // we iterate from right to left so we can safely remove entries
-          for (let i = structs.length - 1; i >= math.max(findIndexSS(structs, clock), 1); i--) {
-            tryToMergeWithLeft(structs, i)
+          // on all affected store.clients props, try to merge
+          for (const [client, clock] of transaction.stateUpdates) {
+            /**
+             * @type {Array<AbstractStruct>}
+             */
+            // @ts-ignore
+            const structs = store.clients.get(client)
+            // we iterate from right to left so we can safely remove entries
+            for (let i = structs.length - 1; i >= math.max(findIndexSS(structs, clock), 1); i--) {
+              tryToMergeWithLeft(structs, i)
+            }
           }
-        }
-        // try to merge replacedItems
-        for (const replacedItem of replacedItems) {
-          const id = replacedItem.id
-          const client = id.client
-          const clock = id.clock
-          /**
-           * @type {Array<AbstractStruct>}
-           */
-          // @ts-ignore
-          const structs = store.clients.get(client)
-          const replacedStructPos = findIndexSS(structs, clock)
-          if (replacedStructPos + 1 < structs.length) {
-            tryToMergeWithLeft(structs, replacedStructPos + 1)
-          }
-          if (replacedStructPos > 0) {
-            tryToMergeWithLeft(structs, replacedStructPos)
+          // try to merge replacedItems
+          for (const replacedItem of replacedItems) {
+            const id = replacedItem.id
+            const client = id.client
+            const clock = id.clock
+            /**
+             * @type {Array<AbstractStruct>}
+             */
+            // @ts-ignore
+            const structs = store.clients.get(client)
+            const replacedStructPos = findIndexSS(structs, clock)
+            if (replacedStructPos + 1 < structs.length) {
+              tryToMergeWithLeft(structs, replacedStructPos + 1)
+            }
+            if (replacedStructPos > 0) {
+              tryToMergeWithLeft(structs, replacedStructPos)
+            }
           }
+          this.emit('afterTransactionCleanup', [this, transaction])
         }
-        this.emit('afterTransactionCleanup', [this, transaction])
       }
     }
   }
diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js
index 999c1f88..c2bb85ce 100644
--- a/src/utils/YEvent.js
+++ b/src/utils/YEvent.js
@@ -81,7 +81,7 @@ export class YEvent {
  *   console.log(path) // might look like => [2, 'key1']
  *   child === type.get(path[0]).get(path[1])
  *
- * @param {AbstractType<any> parent
+ * @param {AbstractType<any>} parent
  * @param {AbstractItem} child target
  * @return {Array<string|number>} Path to the target
  */
diff --git a/src/utils/relativePosition.js b/src/utils/relativePosition.js
index cc094bbb..4c48d097 100644
--- a/src/utils/relativePosition.js
+++ b/src/utils/relativePosition.js
@@ -76,13 +76,13 @@ export class AbsolutePosition {
 }
 
 /**
- * @param {AbstractType<any> type
+ * @param {AbstractType<any>} type
  * @param {number} offset
  */
 export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset)
 
 /**
- * @param {AbstractType<any> type
+ * @param {AbstractType<any>} type
  * @param {ID.ID|null} item
  */
 export const createRelativePosition = (type, item) => {
@@ -99,7 +99,7 @@ export const createRelativePosition = (type, item) => {
 /**
  * Create a relativePosition based on a absolute position.
  *
- * @param {AbstractType<any> type The base type (e.g. YText or YArray).
+ * @param {AbstractType<any>} type The base type (e.g. YText or YArray).
  * @param {number} offset The absolute position.
  * @return {RelativePosition}
  */