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} */