restructure EventHandler

This commit is contained in:
Kevin Jahns 2019-04-04 13:50:00 +02:00
parent 6578727c9c
commit 8dbd2c4696
11 changed files with 187 additions and 179 deletions

View File

@ -33,7 +33,7 @@ export class ItemFormat extends AbstractItem {
* @param {ID} id * @param {ID} id
* @param {AbstractItem | null} left * @param {AbstractItem | null} left
* @param {AbstractItem | null} right * @param {AbstractItem | null} right
* @param {AbstractType<any> parent * @param {AbstractType<any>} parent
* @param {string | null} parentSub * @param {string | null} parentSub
*/ */
copy (id, left, right, parent, parentSub) { copy (id, left, right, parent, parentSub) {

View File

@ -34,7 +34,7 @@ export class ItemJSON extends AbstractItem {
* @param {ID} id * @param {ID} id
* @param {AbstractItem | null} left * @param {AbstractItem | null} left
* @param {AbstractItem | null} right * @param {AbstractItem | null} right
* @param {AbstractType<any> parent * @param {AbstractType<any>} parent
* @param {string | null} parentSub * @param {string | null} parentSub
*/ */
copy (id, left, right, parent, parentSub) { copy (id, left, right, parent, parentSub) {

View File

@ -33,7 +33,7 @@ export class ItemString extends AbstractItem {
* @param {ID} id * @param {ID} id
* @param {AbstractItem | null} left * @param {AbstractItem | null} left
* @param {AbstractItem | null} right * @param {AbstractItem | null} right
* @param {AbstractType<any> parent * @param {AbstractType<any>} parent
* @param {string | null} parentSub * @param {string | null} parentSub
*/ */
copy (id, left, right, parent, parentSub) { copy (id, left, right, parent, parentSub) {

View File

@ -67,7 +67,7 @@ export class ItemType extends AbstractItem {
* @param {ID} id * @param {ID} id
* @param {AbstractItem | null} left * @param {AbstractItem | null} left
* @param {AbstractItem | null} right * @param {AbstractItem | null} right
* @param {AbstractType<any> parent * @param {AbstractType<any>} parent
* @param {string | null} parentSub * @param {string | null} parentSub
* @return {AbstractItem} TODO, returns itemtype * @return {AbstractItem} TODO, returns itemtype
*/ */
@ -150,7 +150,7 @@ export class ItemTypeRef extends AbstractItemRef {
super(decoder, id, info) super(decoder, id, info)
const typeRef = decoding.readVarUint(decoder) const typeRef = decoding.readVarUint(decoder)
/** /**
* @type {AbstractType<any> * @type {AbstractType<any>}
*/ */
this.type = typeRefs[typeRef](decoder) this.type = typeRefs[typeRef](decoder)
} }

View File

@ -3,8 +3,8 @@
*/ */
import { Y } from '../utils/Y.js' // eslint-disable-line import { Y } from '../utils/Y.js' // eslint-disable-line
import { EventHandler } from '../utils/EventHandler.js' import * as eventHandler from '../utils/EventHandler.js'
import { YEvent } from '../utils/YEvent.js' import { YEvent } from '../utils/YEvent.js' // eslint-disable-line
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
import { Encoder } from 'lib0/encoding.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 { ID, createID } from '../utils/ID.js' // eslint-disable-line
import { getItemCleanStart, getItemCleanEnd } from '../utils/StructStore.js' import { getItemCleanStart, getItemCleanEnd } from '../utils/StructStore.js'
import * as iterator from 'lib0/iterator.js' import * as iterator from 'lib0/iterator.js'
import * as error from 'lib0/error.js'
/** /**
* @template EventType * @template EventType
@ -43,8 +44,16 @@ export class AbstractType {
*/ */
this._y = null this._y = null
this._length = 0 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 * @private
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified. * @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/ */
_callObserver (transaction, parentSubs) { _callObserver (transaction, parentSubs) {
this._callEventHandler(transaction, new YEvent(this, transaction)) throw error.methodUnimplemented()
} }
/** /**
@ -108,8 +119,8 @@ export class AbstractType {
* @param {any} event * @param {any} event
*/ */
_callEventHandler (transaction, event) { _callEventHandler (transaction, event) {
eventHandler.callEventListeners(this._eH, [event, transaction])
const changedParentTypes = transaction.changedParentTypes const changedParentTypes = transaction.changedParentTypes
this._eventHandler.callEventListeners(transaction, event)
/** /**
* @type {AbstractType<EventType>} * @type {AbstractType<EventType>}
*/ */
@ -127,37 +138,37 @@ export class AbstractType {
/** /**
* Observe all events that are created on this type. * 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) { observe (f) {
this._eventHandler.addEventListener(f) eventHandler.addEventListener(this._eH, f)
} }
/** /**
* Observe all events that are created by this type and its children. * 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) { observeDeep (f) {
this._deepEventHandler.addEventListener(f) eventHandler.addEventListener(this._dEH, f)
} }
/** /**
* Unregister an observer function. * Unregister an observer function.
* *
* @param {Function} f Observer function * @param {function(EventType,Transaction):void} f Observer function
*/ */
unobserve (f) { unobserve (f) {
this._eventHandler.removeEventListener(f) eventHandler.removeEventListener(this._eH, f)
} }
/** /**
* Unregister an observer function. * Unregister an observer function.
* *
* @param {Function} f Observer function * @param {function(Array<YEvent>,Transaction):void} f Observer function
*/ */
unobserveDeep (f) { unobserveDeep (f) {
this._deepEventHandler.removeEventListener(f) eventHandler.removeEventListener(this._dEH, f)
} }
/** /**

View File

@ -9,7 +9,7 @@ import * as decoding from 'lib0/decoding.js'
/** /**
* You can manage binding to a custom type with YXmlHook. * You can manage binding to a custom type with YXmlHook.
* *
* @public * @extends {YMap<any>}
*/ */
export class YXmlHook extends YMap { export class YXmlHook extends YMap {
/** /**

View File

@ -1,70 +1,66 @@
/** import { Transaction } from './Transaction.js' // eslint-disable-line
* @module utils import { YEvent } from './YEvent.js' // eslint-disable-line
*/ import * as f from 'lib0/function.js'
/** /**
* General event handler implementation. * General event handler implementation.
*
* @template ARG0, ARG1
*/ */
export class EventHandler { export class EventHandler {
constructor () { constructor () {
this.eventListeners = [] /**
} * @type {Array<function(ARG0, ARG1):void>}
*/
/** this.l = []
* 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)
}
}
} }
} }
/**
* @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)

View File

@ -1,3 +1,4 @@
// @ts-nocheck
import * as ID from './ID.js' import * as ID from './ID.js'
import { isParentOf } from './isParentOf.js' import { isParentOf } from './isParentOf.js'

View File

@ -15,6 +15,7 @@ import { YText } from '../types/YText.js'
import { YMap } from '../types/YMap.js' import { YMap } from '../types/YMap.js'
import { YXmlFragment } from '../types/YXmlElement.js' import { YXmlFragment } from '../types/YXmlElement.js'
import { YEvent } from './YEvent.js' // eslint-disable-line import { YEvent } from './YEvent.js' // eslint-disable-line
import * as eventHandler from './EventHandler.js'
/** /**
* A Yjs instance handles the state of shared data. * A Yjs instance handles the state of shared data.
@ -74,108 +75,107 @@ export class Y extends Observable {
} }
try { try {
f(this._transaction) f(this._transaction)
} catch (e) { } finally {
console.error(e) if (initialCall) {
} const transaction = this._transaction
if (initialCall) { this._transaction = null
const transaction = this._transaction // only call event listeners / observers if anything changed
this._transaction = null const transactionChangedContent = transaction.changedParentTypes.size !== 0
// only call event listeners / observers if anything changed if (transactionChangedContent) {
const transactionChangedContent = transaction.changedParentTypes.size !== 0 this.emit('beforeObserverCalls', [this, this._transaction])
if (transactionChangedContent) { // emit change events on changed types
this.emit('beforeObserverCalls', [this, this._transaction]) transaction.changed.forEach((subs, itemtype) => {
// emit change events on changed types itemtype._callObserver(transaction, subs)
transaction.changed.forEach((subs, itemtype) => { })
itemtype._callObserver(transaction, subs) transaction.changedParentTypes.forEach((events, type) => {
}) events = events
transaction.changedParentTypes.forEach((events, type) => { .filter(event =>
events = events event.target._item === null || !event.target._item.deleted
.filter(event => )
event.target._item === null || !event.target._item.deleted events
) .forEach(event => {
events event.currentTarget = type
.forEach(event => { })
event.currentTarget = type // we don't need to check for events.length
}) // because we know it has at least one element
// we don't have to check for events.length eventHandler.callEventListeners(type._dEH, [events, transaction])
// 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])
// when all changes & events are processed, emit afterTransaction event // transaction cleanup
this.emit('afterTransaction', [this, transaction]) const store = transaction.y.store
// transaction cleanup const ds = transaction.deleteSet
const store = transaction.y.store // replace deleted items with ItemDeleted / GC
const ds = transaction.deleteSet sortAndMergeDeleteSet(ds)
// replace deleted items with ItemDeleted / GC
sortAndMergeDeleteSet(ds)
/**
* @type {Set<ItemDeleted|GC>}
*/
const replacedItems = new Set()
for (const [client, deleteItems] of ds.clients) {
/** /**
* @type {Array<AbstractStruct>} * @type {Set<ItemDeleted|GC>}
*/ */
// @ts-ignore const replacedItems = new Set()
const structs = store.clients.get(client) for (const [client, deleteItems] of ds.clients) {
for (let di = 0; di < deleteItems.length; di++) { /**
const deleteItem = deleteItems[di] * @type {Array<AbstractStruct>}
for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) { */
const struct = structs[si] // @ts-ignore
if (deleteItem.clock + deleteItem.len < struct.id.clock) { const structs = store.clients.get(client)
break for (let di = 0; di < deleteItems.length; di++) {
} const deleteItem = deleteItems[di]
if (struct.deleted && struct instanceof AbstractItem) { for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) {
// check if we can GC const struct = structs[si]
replacedItems.add(struct.gc(this)) 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 {Array<AbstractStruct>} structs * @param {number} pos
* @param {number} pos */
*/ const tryToMergeWithLeft = (structs, pos) => {
const tryToMergeWithLeft = (structs, pos) => { const left = structs[pos - 1]
const left = structs[pos - 1] const right = structs[pos]
const right = structs[pos] if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.deleted === right.deleted && left.constructor === right.constructor) { if (left.mergeWith(right)) {
if (left.mergeWith(right)) { structs.splice(pos, 1)
structs.splice(pos, 1) }
} }
} }
} // on all affected store.clients props, try to merge
// on all affected store.clients props, try to merge for (const [client, clock] of transaction.stateUpdates) {
for (const [client, clock] of transaction.stateUpdates) { /**
/** * @type {Array<AbstractStruct>}
* @type {Array<AbstractStruct>} */
*/ // @ts-ignore
// @ts-ignore const structs = store.clients.get(client)
const structs = store.clients.get(client) // we iterate from right to left so we can safely remove entries
// 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--) {
for (let i = structs.length - 1; i >= math.max(findIndexSS(structs, clock), 1); i--) { tryToMergeWithLeft(structs, i)
tryToMergeWithLeft(structs, i) }
} }
} // try to merge replacedItems
// try to merge replacedItems for (const replacedItem of replacedItems) {
for (const replacedItem of replacedItems) { const id = replacedItem.id
const id = replacedItem.id const client = id.client
const client = id.client const clock = id.clock
const clock = id.clock /**
/** * @type {Array<AbstractStruct>}
* @type {Array<AbstractStruct>} */
*/ // @ts-ignore
// @ts-ignore const structs = store.clients.get(client)
const structs = store.clients.get(client) const replacedStructPos = findIndexSS(structs, clock)
const replacedStructPos = findIndexSS(structs, clock) if (replacedStructPos + 1 < structs.length) {
if (replacedStructPos + 1 < structs.length) { tryToMergeWithLeft(structs, replacedStructPos + 1)
tryToMergeWithLeft(structs, replacedStructPos + 1) }
} if (replacedStructPos > 0) {
if (replacedStructPos > 0) { tryToMergeWithLeft(structs, replacedStructPos)
tryToMergeWithLeft(structs, replacedStructPos) }
} }
this.emit('afterTransactionCleanup', [this, transaction])
} }
this.emit('afterTransactionCleanup', [this, transaction])
} }
} }
} }

View File

@ -81,7 +81,7 @@ export class YEvent {
* console.log(path) // might look like => [2, 'key1'] * console.log(path) // might look like => [2, 'key1']
* child === type.get(path[0]).get(path[1]) * child === type.get(path[0]).get(path[1])
* *
* @param {AbstractType<any> parent * @param {AbstractType<any>} parent
* @param {AbstractItem} child target * @param {AbstractItem} child target
* @return {Array<string|number>} Path to the target * @return {Array<string|number>} Path to the target
*/ */

View File

@ -76,13 +76,13 @@ export class AbsolutePosition {
} }
/** /**
* @param {AbstractType<any> type * @param {AbstractType<any>} type
* @param {number} offset * @param {number} offset
*/ */
export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset) export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset)
/** /**
* @param {AbstractType<any> type * @param {AbstractType<any>} type
* @param {ID.ID|null} item * @param {ID.ID|null} item
*/ */
export const createRelativePosition = (type, item) => { export const createRelativePosition = (type, item) => {
@ -99,7 +99,7 @@ export const createRelativePosition = (type, item) => {
/** /**
* Create a relativePosition based on a absolute position. * 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. * @param {number} offset The absolute position.
* @return {RelativePosition} * @return {RelativePosition}
*/ */