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 {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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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)
}

View File

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

View File

@ -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 {
/**

View File

@ -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)

View File

@ -1,3 +1,4 @@
// @ts-nocheck
import * as ID from './ID.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 { 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])
}
}
}

View File

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

View File

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