diff --git a/src/index.js b/src/index.js index b99154e2..ec2b4c5d 100644 --- a/src/index.js +++ b/src/index.js @@ -2,9 +2,7 @@ export { Y } from './utils/Y.js' export { UndoManager } from './utils/UndoManager.js' export { Transaction } from './utils/Transaction.js' - -export { Item, stringifyItemID } from './structs/Item.js' -export { Delete } from './structs/Delete.js' +export { Delete } from './Delete.js' export { ItemJSON } from './structs/ItemJSON.js' export { ItemString } from './structs/ItemString.js' export { ItemFormat } from './structs/ItemFormat.js' @@ -21,10 +19,8 @@ export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/ export { getRelativePosition, fromRelativePosition, equal as equalRelativePosition } from './utils/relativePosition.js' -export { ID, createID, RootFakeUserID, RootID } from './utils/ID.js' -export { DeleteStore, DSNode } from './utils/DeleteStore.js' +export { ID, createID } from './utils/ID.js' +export { DeleteStore, DSNode } from './utils/DeleteSet.js/index.js' export { deleteItemRange } from './utils/structManipulation.js' export { integrateRemoteStructs } from './utils/integrateRemoteStructs.js' export { isParentOf } from './utils/isParentOf.js' - -export * from './utils/structReferences.js' diff --git a/src/structs/AbstractItem.js b/src/structs/AbstractItem.js new file mode 100644 index 00000000..a9c73563 --- /dev/null +++ b/src/structs/AbstractItem.js @@ -0,0 +1,570 @@ +/** + * @module structs + */ + +import { readID, createID, writeID, writeNullID, ID, createNextID } from '../utils/ID.js' // eslint-disable-line +import { Delete } from '../Delete.js' +import { writeStructToTransaction } from '../utils/structEncoding.js' +import { GC } from './GC.js' +import * as encoding from 'lib0/encoding.js' +import * as decoding from 'lib0/decoding.js' +import { ItemType } from './ItemType.js' // eslint-disable-line +import { AbstractType } from '../types/AbstractType.js' +import { Y } from '../utils/Y.js' // eslint-disable-line +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line +import * as maplib from 'lib0/map.js' +import * as set from 'lib0/set.js' +import * as binary from 'lib0/binary.js' +import { AbstractRef, AbstractStruct } from './AbstractStruct.js' // eslint-disable-line +import * as error from 'lib0/error.js' + +/** + * Stringify an item id. + * + * @param { ID } id + * @return {string} + */ +export const stringifyID = id => `(${id.client},${id.clock})` + +/** + * Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent). + * + * @param {AbstractItem | null} item + * @return {string} + */ +export const stringifyItemID = item => + item === null ? '()' : (item.id != null ? stringifyID(item.id) : 'y') + +/** + * Helper utility to convert an item to a readable format. + * + * @param {String} name The name of the item class (YText, ItemString, ..). + * @param {AbstractItem} item The item instance. + * @param {String} [append] Additional information to append to the returned + * string. + * @return {String} A readable string that represents the item object. + * + */ +export const logItemHelper = (name, item, append) => { + const left = item.left !== null ? stringifyID(item.left.lastId) : '()' + const origin = item.origin !== null ? stringifyID(item.origin.lastId) : '()' + return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item.right)},parent:${stringifyItemID(item.parent)},parentSub:${item.parentSub}${append !== undefined ? ' - ' + append : ''})` +} + +/** + * Split leftItem into two items + * @param {AbstractItem} leftItem + * @param {Y} y + * @param {number} diff + * @return {any} + */ +export const splitItem = (leftItem, diff) => { + const id = leftItem.id + // create rightItem + const rightItem = leftItem.copy(createID(id.client, id.clock + diff), leftItem, leftItem.rightOrigin, leftItem.parent, leftItem.parentSub) + rightItem.right = leftItem.right + if (leftItem.deleted) { + rightItem.deleted = true + } + // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing) + leftItem.right = rightItem + // update right + if (rightItem.right !== null) { + rightItem.right.left = rightItem + } + // update all origins to the right + // search all relevant items to the right and update origin + // if origin is not it foundOrigins, we don't have to search any longer + const foundOrigins = new Set() + foundOrigins.add(leftItem) + let o = rightItem.right + while (o !== null && foundOrigins.has(o.origin)) { + if (o.origin === leftItem) { + o.origin = rightItem + } + foundOrigins.add(o) + o = o.right + } +} + +/** + * Abstract class that represents any content. + */ +export class AbstractItem extends AbstractStruct { + /** + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {AbstractType | null} parent + * @param {string | null} parentSub + */ + constructor (id, left, right, parent, parentSub) { + if (left !== null) { + parent = left.parent + parentSub = left.parentSub + } else if (right !== null) { + parent = right.parent + parentSub = right.parentSub + } else if (parent === null) { + error.throwUnexpectedCase() + } + super(id) + /** + * The item that was originally to the left of this item. + * @type {AbstractItem | null} + * @readonly + */ + this.origin = left + /** + * The item that is currently to the left of this item. + * @type {AbstractItem | null} + */ + this.left = left + /** + * The item that is currently to the right of this item. + * @type {AbstractItem | null} + */ + this.right = right + /** + * The item that was originally to the right of this item. + * @readonly + * @type {AbstractItem | null} + */ + this.rightOrigin = right + /** + * The parent type. + * @type {AbstractType} + * @readonly + */ + this.parent = parent + /** + * If the parent refers to this item with some kind of key (e.g. YMap, the + * key is specified here. The key is then used to refer to the list in which + * to insert this item. If `parentSub = null` type._start is the list in + * which to insert to. Otherwise it is `parent._map`. + * @type {String | null} + * @readonly + */ + this.parentSub = parentSub + /** + * Whether this item was deleted or not. + * @type {Boolean} + */ + this.deleted = false + /** + * If this type's effect is reundone this type refers to the type that undid + * this operation. + * @type {AbstractItem | null} + */ + this.redone = null + } + + /** + * @param {Transaction} transaction + */ + integrate (transaction) { + const y = transaction.y + const id = this.id + const parent = this.parent + const parentSub = this.parentSub + const length = this.length + const left = this.left + const right = this.right + // integrate + const parentType = parent !== null ? parent.type : maplib.setTfUndefined(y.share, parentSub, () => new AbstractType()) + if (y.ss.getState(id.client) !== id.clock) { + throw new Error('Expected other operation') + } + y.ss.setState(id.client, id.clock + length) + transaction.added.add(this) + /* + # $this has to find a unique position between origin and the next known character + # case 1: $origin equals $o.origin: the $creator parameter decides if left or right + # let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4 + # o2,o3 and o4 origin is 1 (the position of o2) + # there is the case that $this.creator < o2.creator, but o3.creator < $this.creator + # then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex + # therefore $this would be always to the right of o3 + # case 2: $origin < $o.origin + # if current $this insert_position > $o origin: $this ins + # else $insert_position will not change + # (maybe we encounter case 1 later, then this will be to the right of $o) + # case 3: $origin > $o.origin + # $this insert_position is to the left of $o (forever!) + */ + // handle conflicts + /** + * @type {AbstractItem|null} + */ + let o + // set o to the first conflicting item + if (left !== null) { + o = left.right + } else if (this.parentSub !== null) { + o = parentType._map.get(parentSub) || null + } else { + o = parentType._start + } + const conflictingItems = new Set() + const itemsBeforeOrigin = new Set() + // Let c in conflictingItems, b in itemsBeforeOrigin + // ***{origin}bbbb{this}{c,b}{c,b}{o}*** + // Note that conflictingItems is a subset of itemsBeforeOrigin + while (o !== null && o !== right) { + itemsBeforeOrigin.add(o) + conflictingItems.add(o) + if (this.origin === o.origin) { + // case 1 + if (o.id.client < id.client) { + this.left = o + conflictingItems.clear() + } + } else if (itemsBeforeOrigin.has(o.origin)) { + // case 2 + if (!conflictingItems.has(o.origin)) { + this.left = o + conflictingItems.clear() + } + } else { + break + } + // TODO: try to use right_origin instead. + // Then you could basically omit conflictingItems! + // Note: you probably can't use right_origin in every case.. only when setting _left + o = o.right + } + // reconnect left/right + update parent map/start if necessary + if (left !== null) { + const right = left.right + this.right = right + left.right = this + if (right !== null) { + right.left = this + } + } else { + let r + if (parentSub !== null) { + const pmap = parentType._map + r = pmap.get(parentSub) || null + pmap.set(parentSub, this) + } else { + r = parentType._start + parentType._start = this + } + this.right = r + if (r !== null) { + r._left = this + } + } + // adjust the length of parent + if (parentSub === null && this.countable) { + parentType._length += length + } + if (parent !== null && parent.deleted) { + this.delete(transaction, false, true) + } + y.os.put(this) + if (parent !== null) { + maplib.setTfUndefined(transaction.changed, parent, set.create).add(parentSub) + } + + writeStructToTransaction(y._transaction, this) + } + + /** + * Returns the next non-deleted item + * @private + */ + get next () { + let n = this.right + while (n !== null && n._deleted) { + n = n.right + } + return n + } + + /** + * Returns the previous non-deleted item + * @private + */ + get prev () { + let n = this.left + while (n !== null && n._deleted) { + n = n.left + } + return n + } + + /** + * Creates an Item with the same effect as this Item (without position effect) + * + * @param {ID} id + * @param {AbstractItem|null} left + * @param {AbstractItem|null} right + * @param {ItemType|null} parent + * @param {string|null} parentSub + * @return {AbstractItem} + */ + copy (id, left, right, parent, parentSub) { + throw new Error('unimplemented') + } + + /** + * Redoes the effect of this operation. + * + * @param {Y} y The Yjs instance. + * @param {Set} redoitems + * + * @private + */ + redo (y, redoitems) { + if (this.redone !== null) { + return this.redone + } + /** + * @type {any} + */ + let parent = this.parent + if (parent === null) { + return + } + let left, right + if (this.parentSub === null) { + // Is an array item. Insert at the old position + left = this.left + right = this + } else { + // Is a map item. Insert at the start + left = null + right = parent.type._map.get(this.parentSub) + right._delete(y) + } + // make sure that parent is redone + if (parent._deleted === true && parent.redone === null) { + // try to undo parent if it will be undone anyway + if (!redoitems.has(parent) || !parent.redo(y, redoitems)) { + return false + } + } + if (parent.redone !== null) { + while (parent.redone !== null) { + parent = parent.redone + } + // find next cloned_redo items + while (left !== null) { + if (left.redone !== null && left.redone.parent === parent) { + left = left.redone + break + } + left = left.left + } + while (right !== null) { + if (right.redone !== null && right.redone.parent === parent) { + right = right.redone + } + right = right._right + } + } + this.redone = this.copy(createNextID(y), left, right, parent, this.parentSub) + return true + } + + /** + * Computes the last content address of this Item. + * + * @private + */ + get lastId () { + /** + * @type {any} + */ + const id = this.id + return createID(id.user, id.clock + this.length - 1) + } + + /** + * Computes the length of this Item. + */ + get length () { + return 1 + } + + /** + * Should return false if this Item is some kind of meta information + * (e.g. format information). + * + * * Whether this Item should be addressable via `yarray.get(i)` + * * Whether this Item should be counted when computing yarray.length + */ + get countable () { + return true + } + + /** + * Splits this Item so that another Item can be inserted in-between. + * This must be overwritten if _length > 1 + * Returns right part after split + * + * (see {@link ItemJSON}/{@link ItemString} for implementation) + * + * Does not integrate the struct, nor store it in struct store. + * + * This method should only be cally by StructStore. + * + * @param {number} diff + * @return {AbstractItem} + */ + splitAt (diff) { + throw new Error('unimplemented') + } + + /** + * Mark this Item as deleted. + * + * @param {Transaction} transaction + * @param {boolean} createDelete Whether to propagate a message that this + * Type was deleted. + * @param {boolean} [gcChildren] + * + * @private + */ + delete (transaction, createDelete = true, gcChildren) { + if (!this.deleted) { + const y = transaction.y + const parent = this.parent + const len = this.length + // adjust the length of parent + if (this.countable && this.parentSub === null) { + if (parent !== null) { + // parent is y + y.get(this.) + + } else { + transaction.y.get(this.parentSub) + } + } + if (parent.length !== undefined && this.countable) { + parent.length -= len + } + this._deleted = true + y.ds.mark(this.id, this.length, false) + let del = new Delete(this.id, len) + if (createDelete) { + // broadcast and persists Delete + del.integrate(y, true) + } + if (parent !== null) { + maplib.setTfUndefined(transaction.changed, parent, set.create).add(this.parentSub) + } + transaction.deleted.add(this) + } + } + + /** + * @param {Y} y + */ + gcChildren (y) {} + + /** + * @param {Y} y + */ + gc (y) { + if (this.id !== null) { + y.os.replace(this, new GC(this.id, this.length)) + } + } + + getContent () { + throw new Error('Must implement') // TODO: create function in lib0 + } + + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @param {encoding.Encoder} encoder The encoder to write data to. + * @param {number} encodingRef + * @private + */ + write (encoder, encodingRef) { + const info = (encodingRef & binary.BITS5) | + ((this.origin === null) ? 0 : binary.BIT8) | // origin is defined + ((this.rightOrigin === null) ? 0 : binary.BIT7) | // right origin is defined + ((this.parentSub !== null) ? 0 : binary.BIT6) // parentSub is non-null + encoding.writeUint8(encoder, info) + writeID(encoder, this.id) + if (this.origin !== null) { + writeID(encoder, this.origin.lastId) + } + if (this.rightOrigin !== null) { + writeID(encoder, this.rightOrigin.id) + } + if (this.origin === null && this.rightOrigin === null) { + if (this.parent === null) { + writeNullID(encoder) + } else { + // neither origin nor right is defined + writeID(encoder, this.parent.id) + } + if (this.parentSub !== null) { + encoding.writeVarString(encoder, this.parentSub) + } + } + } +} + +export class AbstractItemRef extends AbstractRef { + /** + * @param {decoding.Decoder} decoder + * @param {number} info + */ + constructor (decoder, info) { + super() + const id = readID(decoder) + if (id === null) { + throw new Error('id must not be null') + } + /** + * The uniqe identifier of this type. + * @type {ID} + */ + this.id = id + /** + * The item that was originally to the left of this item. + * @type {ID | null} + */ + this.left = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null + /** + * The item that was originally to the right of this item. + * @type {ID | null} + */ + this.right = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null + const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0 + /** + * The parent type. + * @type {ID | null} + */ + this.parent = canCopyParentInfo ? readID(decoder) : null + /** + * If the parent refers to this item with some kind of key (e.g. YMap, the + * key is specified here. The key is then used to refer to the list in which + * to insert this item. If `parentSub = null` type._start is the list in + * which to insert to. Otherwise it is `parent._map`. + * @type {String | null} + */ + this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null + } + /** + * @return {Array} + */ + getMissing () { + return [ + createID(this.id.client, this.id.clock - 1), + this.left, + this.right, + this.parent + ] + } +} diff --git a/src/structs/AbstractStruct.js b/src/structs/AbstractStruct.js new file mode 100644 index 00000000..11b73964 --- /dev/null +++ b/src/structs/AbstractStruct.js @@ -0,0 +1,38 @@ +import { Y } from '../utils/Y.js' // eslint-disable-line +import { ID } from '../utils/ID.js' // eslint-disable-line +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line + +// eslint-disable-next-line +export class AbstractStruct { + /** + * @param {ID} id + */ + constructor (id) { + /** + * The uniqe identifier of this struct. + * @type {ID} + * @readonly + */ + this.id = id + } + /** + * @type {number} + */ + get length () { + throw new Error('unimplemented') + } +} + +export class AbstractRef { + /** + * @return {Array} + */ + getMissing () { + return [] + } + /** + * @param {Transaction} transaction + * @return {AbstractStruct} + */ + toStruct (transaction) { throw new Error('Must be defined') } +} diff --git a/src/structs/Delete.js b/src/structs/Delete.js deleted file mode 100644 index 5b93d3f2..00000000 --- a/src/structs/Delete.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @module structs - */ - -import { getStructReference } from '../utils/structReferences.js' -import * as ID from '../utils/ID.js' -import { writeStructToTransaction } from '../utils/structEncoding.js' -import * as decoding from 'lib0/decoding.js' -import * as encoding from 'lib0/encoding.js' -import * as Item from './Item.js' -// import { Y } from '../utils/Y.js' // eslint-disable-line -import { deleteItemRange } from '../utils/structManipulation.js' - -/** - * @private - * A Delete change is not a real Item, but it provides the same interface as an - * Item. The only difference is that it will not be saved in the ItemStore - * (OperationStore), but instead it is safed in the DeleteStore. - */ -export class Delete { - constructor () { - /** - * @type {ID.ID} - */ - this._targetID = null - /** - * @type {Item.Item} - */ - this._target = null - this._length = null - } - - /** - * @private - * Read the next Item in a Decoder and fill this Item with the read data. - * - * This is called when data is received from a remote peer. - * - * @param {Y} y The Yjs instance that this Item belongs to. - * @param {decoding.Decoder} decoder The decoder object to read data from. - */ - _fromBinary (y, decoder) { - // TODO: set target, and add it to missing if not found - // There is an edge case in p2p networks! - /** - * @type {any} - */ - const targetID = ID.decode(decoder) - this._targetID = targetID - this._length = decoding.readVarUint(decoder) - if (y.os.getItem(targetID) === null) { - return [targetID] - } else { - return [] - } - } - - /** - * @private - * Transform the properties of this type to binary and write it to an - * BinaryEncoder. - * - * This is called when this Item is sent to a remote peer. - * - * @param {encoding.Encoder} encoder The encoder to write data to. - */ - _toBinary (encoder) { - encoding.writeUint8(encoder, getStructReference(this.constructor)) - this._targetID.encode(encoder) - encoding.writeVarUint(encoder, this._length) - } - - /** - * @private - * Integrates this Item into the shared structure. - * - * This method actually applies the change to the Yjs instance. In the case of - * Delete it marks the delete target as deleted. - * - * * If created remotely (a remote user deleted something), - * this Delete is applied to all structs in id-range. - * * If created lokally (e.g. when y-array deletes a range of elements), - * this struct is broadcasted only (it is already executed) - */ - _integrate (y, locallyCreated = false) { - if (!locallyCreated) { - // from remote - const id = this._targetID - deleteItemRange(y, id.user, id.clock, this._length, false) - } - writeStructToTransaction(y._transaction, this) - } - - /** - * Transform this YXml Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - _logString () { - return `Delete - target: ${Item.stringifyID(this._targetID)}, len: ${this._length}` - } -} diff --git a/src/structs/GC.js b/src/structs/GC.js index 251b8136..f6bce3bd 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -1,106 +1,69 @@ /** * @module structs */ - -import { getStructReference } from '../utils/structReferences.js' -import * as ID from '../utils/ID.js' -import { writeStructToTransaction } from '../utils/structEncoding.js' +import { AbstractRef, AbstractStruct } from './AbstractStruct.js' +import { ID, readID, createID, writeID } from '../utils/ID.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' import * as encoding from 'lib0/encoding.js' -// import { Y } from '../utils/Y.js' // eslint-disable-line + +export const structGCRefNumber = 0 // TODO should have the same base class as Item -export class GC { - constructor () { +export class GC extends AbstractStruct { + /** + * @param {ID} id + * @param {number} length + */ + constructor (id, length) { + super(id) /** - * @type {ID.ID} + * @type {number} */ - this._id = null - this._length = 0 - } - - get _redone () { - return null - } - - get _deleted () { - return true - } - - _integrate (y) { - const id = this._id - const userState = y.ss.getState(id.user) - if (id.clock === userState) { - y.ss.setState(id.user, id.clock + this._length) - } - y.ds.mark(this._id, this._length, true) - let n = y.os.put(this) - const prev = n.prev().val - if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) { - // TODO: do merging for all items! - prev._length += n.val._length - y.os.delete(n.val._id) - n = prev - } - if (n.val) { - n = n.val - } - const next = y.os.findNext(n._id) - if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) { - n._length += next._length - y.os.delete(next._id) - } - if (id.user !== ID.RootFakeUserID) { - writeStructToTransaction(y._transaction, this) - } + this.length = length } /** - * Transform the properties of this type to binary and write it to an - * BinaryEncoder. - * - * This is called when this Item is sent to a remote peer. - * - * @param {encoding.Encoder} encoder The encoder to write data to. - * @private + * @param {encoding.Encoder} encoder */ - _toBinary (encoder) { - encoding.writeUint8(encoder, getStructReference(this.constructor)) - this._id.encode(encoder) - encoding.writeVarUint(encoder, this._length) - } - - /** - * Read the next Item in a Decoder and fill this Item with the read data. - * - * This is called when data is received from a remote peer. - * - * @param {Y} y The Yjs instance that this Item belongs to. - * @param {decoding.Decoder} decoder The decoder object to read data from. - * @private - */ - _fromBinary (y, decoder) { - /** - * @type {any} - */ - const id = ID.decode(decoder) - this._id = id - this._length = decoding.readVarUint(decoder) - const missing = [] - if (y.ss.getState(id.user) < id.clock) { - missing.push(ID.createID(id.user, id.clock - 1)) - } - return missing - } - - _splitAt () { - return this - } - - _clonePartial (diff) { - const gc = new GC() - gc._id = ID.createID(this._id.user, this._id.clock + diff) - gc._length = this._length - diff - return gc + write (encoder) { + encoding.writeUint8(encoder, structGCRefNumber) + writeID(encoder, this.id) + encoding.writeVarUint(encoder, this.length) + } +} + +export class GCRef extends AbstractRef { + /** + * @param {decoding.Decoder} decoder + * @param {number} info + */ + constructor (decoder, info) { + super() + const id = readID(decoder) + if (id === null) { + throw new Error('expected id') + } + /** + * @type {ID} + */ + this.id = id + /** + * @type {number} + */ + this.length = decoding.readVarUint(decoder) + } + missing () { + return [ + createID(this.id.client, this.id.clock - 1) + ] + } + /** + * @return {GC} + */ + toStruct () { + return new GC( + this.id, + this.length + ) } } diff --git a/src/structs/Item.js b/src/structs/Item.js deleted file mode 100644 index 7b3848c8..00000000 --- a/src/structs/Item.js +++ /dev/null @@ -1,634 +0,0 @@ -/** - * @module structs - */ - -import { getStructReference } from '../utils/structReferences.js' -import * as ID from '../utils/ID.js' -import { Delete } from './Delete.js' -import { writeStructToTransaction } from '../utils/structEncoding.js' -import { GC } from './GC.js' -import * as encoding from 'lib0/encoding.js' -import * as decoding from 'lib0/decoding.js' - -/** - * Stringify an item id. - * - * @param {ID.ID | ID.RootID} id - * @return {string} - */ -export const stringifyID = id => id instanceof ID.ID ? `(${id.user},${id.clock})` : `(${id.name},${id.type})` - -/** - * Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent). - * - * @param {Item | Y | null} item - * @return {string} - */ -export const stringifyItemID = item => - item === null ? '()' : (item._id != null ? stringifyID(item._id) : 'y') - -/** - * Helper utility to convert an item to a readable format. - * - * @param {String} name The name of the item class (YText, ItemString, ..). - * @param {Item} item The item instance. - * @param {String} [append] Additional information to append to the returned - * string. - * @return {String} A readable string that represents the item object. - * - */ -export const logItemHelper = (name, item, append) => { - const left = item._left !== null ? stringifyID(item._left._lastId) : '()' - const origin = item._origin !== null ? stringifyID(item._origin._lastId) : '()' - return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item._right)},parent:${stringifyItemID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})` -} - -/** - * @private - */ -export const transactionTypeChanged = (y, type, sub) => { - if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) { - const changedTypes = y._transaction.changedTypes - let subs = changedTypes.get(type) - if (subs === undefined) { - // create if it doesn't exist yet - subs = new Set() - changedTypes.set(type, subs) - } - subs.add(sub) - } -} - -/** - * Helper utility to split an Item (see {@link Item#_splitAt}) - * - copies all properties from a to b - * - connects a to b - * - assigns the correct _id - * - saves b to os - * @private - */ -export const splitHelper = (y, a, b, diff) => { - const aID = a._id - b._id = ID.createID(aID.user, aID.clock + diff) - b._origin = a - b._left = a - b._right = a._right - if (b._right !== null) { - b._right._left = b - } - b._right_origin = a._right_origin - // do not set a._right_origin, as this will lead to problems when syncing - a._right = b - b._parent = a._parent - b._parentSub = a._parentSub - b._deleted = a._deleted - // now search all relevant items to the right and update origin - // if origin is not it foundOrigins, we don't have to search any longer - let foundOrigins = new Set() - foundOrigins.add(a) - let o = b._right - while (o !== null && foundOrigins.has(o._origin)) { - if (o._origin === a) { - o._origin = b - } - foundOrigins.add(o) - o = o._right - } - y.os.put(b) - if (y._transaction !== null) { - if (y._transaction.newTypes.has(a)) { - y._transaction.newTypes.add(b) - } else if (y._transaction.deletedStructs.has(a)) { - y._transaction.deletedStructs.add(b) - } - } -} - -/** - * Abstract class that represents any content. - */ -export class Item { - constructor () { - /** - * The uniqe identifier of this type. - * @type {ID.ID | ID.RootID | null} - */ - this._id = null - /** - * The item that was originally to the left of this item. - * @type {Item | null} - */ - this._origin = null - /** - * The item that is currently to the left of this item. - * @type {Item | null} - */ - this._left = null - /** - * The item that is currently to the right of this item. - * @type {Item | null} - */ - this._right = null - /** - * The item that was originally to the right of this item. - * @type {Item | null} - */ - this._right_origin = null - /** - * The parent type. - * @type {Y | Type | null} - */ - this._parent = null - /** - * If the parent refers to this item with some kind of key (e.g. YMap, the - * key is specified here. The key is then used to refer to the list in which - * to insert this item. If `parentSub = null` type._start is the list in - * which to insert to. Otherwise it is `parent._map`. - * @type {String | null} - */ - this._parentSub = null - /** - * Whether this item was deleted or not. - * @type {Boolean} - */ - this._deleted = false - /** - * If this type's effect is reundone this type refers to the type that undid - * this operation. - * @type {Type | null} - */ - this._redone = null - } - - /** - * Returns the next non-deleted item - * @private - */ - get _next () { - let n = this._right - while (n !== null && n._deleted) { - n = n._right - } - return n - } - - /** - * Returns the previous non-deleted item - * @private - */ - get _prev () { - let n = this._left - while (n !== null && n._deleted) { - n = n._left - } - return n - } - - /** - * Creates an Item with the same effect as this Item (without position effect) - * - * @private - */ - _copy () { - const C = this.constructor - // @ts-ignore - return new C() - } - - /** - * Redoes the effect of this operation. - * - * @param {Y} y The Yjs instance. - * @param {Set} redoitems - * - * @private - */ - _redo (y, redoitems) { - if (this._redone !== null) { - return this._redone - } - if (!(this._parent instanceof Item)) { - return - } - let struct = this._copy() - let left, right - if (this._parentSub === null) { - // Is an array item. Insert at the old position - left = this._left - right = this - } else { - // Is a map item. Insert at the start - left = null - right = this._parent._map.get(this._parentSub) - right._delete(y) - } - let parent = this._parent - // make sure that parent is redone - if (parent._deleted === true && parent._redone === null) { - // try to undo parent if it will be undone anyway - if (!redoitems.has(parent) || !parent._redo(y, redoitems)) { - return false - } - } - if (parent._redone !== null) { - parent = parent._redone - // find next cloned_redo items - while (left !== null) { - if (left._redone !== null && left._redone._parent === parent) { - left = left._redone - break - } - left = left._left - } - while (right !== null) { - if (right._redone !== null && right._redone._parent === parent) { - right = right._redone - } - right = right._right - } - } - struct._origin = left - struct._left = left - struct._right = right - struct._right_origin = right - struct._parent = parent - struct._parentSub = this._parentSub - struct._integrate(y) - this._redone = struct - return true - } - - /** - * Computes the last content address of this Item. - * - * @private - */ - get _lastId () { - /** - * @type {any} - */ - const id = this._id - return ID.createID(id.user, id.clock + this._length - 1) - } - - /** - * Computes the length of this Item. - * - * @private - */ - get _length () { - return 1 - } - - /** - * Should return false if this Item is some kind of meta information - * (e.g. format information). - * - * * Whether this Item should be addressable via `yarray.get(i)` - * * Whether this Item should be counted when computing yarray.length - * - * @private - */ - get _countable () { - return true - } - - /** - * Splits this Item so that another Items can be inserted in-between. - * This must be overwritten if _length > 1 - * Returns right part after split - * * diff === 0 => this - * * diff === length => this._right - * * otherwise => split _content and return right part of split - * (see {@link ItemJSON}/{@link ItemString} for implementation) - * - * @private - */ - _splitAt (y, diff) { - if (diff === 0) { - return this - } - return this._right - } - - /** - * Mark this Item as deleted. - * - * @param {Y} y The Yjs instance - * @param {boolean} createDelete Whether to propagate a message that this - * Type was deleted. - * @param {boolean} [gcChildren] - * - * @private - */ - _delete (y, createDelete = true, gcChildren) { - if (!this._deleted) { - /** - * @type { Type } - */ - const parent = this._parent - const len = this._length - // adjust the length of parent - if (parent.length !== undefined && this._countable) { - parent.length -= len - } - this._deleted = true - y.ds.mark(this._id, this._length, false) - let del = new Delete() - del._targetID = this._id - del._length = len - if (createDelete) { - // broadcast and persists Delete - del._integrate(y, true) - } - transactionTypeChanged(y, parent, this._parentSub) - y._transaction.deletedStructs.add(this) - } - } - - _gcChildren (y) {} - - _gc (y) { - const gc = new GC() - gc._id = this._id - gc._length = this._length - y.os.delete(this._id) - gc._integrate(y) - } - - /** - * This is called right before this Item receives any children. - * It can be overwritten to apply pending changes before applying remote changes - * - * @private - */ - _beforeChange () { - // nop - } - - /** - * Integrates this Item into the shared structure. - * - * This method actually applies the change to the Yjs instance. In case of - * Item it connects _left and _right to this Item and calls the - * {@link Item#beforeChange} method. - * - * * Integrate the struct so that other types/structs can see it - * * Add this struct to y.os - * * Check if this is struct deleted - * - * @param {Y} y - * - * @private - */ - _integrate (y) { - y._transaction.newTypes.add(this) - /** - * @type {any} - */ - const parent = this._parent - /** - * @type {any} - */ - const selfID = this._id - const user = selfID === null ? y.userID : selfID.user - const userState = y.ss.getState(user) - if (selfID === null) { - this._id = y.ss.getNextID(this._length) - } else if (selfID.user === ID.RootFakeUserID) { - // is parent - return - } else if (selfID.clock < userState) { - // already applied.. - return - } else if (selfID.clock === userState) { - y.ss.setState(selfID.user, userState + this._length) - } else { - // missing content from user - throw new Error('Can not apply yet!') - } - if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) { - // this is the first time parent is updated - // or this types is new - parent._beforeChange() - } - - /* - # $this has to find a unique position between origin and the next known character - # case 1: $origin equals $o.origin: the $creator parameter decides if left or right - # let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4 - # o2,o3 and o4 origin is 1 (the position of o2) - # there is the case that $this.creator < o2.creator, but o3.creator < $this.creator - # then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex - # therefore $this would be always to the right of o3 - # case 2: $origin < $o.origin - # if current $this insert_position > $o origin: $this ins - # else $insert_position will not change - # (maybe we encounter case 1 later, then this will be to the right of $o) - # case 3: $origin > $o.origin - # $this insert_position is to the left of $o (forever!) - */ - // handle conflicts - let o - // set o to the first conflicting item - if (this._left !== null) { - o = this._left._right - } else if (this._parentSub !== null) { - o = parent._map.get(this._parentSub) || null - } else { - o = parent._start - } - let conflictingItems = new Set() - let itemsBeforeOrigin = new Set() - // Let c in conflictingItems, b in itemsBeforeOrigin - // ***{origin}bbbb{this}{c,b}{c,b}{o}*** - // Note that conflictingItems is a subset of itemsBeforeOrigin - while (o !== null && o !== this._right) { - itemsBeforeOrigin.add(o) - conflictingItems.add(o) - if (this._origin === o._origin) { - // case 1 - if (o._id.user < this._id.user) { - this._left = o - conflictingItems.clear() - } - } else if (itemsBeforeOrigin.has(o._origin)) { - // case 2 - if (!conflictingItems.has(o._origin)) { - this._left = o - conflictingItems.clear() - } - } else { - break - } - // TODO: try to use right_origin instead. - // Then you could basically omit conflictingItems! - // Note: you probably can't use right_origin in every case.. only when setting _left - o = o._right - } - // reconnect left/right + update parent map/start if necessary - const parentSub = this._parentSub - if (this._left === null) { - let right - if (parentSub !== null) { - const pmap = parent._map - right = pmap.get(parentSub) || null - pmap.set(parentSub, this) - } else { - right = parent._start - parent._start = this - } - this._right = right - if (right !== null) { - right._left = this - } - } else { - const left = this._left - const right = left._right - this._right = right - left._right = this - if (right !== null) { - right._left = this - } - } - // adjust the length of parent - if (parentSub === null && parent.length !== undefined && this._countable) { - parent.length += this._length - } - if (parent._deleted) { - this._delete(y, false, true) - } - y.os.put(this) - transactionTypeChanged(y, parent, parentSub) - if (this._id.user !== ID.RootFakeUserID) { - writeStructToTransaction(y._transaction, this) - } - } - - /** - * Transform the properties of this type to binary and write it to an - * BinaryEncoder. - * - * This is called when this Item is sent to a remote peer. - * - * @param {encoding.Encoder} encoder The encoder to write data to. - * - * @private - */ - _toBinary (encoder) { - encoding.writeUint8(encoder, getStructReference(this.constructor)) - let info = 0 - if (this._origin !== null) { - info += 0b1 // origin is defined - } - // TODO: remove - /* no longer send _left - if (this._left !== this._origin) { - info += 0b10 // do not copy origin to left - } - */ - if (this._right_origin !== null) { - info += 0b100 - } - if (this._parentSub !== null) { - info += 0b1000 - } - encoding.writeUint8(encoder, info) - this._id.encode(encoder) - if (info & 0b1) { - this._origin._lastId.encode(encoder) - } - // TODO: remove - /* see above - if (info & 0b10) { - encoder.writeID(this._left._lastId) - } - */ - if (info & 0b100) { - this._right_origin._id.encode(encoder) - } - if ((info & 0b101) === 0) { - // neither origin nor right is defined - this._parent._id.encode(encoder) - } - if (info & 0b1000) { - encoding.writeVarString(encoder, JSON.stringify(this._parentSub)) - } - } - - /** - * Read the next Item in a Decoder and fill this Item with the read data. - * - * This is called when data is received from a remote peer. - * - * @param {Y} y The Yjs instance that this Item belongs to. - * @param {decoding.Decoder} decoder The decoder object to read data from. - * - * @private - */ - _fromBinary (y, decoder) { - let missing = [] - const info = decoding.readUint8(decoder) - const id = ID.decode(decoder) - this._id = id - // read origin - if (info & 0b1) { - // origin != null - const originID = ID.decode(decoder) - // we have to query for left again because it might have been split/merged.. - const origin = y.os.getItemCleanEnd(originID) - if (origin === null) { - missing.push(originID) - } else { - this._origin = origin - this._left = this._origin - } - } - // read right - if (info & 0b100) { - // right != null - const rightID = ID.decode(decoder) - // we have to query for right again because it might have been split/merged.. - const right = y.os.getItemCleanStart(rightID) - if (right === null) { - missing.push(rightID) - } else { - this._right = right - this._right_origin = right - } - } - // read parent - if ((info & 0b101) === 0) { - // neither origin nor right is defined - const parentID = ID.decode(decoder) - // parent does not change, so we don't have to search for it again - if (this._parent === null) { - let parent - if (parentID.constructor === ID.RootID) { - parent = y.os.get(parentID) - } else { - parent = y.os.getItem(parentID) - } - if (parent === null) { - missing.push(parentID) - } else { - this._parent = parent - } - } - } else if (this._parent === null) { - if (this._origin !== null) { - this._parent = this._origin._parent - } else if (this._right_origin !== null) { - this._parent = this._right_origin._parent - } - } - if (info & 0b1000) { - // TODO: maybe put this in read parent condition (you can also read parentsub from left/right) - this._parentSub = JSON.parse(decoding.readVarString(decoder)) - } - if (id instanceof ID.ID && y.ss.getState(id.user) < id.clock) { - missing.push(ID.createID(id.user, id.clock - 1)) - } - return missing - } -} diff --git a/src/structs/ItemBinary.js b/src/structs/ItemBinary.js index 1f8e6ae0..25c1dad7 100644 --- a/src/structs/ItemBinary.js +++ b/src/structs/ItemBinary.js @@ -4,44 +4,83 @@ // TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency -import { Item, logItemHelper } from './Item.js' +import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' +import { ID } from '../utils/ID.js' // eslint-disable-line +import { ItemType } from './ItemType.js' // eslint-disable-line import { Y } from '../utils/Y.js' // eslint-disable-line +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line +import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' -export class ItemBinary extends Item { - constructor () { - super() - this._content = null - } - _copy () { - let struct = super._copy() - struct._content = this._content - return struct - } +export const structBinaryRefNumber = 1 + +export class ItemBinary extends AbstractItem { /** - * @param {Y} y - * @param {decoding.Decoder} decoder + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + * @param {ArrayBuffer} content */ - _fromBinary (y, decoder) { - const missing = super._fromBinary(y, decoder) - this._content = decoding.readPayload(decoder) - return missing + constructor (id, left, right, parent, parentSub, content) { + super(id, left, right, parent, parentSub) + this.content = content } /** - * @param {encoding.Encoder} encoder + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub */ - _toBinary (encoder) { - super._toBinary(encoder) - encoding.writePayload(encoder, this._content) + copy (id, left, right, parent, parentSub) { + return new ItemBinary(id, left, right, parent, parentSub, this.content) } /** - * Transform this YXml Type to a readable format. + * Transform this Type to a readable format. * Useful for logging as all Items and Delete implement this method. * * @private */ - _logString () { + logString () { return logItemHelper('ItemBinary', this) } + /** + * @param {encoding.Encoder} encoder + */ + write (encoder) { + super.write(encoder, structBinaryRefNumber) + encoding.writePayload(encoder, this.content) + } +} + +export class ItemBinaryRef extends AbstractItemRef { + /** + * @param {decoding.Decoder} decoder + * @param {number} info + */ + constructor (decoder, info) { + super(decoder, info) + /** + * @type {ArrayBuffer} + */ + this.content = decoding.readPayload(decoder) + } + /** + * @param {Transaction} transaction + * @return {ItemBinary} + */ + toStruct (transaction) { + const store = transaction.y.store + return new ItemBinary( + this.id, + this.left === null ? null : getItemCleanEnd(store, transaction, this.left), + this.right === null ? null : getItemCleanStart(store, transaction, this.right), + this.parent === null ? null : getItemType(store, this.parent), + this.parentSub, + this.content + ) + } } diff --git a/src/structs/ItemDeleted.js b/src/structs/ItemDeleted.js new file mode 100644 index 00000000..fb6d34ec --- /dev/null +++ b/src/structs/ItemDeleted.js @@ -0,0 +1,83 @@ +/** + * @module structs + */ + +// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency + +import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js' +import * as encoding from 'lib0/encoding.js' +import * as decoding from 'lib0/decoding.js' +import { ID } from '../utils/ID.js' // eslint-disable-line +import { ItemType } from './ItemType.js' // eslint-disable-line +import { Y } from '../utils/Y.js' // eslint-disable-line + +export const structDeletedRefNumber = 2 + +export class ItemBinary extends AbstractItem { + /** + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + * @param {ArrayBuffer} content + */ + constructor (id, left, right, parent, parentSub, content) { + super(id, left, right, parent, parentSub) + this.content = content + } + /** + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + */ + copy (id, left, right, parent, parentSub) { + return new ItemBinary(id, left, right, parent, parentSub, this.content) + } + /** + * Transform this Type to a readable format. + * Useful for logging as all Items and Delete implement this method. + * + * @private + */ + logString () { + return logItemHelper('ItemBinary', this) + } + /** + * @param {encoding.Encoder} encoder + */ + write (encoder) { + super.write(encoder, structDeletedRefNumber) + encoding.writePayload(encoder, this.content) + } +} + +export class ItemDeletedRef extends AbstractItemRef { + /** + * @param {decoding.Decoder} decoder + * @param {number} info + */ + constructor (decoder, info) { + super(decoder, info) + /** + * @type {ArrayBuffer} + */ + this.content = decoding.readPayload(decoder) + } + /** + * @param {Y} y + * @return {ItemBinary} + */ + toStruct (y) { + return new ItemBinary( + this.id, + this.left === null ? null : y.os.getItemCleanEnd(this.left), + this.right === null ? null : y.os.getItemCleanStart(this.right), + this.parent === null ? null : y.os.getItem(this.parent), + this.parentSub, + this.content + ) + } +} diff --git a/src/structs/ItemEmbed.js b/src/structs/ItemEmbed.js index 252ab1ee..46ff63d3 100644 --- a/src/structs/ItemEmbed.js +++ b/src/structs/ItemEmbed.js @@ -2,47 +2,90 @@ * @module structs */ -import { Item, logItemHelper } from './Item.js' +import { AbstractItem, AbstractItemRef, logItemHelper } from './AbstractItem.js' +import { ItemType } from './ItemType.js' // eslint-disable-line import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { Y } from '../utils/Y.js' // eslint-disable-line +import { ID } from '../utils/ID.js' // eslint-disable-line +import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line -export class ItemEmbed extends Item { - constructor () { - super() - this.embed = null - } - _copy (undeleteChildren, copyPosition) { - let struct = super._copy() - struct.embed = this.embed - return struct - } - get _length () { - return 1 - } +export const structEmbedRefNumber = 3 + +export class ItemEmbed extends AbstractItem { /** - * @param {Y} y - * @param {decoding.Decoder} decoder + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + * @param {Object} embed */ - _fromBinary (y, decoder) { - const missing = super._fromBinary(y, decoder) - this.embed = JSON.parse(decoding.readVarString(decoder)) - return missing + constructor (id, left, right, parent, parentSub, embed) { + super(id, left, right, parent, parentSub) + this.embed = embed } /** - * @param {encoding.Encoder} encoder + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub */ - _toBinary (encoder) { - super._toBinary(encoder) - encoding.writeVarString(encoder, JSON.stringify(this.embed)) + copy (id, left, right, parent, parentSub) { + return new ItemEmbed(id, left, right, parent, parentSub, this.embed) } /** - * Transform this YXml Type to a readable format. + * Transform this Type to a readable format. * Useful for logging as all Items and Delete implement this method. * * @private */ - _logString () { - return logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`) + logString () { + return logItemHelper('ItemEmbed', this) + } + /** + * @type {number} + */ + get _length () { + return 1 + } + + /** + * @param {encoding.Encoder} encoder + */ + write (encoder) { + super.write(encoder, structEmbedRefNumber) + encoding.writeVarString(encoder, JSON.stringify(this.embed)) + } +} + +export class ItemEmbedRef extends AbstractItemRef { + /** + * @param {decoding.Decoder} decoder + * @param {number} info + */ + constructor (decoder, info) { + super(decoder, info) + /** + * @type {ArrayBuffer} + */ + this.embed = JSON.parse(decoding.readVarString(decoder)) + } + /** + * @param {Transaction} transaction + * @return {ItemEmbed} + */ + toStruct (transaction) { + const store = transaction.y.store + return new ItemEmbed( + this.id, + this.left === null ? null : getItemCleanEnd(store, transaction, this.left), + this.right === null ? null : getItemCleanStart(store, transaction, this.right), + this.parent === null ? null : getItemType(store, this.parent), + this.parentSub, + this.embed + ) } } diff --git a/src/structs/ItemFormat.js b/src/structs/ItemFormat.js index 8c0e8056..c7dbe753 100644 --- a/src/structs/ItemFormat.js +++ b/src/structs/ItemFormat.js @@ -2,22 +2,50 @@ * @module structs */ -import { Item, logItemHelper } from './Item.js' +import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { Y } from '../utils/Y.js' // eslint-disable-line +import { ID } from '../utils/ID.js' // eslint-disable-line +import { ItemType } from './ItemType.js' // eslint-disable-line +import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line -export class ItemFormat extends Item { - constructor () { - super() - this.key = null - this.value = null +export const structFormatRefNumber = 4 + +export class ItemFormat extends AbstractItem { + /** + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + * @param {string} key + * @param {any} value + */ + constructor (id, left, right, parent, parentSub, key, value) { + super(id, left, right, parent, parentSub) + this.key = key + this.value = value } - _copy (undeleteChildren, copyPosition) { - let struct = super._copy() - struct.key = this.key - struct.value = this.value - return struct + /** + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + */ + copy (id, left, right, parent, parentSub) { + return new ItemFormat(id, left, right, parent, parentSub, this.key, this.value) + } + /** + * Transform this Type to a readable format. + * Useful for logging as all Items and Delete implement this method. + * + * @private + */ + logString () { + return logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`) } get _length () { return 1 @@ -25,31 +53,43 @@ export class ItemFormat extends Item { get _countable () { return false } - /** - * @param {Y} y - * @param {decoding.Decoder} decoder - */ - _fromBinary (y, decoder) { - const missing = super._fromBinary(y, decoder) - this.key = decoding.readVarString(decoder) - this.value = JSON.parse(decoding.readVarString(decoder)) - return missing - } /** * @param {encoding.Encoder} encoder */ - _toBinary (encoder) { - super._toBinary(encoder) + write (encoder) { + super.write(encoder, structFormatRefNumber) encoding.writeVarString(encoder, this.key) encoding.writeVarString(encoder, JSON.stringify(this.value)) } +} + +export class ItemFormatRef extends AbstractItemRef { /** - * Transform this YXml Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private + * @param {decoding.Decoder} decoder + * @param {number} info */ - _logString () { - return stringify.logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`) + constructor (decoder, info) { + super(decoder, info) + /** + * @type {string} + */ + this.key = decoding.readVarString(decoder) + this.value = JSON.parse(decoding.readVarString(decoder)) + } + /** + * @param {Transaction} transaction + * @return {ItemFormat} + */ + toStruct (transaction) { + const store = transaction.y.store + return new ItemFormat( + this.id, + this.left === null ? null : getItemCleanEnd(store, transaction, this.left), + this.right === null ? null : getItemCleanStart(store, transaction, this.right), + this.parent === null ? null : getItemType(store, this.parent), + this.parentSub, + this.key, + this.value + ) } } diff --git a/src/structs/ItemJSON.js b/src/structs/ItemJSON.js index e4933334..179af5a4 100644 --- a/src/structs/ItemJSON.js +++ b/src/structs/ItemJSON.js @@ -2,81 +2,109 @@ * @module structs */ -import { Item, splitHelper, logItemHelper } from './Item.js' +import { AbstractItem, logItemHelper, AbstractItemRef, splitItem } from './AbstractItem.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { Y } from '../utils/Y.js' // eslint-disable-line +import { ID } from '../utils/ID.js' // eslint-disable-line +import { ItemType } from './ItemType.js' // eslint-disable-line +import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line -export class ItemJSON extends Item { - constructor () { - super() - this._content = null - } - _copy () { - let struct = super._copy() - struct._content = this._content - return struct - } - get _length () { - const c = this._content - return c !== null ? c.length : 0 - } +export const structJSONRefNumber = 5 + +export class ItemJSON extends AbstractItem { /** - * @param {Y} y - * @param {decoding.Decoder} decoder + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + * @param {Array} content */ - _fromBinary (y, decoder) { - let missing = super._fromBinary(y, decoder) - let len = decoding.readVarUint(decoder) - this._content = new Array(len) - for (let i = 0; i < len; i++) { - const ctnt = decoding.readVarString(decoder) - let parsed - if (ctnt === 'undefined') { - parsed = undefined - } else { - parsed = JSON.parse(ctnt) - } - this._content[i] = parsed - } - return missing + constructor (id, left, right, parent, parentSub, content) { + super(id, left, right, parent, parentSub) + this.content = content } /** - * @param {encoding.Encoder} encoder + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub */ - _toBinary (encoder) { - super._toBinary(encoder) - const len = this._length - encoding.writeVarUint(encoder, len) - for (let i = 0; i < len; i++) { - let encoded - const content = this._content[i] - if (content === undefined) { - encoded = 'undefined' - } else { - encoded = JSON.stringify(content) - } - encoding.writeVarString(encoder, encoded) - } + copy (id, left, right, parent, parentSub) { + return new ItemJSON(id, left, right, parent, parentSub, this.content) } /** - * Transform this YXml Type to a readable format. + * Transform this Type to a readable format. * Useful for logging as all Items and Delete implement this method. * * @private */ - _logString () { - return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`) + logString () { + return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this.content)}`) } - _splitAt (y, diff) { - if (diff === 0) { - return this - } else if (diff >= this._length) { - return this._right + get length () { + return this.content.length + } + /** + * @param {number} diff + */ + splitAt (diff) { + /** + * @type {ItemJSON} + */ + const right = splitItem(this, diff) + right.content = this.content.splice(diff) + return right + } + /** + * @param {encoding.Encoder} encoder + */ + write (encoder) { + super.write(encoder, structJSONRefNumber) + const len = this.content.length + encoding.writeVarUint(encoder, len) + for (let i = 0; i < len; i++) { + const c = this.content[i] + encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c)) } - let item = new ItemJSON() - item._content = this._content.splice(diff) - splitHelper(y, this, item, diff) - return item + } +} + +export class ItemJSONRef extends AbstractItemRef { + /** + * @param {decoding.Decoder} decoder + * @param {number} info + */ + constructor (decoder, info) { + super(decoder, info) + const len = decoding.readVarUint(decoder) + const cs = [] + for (let i = 0; i < len; i++) { + const c = decoding.readVarString(decoder) + if (c === 'undefined') { + cs.push(undefined) + } else { + cs.push(JSON.parse(c)) + } + } + this.content = cs + } + /** + * @param {Transaction} transaction + * @return {ItemJSON} + */ + toStruct (transaction) { + const store = transaction.y.store + return new ItemJSON( + this.id, + this.left === null ? null : getItemCleanEnd(store, transaction, this.left), + this.right === null ? null : getItemCleanStart(store, transaction, this.right), + this.parent === null ? null : getItemType(store, this.parent), + this.parentSub, + this.content + ) } } diff --git a/src/structs/ItemString.js b/src/structs/ItemString.js index d4baa752..b1b2b9d4 100644 --- a/src/structs/ItemString.js +++ b/src/structs/ItemString.js @@ -2,59 +2,100 @@ * @module structs */ -import { Item, splitHelper, logItemHelper } from './Item.js' +import { AbstractItem, logItemHelper, AbstractItemRef, splitItem } from './AbstractItem.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { Y } from '../utils/Y.js' // eslint-disable-line +import { ID } from '../utils/ID.js' // eslint-disable-line +import { ItemType } from './ItemType.js' // eslint-disable-line +import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line -export class ItemString extends Item { - constructor () { - super() - this._content = null - } - _copy () { - let struct = super._copy() - struct._content = this._content - return struct - } - get _length () { - return this._content.length - } +export const structStringRefNumber = 6 + +export class ItemString extends AbstractItem { /** - * @param {Y} y - * @param {decoding.Decoder} decoder + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + * @param {string} string */ - _fromBinary (y, decoder) { - let missing = super._fromBinary(y, decoder) - this._content = decoding.readVarString(decoder) - return missing + constructor (id, left, right, parent, parentSub, string) { + super(id, left, right, parent, parentSub) + this.string = string } /** - * @param {encoding.Encoder} encoder + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub */ - _toBinary (encoder) { - super._toBinary(encoder) - encoding.writeVarString(encoder, this._content) + copy (id, left, right, parent, parentSub) { + return new ItemString(id, left, right, parent, parentSub, this.string) } /** - * Transform this YXml Type to a readable format. + * Transform this Type to a readable format. * Useful for logging as all Items and Delete implement this method. * * @private */ - _logString () { - return logItemHelper('ItemString', this, `content:"${this._content}"`) + logString () { + return logItemHelper('ItemString', this, `content:"${this.string}"`) } - _splitAt (y, diff) { + get length () { + return this.string.length + } + splitAt (y, diff) { if (diff === 0) { return this - } else if (diff >= this._length) { - return this._right + } else if (diff >= this.string.length) { + return this.right } - let item = new ItemString() - item._content = this._content.slice(diff) - this._content = this._content.slice(0, diff) - splitHelper(y, this, item, diff) - return item + /** + * @type {ItemString} + */ + const right = splitItem(this, y, diff) + right.string = this.string.slice(diff) + right.string = this.string.slice(0, diff) + return right + } + /** + * @param {encoding.Encoder} encoder + */ + write (encoder) { + super.write(encoder, structStringRefNumber) + encoding.writeVarString(encoder, this.string) + } +} + +export class ItemStringRef extends AbstractItemRef { + /** + * @param {decoding.Decoder} decoder + * @param {number} info + */ + constructor (decoder, info) { + super(decoder, info) + /** + * @type {string} + */ + this.string = decoding.readVarString(decoder) + } + /** + * @param {Transaction} transaction + * @return {ItemString} + */ + toStruct (transaction) { + const store = transaction.y.store + return new ItemString( + this.id, + this.left === null ? null : getItemCleanEnd(store, transaction, this.left), + this.right === null ? null : getItemCleanStart(store, transaction, this.right), + this.parent === null ? null : getItemType(store, this.parent), + this.parentSub, + this.string + ) } } diff --git a/src/structs/ItemType.js b/src/structs/ItemType.js new file mode 100644 index 00000000..1620d159 --- /dev/null +++ b/src/structs/ItemType.js @@ -0,0 +1,169 @@ +/** + * @module structs + */ + +// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency + +import { ID } from '../utils/ID.js' // eslint-disable-line +import { Y } from '../utils/Y.js' // eslint-disable-line +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js' +import * as encoding from 'lib0/encoding.js' // eslint-disable-line +import * as decoding from 'lib0/decoding.js' +import { readYArray } from '../types/YArray.js' +import { readYMap } from '../types/YMap.js' +import { readYText } from '../types/YText.js' +import { readYXmlElement, readYXmlFragment } from '../types/YXmlElement.js' +import { readYXmlHook } from '../types/YXmlHook.js' +import { readYXmlText } from '../types/YXmlText.js' +import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line + + +const gcChildren = (y, item) => { + while (item !== null) { + item._delete(y, false, true) + item._gc(y) + item = item._right + } +} + +export const structTypeRefNumber = 7 + +/** + * @type {Array} + */ +export const typeRefs = [ + readYArray, + readYMap, + readYText, + readYXmlElement, + readYXmlFragment, + readYXmlHook, + readYXmlText +] + +export class ItemType extends AbstractItem { + /** + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + * @param {AbstractType} type + */ + constructor (id, left, right, parent, parentSub, type) { + super(id, left, right, parent, parentSub) + this.type = type + } + /** + * @param {ID} id + * @param {AbstractItem | null} left + * @param {AbstractItem | null} right + * @param {ItemType | null} parent + * @param {string | null} parentSub + * @return {AbstractItem} TODO, returns itemtype + */ + copy (id, left, right, parent, parentSub) { + return new ItemType(id, left, right, parent, parentSub, this.type._copy()) + } + /** + * Transform this Type to a readable format. + * Useful for logging as all Items and Delete implement this method. + * + * @private + */ + logString () { + return logItemHelper('ItemType', this) + } + /** + * @param {encoding.Encoder} encoder + */ + write (encoder) { + super.write(encoder, structTypeRefNumber) + this.type._write(encoder) + } + /** + * Mark this Item as deleted. + * + * @param {Transaction} transaction The Yjs instance + * @param {boolean} createDelete Whether to propagate a message that this + * Type was deleted. + * @param {boolean} [gcChildren=(y._hasUndoManager===false)] Whether to garbage + * collect the children of this type. + * @private + */ + delete (transaction, createDelete, gcChildren = transaction.y.gcEnabled) { + const y = transaction.y + super.delete(transaction, createDelete, gcChildren) + transaction.changed.delete(this) + // delete map types + for (let value of this.type._map.values()) { + if (!value._deleted) { + value._delete(y, false, gcChildren) + } + } + // delete array types + let t = this.type._start + while (t !== null) { + if (!t._deleted) { + t._delete(y, false, gcChildren) + } + t = t._right + } + if (gcChildren) { + this.gcChildren(y) + } + } + + /** + * @param {Y} y + */ + gcChildren (y) { + gcChildren(y, this.type._start) + this.type._start = null + this.type._map.forEach(item => { + gcChildren(y, item) + }) + this._map = new Map() + } + + /** + * @param {Y} y + */ + gc (y) { + this.gcChildren(y) + super.gc(y) + } +} + +export class ItemBinaryRef extends AbstractItemRef { + /** + * @param {decoding.Decoder} decoder + * @param {number} info + */ + constructor (decoder, info) { + super(decoder, info) + const typeRef = decoding.readVarUint(decoder) + /** + * @type {AbstractType} + */ + this.type = typeRefs[typeRef](decoder) + } + /** + * @param {Transaction} transaction + * @return {ItemType} + */ + toStruct (transaction) { + const store = transaction.y.store + return new ItemType( + this.id, + this.left === null ? null : getItemCleanEnd(store, transaction, this.left), + this.right === null ? null : getItemCleanStart(store, transaction, this.right), + this.parent === null ? null : getItemType(store, this.parent), + this.parentSub, + this.type + ) + } +} diff --git a/src/structs/Type.js b/src/structs/Type.js deleted file mode 100644 index 295d4803..00000000 --- a/src/structs/Type.js +++ /dev/null @@ -1,277 +0,0 @@ -/** - * @module structs - */ - -import { Item } from './Item.js' -import { EventHandler } from '../utils/EventHandler.js' -import { createID } from '../utils/ID.js' -import { YEvent } from '../utils/YEvent.js' -import { Y } from '../utils/Y.js' // eslint-disable-line - -// restructure children as if they were inserted one after another -const integrateChildren = (y, start) => { - let right - do { - right = start._right - start._right = null - start._right_origin = null - start._origin = start._left - start._integrate(y) - start = right - } while (right !== null) -} - -export const getListItemIDByPosition = (type, i) => { - let pos = 0 - let n = type._start - while (n !== null) { - if (!n._deleted) { - if (pos <= i && i < pos + n._length) { - const id = n._id - return createID(id.user, id.clock + i - pos) - } - pos++ - } - n = n._right - } -} - -const gcChildren = (y, item) => { - while (item !== null) { - item._delete(y, false, true) - item._gc(y) - item = item._right - } -} - -/** - * Abstract Yjs Type class - */ -export class Type extends Item { - constructor () { - super() - this._map = new Map() - this._start = null - this._y = null - this._eventHandler = new EventHandler() - this._deepEventHandler = new EventHandler() - } - - /** - * The first non-deleted item - */ - get _first () { - let n = this._start - while (n !== null && n._deleted) { - n = n._right - } - return n - } - - /** - * Compute the path from this type to the specified target. - * - * @example - * It should be accessible via `this.get(result[0]).get(result[1])..` - * const path = type.getPathTo(child) - * // assuming `type instanceof YArray` - * console.log(path) // might look like => [2, 'key1'] - * child === type.get(path[0]).get(path[1]) - * - * @param {Type | Y | any} type Type target - * @return {Array} Path to the target - */ - getPathTo (type) { - if (type === this) { - return [] - } - const path = [] - const y = this._y - while (type !== this && type !== y) { - let parent = type._parent - if (type._parentSub !== null) { - path.unshift(type._parentSub) - } else { - // parent is array-ish - for (let [i, child] of parent) { - if (child === type) { - path.unshift(i) - break - } - } - } - type = parent - } - if (type !== this) { - throw new Error('The type is not a child of this node') - } - return path - } - - /** - * Creates YArray Event and calls observers. - * @private - */ - _callObserver (transaction, parentSubs, remote) { - this._callEventHandler(transaction, new YEvent(this)) - } - - /** - * Call event listeners with an event. This will also add an event to all - * parents (for `.observeDeep` handlers). - * @private - */ - _callEventHandler (transaction, event) { - const changedParentTypes = transaction.changedParentTypes - this._eventHandler.callEventListeners(transaction, event) - /** - * @type {any} - */ - let type = this - while (type !== this._y) { - let events = changedParentTypes.get(type) - if (events === undefined) { - events = [] - changedParentTypes.set(type, events) - } - events.push(event) - type = type._parent - } - } - - /** - * Helper method to transact if the y instance is available. - * - * TODO: Currently event handlers are not thrown when a type is not registered - * with a Yjs instance. - * @private - */ - _transact (f) { - const y = this._y - if (y !== null) { - y.transact(f) - } else { - f(y) - } - } - - /** - * Observe all events that are created on this type. - * - * @param {Function} f Observer function - */ - observe (f) { - this._eventHandler.addEventListener(f) - } - - /** - * Observe all events that are created by this type and its children. - * - * @param {Function} f Observer function - */ - observeDeep (f) { - this._deepEventHandler.addEventListener(f) - } - - /** - * Unregister an observer function. - * - * @param {Function} f Observer function - */ - unobserve (f) { - this._eventHandler.removeEventListener(f) - } - - /** - * Unregister an observer function. - * - * @param {Function} f Observer function - */ - unobserveDeep (f) { - this._deepEventHandler.removeEventListener(f) - } - - /** - * Integrate this type into the Yjs instance. - * - * * Save this struct in the os - * * This type is sent to other client - * * Observer functions are fired - * - * @param {Y} y The Yjs instance - * @private - */ - _integrate (y) { - super._integrate(y) - this._y = y - // when integrating children we must make sure to - // integrate start - const start = this._start - if (start !== null) { - this._start = null - integrateChildren(y, start) - } - // integrate map children_integrate - const map = this._map - this._map = new Map() - for (let t of map.values()) { - // TODO make sure that right elements are deleted! - integrateChildren(y, t) - } - } - - _gcChildren (y) { - gcChildren(y, this._start) - this._start = null - this._map.forEach(item => { - gcChildren(y, item) - }) - this._map = new Map() - } - - _gc (y) { - this._gcChildren(y) - super._gc(y) - } - - /** - * @abstract - * @return {Object | Array | number | string} - */ - toJSON () {} - - /** - * Mark this Item as deleted. - * - * @param {Y} y The Yjs instance - * @param {boolean} createDelete Whether to propagate a message that this - * Type was deleted. - * @param {boolean} [gcChildren=(y._hasUndoManager===false)] Whether to garbage - * collect the children of this type. - * @private - */ - _delete (y, createDelete, gcChildren) { - if (gcChildren === undefined || !y.gcEnabled) { - gcChildren = y._hasUndoManager === false && y.gcEnabled - } - super._delete(y, createDelete, gcChildren) - y._transaction.changedTypes.delete(this) - // delete map types - for (let value of this._map.values()) { - if (value instanceof Item && !value._deleted) { - value._delete(y, false, gcChildren) - } - } - // delete array types - let t = this._start - while (t !== null) { - if (!t._deleted) { - t._delete(y, false, gcChildren) - } - t = t._right - } - if (gcChildren) { - this._gcChildren(y) - } - } -} diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js new file mode 100644 index 00000000..7a2f591c --- /dev/null +++ b/src/types/AbstractType.js @@ -0,0 +1,237 @@ +/** + * @module structs + */ + +import { Y } from '../utils/Y.js' // eslint-disable-line +import { EventHandler } from '../utils/EventHandler.js' +import { YEvent } from '../utils/YEvent.js' +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 +import { Transaction, nextID } from '../utils/Transaction.js' // eslint-disable-line + +/** + * Restructure children as if they were inserted one after another + * @param {Transaction} transaction + * @param {AbstractItem} start + */ +const integrateChildren = (transaction, start) => { + let right + while (true) { + right = start.right + start.id = nextID(transaction) + start.right = null + start.rightOrigin = null + start.origin = start.left + start.integrate(transaction) + if (right !== null) { + start = right + } else { + break + } + } +} + +/** + * Abstract Yjs Type class + */ +export class AbstractType { + constructor () { + /** + * @type {ItemType|null} + */ + this._item = null + /** + * @private + * @type {Map} + */ + this._map = new Map() + /** + * @private + * @type {AbstractItem|null} + */ + this._start = null + /** + * @private + * @type {Y|null} + */ + this._y = null + this._length = 0 + this._eventHandler = new EventHandler() + this._deepEventHandler = new EventHandler() + } + + /** + * Integrate this type into the Yjs instance. + * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Transaction} transaction The Yjs instance + * @param {ItemType} item + * @private + */ + _integrate (transaction, item) { + this._y = transaction.y + this._item = item + // when integrating children we must make sure to + // integrate start + const start = this._start + if (start !== null) { + this._start = null + integrateChildren(transaction, start) + } + // integrate map children_integrate + const map = this._map + this._map = new Map() + map.forEach(t => { + t.right = null + t.rightOrigin = null + integrateChildren(transaction, t) + }) + } + + /** + * @return {AbstractType} + */ + _copy () { + throw new Error('unimplemented') + } + + /** + * @param {Encoder} encoder + */ + _write (encoder) { + throw new Error('unimplemented') + } + + /** + * The first non-deleted item + */ + get _first () { + let n = this._start + while (n !== null && n.deleted) { + n = n.right + } + return n + } + + /** + * Creates YArray Event and calls observers. + * @private + */ + _callObserver (transaction, parentSubs, remote) { + this._callEventHandler(transaction, new YEvent(this)) + } + + /** + * Call event listeners with an event. This will also add an event to all + * parents (for `.observeDeep` handlers). + * @private + */ + _callEventHandler (transaction, event) { + const changedParentTypes = transaction.changedParentTypes + this._eventHandler.callEventListeners(transaction, event) + /** + * @type {any} + */ + let type = this + while (type !== this._y) { + let events = changedParentTypes.get(type) + if (events === undefined) { + events = [] + changedParentTypes.set(type, events) + } + events.push(event) + type = type._parent + } + } + + /** + * Helper method to transact if the y instance is available. + * + * TODO: Currently event handlers are not thrown when a type is not registered + * with a Yjs instance. + * @private + */ + _transact (f) { + const y = this._y + if (y !== null) { + y.transact(f) + } else { + f(y) + } + } + + /** + * Observe all events that are created on this type. + * + * @param {Function} f Observer function + */ + observe (f) { + this._eventHandler.addEventListener(f) + } + + /** + * Observe all events that are created by this type and its children. + * + * @param {Function} f Observer function + */ + observeDeep (f) { + this._deepEventHandler.addEventListener(f) + } + + /** + * Unregister an observer function. + * + * @param {Function} f Observer function + */ + unobserve (f) { + this._eventHandler.removeEventListener(f) + } + + /** + * Unregister an observer function. + * + * @param {Function} f Observer function + */ + unobserveDeep (f) { + this._deepEventHandler.removeEventListener(f) + } + + /** + * @abstract + * @return {Object | Array | number | string} + */ + toJSON () {} +} + +/** + * @param {AbstractType} type + * @return {Array} + */ +export const typeToArray = type => { + +} + +/** + * Executes a provided function on once on overy element of this YArray. + * + * @param {AbstractType} type + * @param {function(any,number,AbstractType):void} f A function to execute on every element of this YArray. + * @param {HistorySnapshot} [snapshot] + */ +export const typeForEach = (type, f, snapshot) => { + let index = 0 + let n = type._start + while (n !== null) { + if (isVisible(n, snapshot) && n._countable) { + const c = n.getContent() + for (let i = 0; i < c.length; i++) { + f(c[i], index++, type) + } + } + n = n._right + } +} diff --git a/src/types/YArray.js b/src/types/YArray.js index 1e04527a..3f6e23aa 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -2,14 +2,14 @@ * @module types */ -import { Type } from '../structs/Type.js' +import { AbstractType } from './AbstractType.js' import { ItemJSON } from '../structs/ItemJSON.js' import { ItemString } from '../structs/ItemString.js' +import { ItemBinary } from '../structs/ItemBinary.js' +import { stringifyItemID, logItemHelper } from '../structs/AbstractItem.js' // eslint-disable-line import { YEvent } from '../utils/YEvent.js' import { Transaction } from '../utils/Transaction.js' // eslint-disable-line -import { Item, stringifyItemID, logItemHelper } from '../structs/Item.js' // eslint-disable-line -import { ItemBinary } from '../structs/ItemBinary.js' -import { isVisible } from '../utils/snapshot.js' +import { isVisible, HistorySnapshot } from '../utils/snapshot.js' // eslint-disable-line /** * Event that describes the changes on a YArray @@ -38,8 +38,8 @@ export class YArrayEvent extends YEvent { const target = this.target const transaction = this._transaction const addedElements = new Set() - transaction.newTypes.forEach(type => { - if (type._parent === target && !transaction.deletedStructs.has(type)) { + transaction.added.forEach(type => { + if (type._parent === target && !transaction.deleted.has(type)) { addedElements.add(type) } }) @@ -58,8 +58,8 @@ export class YArrayEvent extends YEvent { const target = this.target const transaction = this._transaction const removedElements = new Set() - transaction.deletedStructs.forEach(struct => { - if (struct._parent === target && !transaction.newTypes.has(struct)) { + transaction.deleted.forEach(struct => { + if (struct._parent === target && !transaction.added.has(struct)) { removedElements.add(struct) } }) @@ -72,7 +72,7 @@ export class YArrayEvent extends YEvent { /** * A shared Array implementation. */ -export class YArray extends Type { +export class YArray extends AbstractType { constructor () { super() this.length = 0 @@ -128,7 +128,7 @@ export class YArray extends Type { */ toJSON () { return this.map(c => { - if (c instanceof Type) { + if (c instanceof AbstractType) { return c.toJSON() } return c @@ -140,7 +140,7 @@ export class YArray extends Type { * element of this YArray. * * @param {Function} f Function that produces an element of the new Array - * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] + * @param {HistorySnapshot} [snapshot] * @return {Array} A new array with each element being the result of the * callback function */ @@ -156,7 +156,7 @@ export class YArray extends Type { * Executes a provided function on once on overy element of this YArray. * * @param {Function} f A function to execute on every element of this YArray. - * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] + * @param {HistorySnapshot} [snapshot] */ forEach (f, snapshot) { let index = 0 @@ -404,3 +404,5 @@ export class YArray extends Type { return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`) } } + +export const readYArray = decoder => new YArray() \ No newline at end of file diff --git a/src/types/YMap.js b/src/types/YMap.js index fbcdfe9b..c411a084 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -2,12 +2,11 @@ * @module types */ -import { Item, logItemHelper } from '../structs/Item.js' -import { Type } from '../structs/Type.js' +import { AbstractType } from './AbstractType.js' import { ItemJSON } from '../structs/ItemJSON.js' import { YEvent } from '../utils/YEvent.js' import { ItemBinary } from '../structs/ItemBinary.js' -import { isVisible } from '../utils/snapshot.js' +import { HistorySnapshot, isVisible } from '../utils/snapshot.js' // eslint-disable-line /** * Event that describes the changes on a YMap. @@ -28,7 +27,7 @@ export class YMapEvent extends YEvent { /** * A shared Map implementation. */ -export class YMap extends Type { +export class YMap extends AbstractType { /** * Creates YMap Event and calls observers. * @@ -68,7 +67,7 @@ export class YMap extends Type { /** * Returns the keys for each element in the YMap Type. * - * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] + * @param {HistorySnapshot} [snapshot] * @return {Array} */ keys (snapshot) { @@ -156,7 +155,7 @@ export class YMap extends Type { * Returns a specified element from this YMap. * * @param {string} key The key of the element to return. - * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] + * @param {HistorySnapshot} [snapshot] */ get (key, snapshot) { let v = this._map.get(key) @@ -184,7 +183,7 @@ export class YMap extends Type { * Returns a boolean indicating whether the specified key exists or not. * * @param {string} key The key to test. - * @param {import('../protocols/history.js').HistorySnapshot} [snapshot] + * @param {HistorySnapshot} [snapshot] */ has (key, snapshot) { let v = this._map.get(key) @@ -210,3 +209,5 @@ export class YMap extends Type { return logItemHelper('YMap', this, `mapSize:${this._map.size}`) } } + +export const readYMap = decoder => new YMap() \ No newline at end of file diff --git a/src/types/YText.js b/src/types/YText.js index 0d78f1de..5bbf5250 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -2,7 +2,7 @@ * @module types */ -import { logItemHelper } from '../structs/Item.js' +import { logItemHelper } from '../structs/AbstractItem.js/index.js' import { ItemEmbed } from '../structs/ItemEmbed.js' import { ItemString } from '../structs/ItemString.js' import { ItemFormat } from '../structs/ItemFormat.js' @@ -723,3 +723,5 @@ export class YText extends YArray { return logItemHelper('YText', this) } } + +export const readYText = decoder => new YText() \ No newline at end of file diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js index cd919739..a5a50799 100644 --- a/src/types/YXmlElement.js +++ b/src/types/YXmlElement.js @@ -2,7 +2,7 @@ * @module types */ -import { logItemHelper } from '../structs/Item.js' +import { logItemHelper } from '../structs/AbstractItem.js/index.js' import { YMap } from './YMap.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' @@ -433,3 +433,6 @@ export class YXmlElement extends YXmlFragment { return dom } } + +export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder)) +export const readYXmlFragment = decoder => new YXmlFragment() \ No newline at end of file diff --git a/src/types/YXmlEvent.js b/src/types/YXmlEvent.js index e41d9f26..54f9589d 100644 --- a/src/types/YXmlEvent.js +++ b/src/types/YXmlEvent.js @@ -4,7 +4,7 @@ import { YEvent } from '../utils/YEvent.js' -import { Type } from '../structs/Type.js' // eslint-disable-line +import { Type } from './AbstractType.js/index.js.js.js.js' // eslint-disable-line import { Transaction } from '../utils/Transaction.js' // eslint-disable-line /** diff --git a/src/types/YXmlHook.js b/src/types/YXmlHook.js index e41c3536..fcc3de3f 100644 --- a/src/types/YXmlHook.js +++ b/src/types/YXmlHook.js @@ -114,3 +114,5 @@ export class YXmlHook extends YMap { super._integrate(y) } } + +export const readYXmlHook = decoder => new YXmlHook() \ No newline at end of file diff --git a/src/types/YXmlText.js b/src/types/YXmlText.js index 9b31151c..58f87f79 100644 --- a/src/types/YXmlText.js +++ b/src/types/YXmlText.js @@ -48,3 +48,5 @@ export class YXmlText extends YText { super._delete(y, createDelete, gcChildren) } } + +export const readYXmlText = decoder => new YXmlText() diff --git a/src/utils/BindMapping.js b/src/utils/BindMapping.js deleted file mode 100644 index 43568314..00000000 --- a/src/utils/BindMapping.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Type that maps from Yjs type to Target type. - * Used to implement double bindings. - * - * @private - * @template Y - * @template T - */ -export class BindMapping { - /** - */ - constructor () { - /** - * @type Map - */ - this.yt = new Map() - /** - * @type Map - */ - this.ty = new Map() - } - /** - * Map y to t. Removes all existing bindings from y and t - * @param {Y} y - * @param {T} t - */ - bind (y, t) { - const existingT = this.yt.get(y) - if (existingT !== undefined) { - this.ty.delete(existingT) - } - const existingY = this.ty.get(t) - if (existingY !== undefined) { - this.yt.delete(existingY) - } - this.yt.set(y, t) - this.ty.set(t, y) - } - /** - * @param {Y} y - * @return {boolean} - */ - hasY (y) { - return this.yt.has(y) - } - /** - * @param {T} t - * @return {boolean} - */ - hasT (t) { - return this.ty.has(t) - } - /** - * @param {Y} y - * @return {T} - */ - getY (y) { - return this.yt.get(y) - } - /** - * @param {T} t - * @return {Y} - */ - getT (t) { - return this.ty.get(t) - } -} diff --git a/src/utils/DeleteSet.js b/src/utils/DeleteSet.js new file mode 100644 index 00000000..e1b6aa21 --- /dev/null +++ b/src/utils/DeleteSet.js @@ -0,0 +1,142 @@ +import * as map from 'lib0/map.js' +import * as encoding from 'lib0/encoding.js' +import * as decoding from 'lib0/decoding.js' +import { StructStore, getItemRange } from './StructStore.js' // eslint-disable-line +import { Transaction } from './Transaction.js' // eslint-disable-line + +class DeleteItem { + /** + * @param {number} clock + * @param {number} len + */ + constructor (clock, len) { + /** + * @type {number} + */ + this.clock = clock + /** + * @type {number} + */ + this.len = len + } +} + +/** + * We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed. + * - When created in a transaction, it must only be accessed after sorting, and merging + * - This DeleteSet is send to other clients + * - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore + * - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged. + * + */ +export class DeleteSet { + constructor () { + /** + * @type {Map>} + */ + this.clients = new Map() + } +} + +/** + * @param {DeleteSet} ds + */ +export const sortAndMergeDeleteSet = ds => { + ds.clients.forEach(dels => { + dels.sort((a, b) => a.clock - b.clock) + // i is the current pointer + // j refers to the current insert position for the pointed item + // try to merge dels[i] with dels[i-1] + let i, j + for (i = 1, j = 1; i < dels.length; i++) { + const left = dels[i - 1] + const right = dels[i] + if (left.clock + left.len === right.clock) { + left.len += right.len + } else { + if (j < i) { + dels[j] = right + } + j++ + } + } + dels.length = j + }) +} + +/** + * @param {Transaction} transaction + */ +export const createDeleteSetFromTransaction = transaction => { + const ds = new DeleteSet() + transaction.deleted.forEach(item => { + map.setTfUndefined(ds.clients, item.id.client, () => []).push(new DeleteItem(item.id.clock, item.length)) + }) + sortAndMergeDeleteSet(ds) + return ds +} + +/** + * @param {StructStore} ss + * @return {DeleteSet} Merged and sorted DeleteSet + */ +export const createDeleteSetFromStructStore = ss => { + const ds = new DeleteSet() + ss.clients.forEach((structs, client) => { + /** + * @type {Array} + */ + const dsitems = [] + for (let i = 0; i < structs.length; i++) { + const struct = structs[i] + const clock = struct.id.clock + let len = struct.length + if (i + 1 < structs.length) { + for (let next = structs[i + 1]; i + 1 < structs.length && next.id.clock === clock + len; i++) { + len += next.length + } + } + dsitems.push(new DeleteItem(clock, len)) + } + if (dsitems.length > 0) { + ds.clients.set(client, dsitems) + } + }) + return ds +} + +/** + * @param {encoding.Encoder} encoder + * @param {DeleteSet} ds + */ +export const writeDeleteSet = (encoder, ds) => { + encoding.writeVarUint(encoder, ds.clients.size) + ds.clients.forEach((dsitems, client) => { + encoding.writeVarUint(encoder, client) + const len = dsitems.length + encoding.writeVarUint(encoder, len) + for (let i = 0; i < len; i++) { + const item = dsitems[i] + encoding.writeVarUint(encoder, item.clock) + encoding.writeVarUint(encoder, item.len) + } + }) +} + +/** + * @param {decoding.Decoder} decoder + * @param {StructStore} ss + * @param {Transaction} transaction + */ +export const readDeleteSet = (decoder, ss, transaction) => { + const numClients = decoding.readVarUint(decoder) + for (let i = 0; i < numClients; i++) { + const client = decoding.readVarUint(decoder) + const len = decoding.readVarUint(decoder) + for (let i = 0; i < len; i++) { + const clock = decoding.readVarUint(decoder) + const len = decoding.readVarUint(decoder) + getItemRange(ss, transaction, client, clock, len).forEach(struct => struct.delete(transaction)) + } + } +} diff --git a/src/utils/DeleteStore.js b/src/utils/DeleteStore.js deleted file mode 100644 index f48c9534..00000000 --- a/src/utils/DeleteStore.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @module utils - */ - -import { Tree } from 'lib0/tree.js' -import * as ID from './ID.js' - -export class DSNode { - constructor (id, len, gc) { - this._id = id - this.len = len - this.gc = gc - } - clone () { - return new DSNode(this._id, this.len, this.gc) - } -} - -export class DeleteStore extends Tree { - logTable () { - const deletes = [] - this.iterate(null, null, n => { - deletes.push({ - user: n._id.user, - clock: n._id.clock, - len: n.len, - gc: n.gc - }) - }) - console.table(deletes) - } - isDeleted (id) { - var n = this.findWithUpperBound(id) - return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len - } - mark (id, length, gc) { - if (length === 0) return - // Step 1. Unmark range - const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1)) - // Resize left DSNode if necessary - if (leftD !== null && leftD._id.user === id.user) { - if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) { - // node is overlapping. need to resize - if (id.clock + length < leftD._id.clock + leftD.len) { - // overlaps new mark range and some more - // create another DSNode to the right of new mark - this.put(new DSNode(ID.createID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc)) - } - // resize left DSNode - leftD.len = id.clock - leftD._id.clock - } // Otherwise there is no overlapping - } - // Resize right DSNode if necessary - const upper = ID.createID(id.user, id.clock + length - 1) - const rightD = this.findWithUpperBound(upper) - if (rightD !== null && rightD._id.user === id.user) { - if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node - const d = id.clock + length - rightD._id.clock - rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d) - rightD.len -= d - } - } - // Now we only have to delete all inner marks - const deleteNodeIds = [] - this.iterate(id, upper, m => { - deleteNodeIds.push(m._id) - }) - for (let i = deleteNodeIds.length - 1; i >= 0; i--) { - this.delete(deleteNodeIds[i]) - } - let newMark = new DSNode(id, length, gc) - // Step 2. Check if we can extend left or right - if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) { - // We can extend left - leftD.len += length - newMark = leftD - } - const rightNext = this.find(ID.createID(id.user, id.clock + length)) - if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) { - // We can merge newMark and rightNext - newMark.len += rightNext.len - this.delete(rightNext._id) - } - if (leftD !== newMark) { - // only put if we didn't extend left - this.put(newMark) - } - } -} diff --git a/src/utils/ID.js b/src/utils/ID.js index 682bd0a3..e3e37925 100644 --- a/src/utils/ID.js +++ b/src/utils/ID.js @@ -2,81 +2,72 @@ * @module utils */ -import { getStructReference } from '../utils/structReferences.js' import * as decoding from 'lib0/decoding.js' import * as encoding from 'lib0/encoding.js' export class ID { - constructor (user, clock) { - this.user = user // TODO: rename to client + /** + * @param {number} client client id + * @param {number} clock unique per client id, continuous number + */ + constructor (client, clock) { + /** + * @type {number} Client id + */ + this.client = client + /** + * @type {number} unique per client id, continuous number + */ this.clock = clock } + /** + * @return {ID} + */ clone () { - return new ID(this.user, this.clock) + return new ID(this.client, this.clock) } + /** + * @param {ID} id + * @return {boolean} + */ equals (id) { - return id !== null && id.user === this.user && id.clock === this.clock + return id !== null && id.client === this.client && id.clock === this.clock } + /** + * @param {ID} id + * @return {boolean} + */ lessThan (id) { if (id.constructor === ID) { - return this.user < id.user || (this.user === id.user && this.clock < id.clock) + return this.client < id.client || (this.client === id.client && this.clock < id.clock) } else { return false } } - /** - * @param {encoding.Encoder} encoder - */ - encode (encoder) { - encoding.writeVarUint(encoder, this.user) - encoding.writeVarUint(encoder, this.clock) - } -} - -export const createID = (user, clock) => new ID(user, clock) - -export const RootFakeUserID = 0xFFFFFF - -export class RootID { - /** - * @param {string} name - * @param {Function?} typeConstructor - */ - constructor (name, typeConstructor) { - this.user = RootFakeUserID - this.name = name - this.type = typeConstructor === null ? null : getStructReference(typeConstructor) - } - equals (id) { - return id !== null && id.user === this.user && id.name === this.name && id.type === this.type - } - lessThan (id) { - if (id.constructor === RootID) { - return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type))) - } else { - return true - } - } - /** - * @param {encoding.Encoder} encoder - */ - encode (encoder) { - encoding.writeVarUint(encoder, this.user) - encoding.writeVarString(encoder, this.name) - encoding.writeVarUint(encoder, this.type) - } } /** - * Create a new root id. - * - * @example - * y.define('name', Y.Array) // name, and typeConstructor - * - * @param {string} name - * @param {Function?} typeConstructor must be defined in structReferences + * @param {number} client + * @param {number} clock */ -export const createRootID = (name, typeConstructor) => new RootID(name, typeConstructor) +export const createID = (client, clock) => new ID(client, clock) + +const isNullID = 0xFFFFFF + +/** + * @param {encoding.Encoder} encoder + * @param {ID} id + */ +export const writeID = (encoder, id) => { + encoding.writeVarUint(encoder, id.client) + encoding.writeVarUint(encoder, id.clock) +} + +/** + * @param {encoding.Encoder} encoder + */ +export const writeNullID = (encoder) => + encoding.writeVarUint(encoder, isNullID) /** * Read ID. @@ -84,15 +75,9 @@ export const createRootID = (name, typeConstructor) => new RootID(name, typeCons * * Otherwise an ID is returned * * @param {decoding.Decoder} decoder - * @return {ID|RootID} + * @return {ID | null} */ -export const decode = decoder => { - const user = decoding.readVarUint(decoder) - if (user === RootFakeUserID) { - // read property name and type id - const rid = createRootID(decoding.readVarString(decoder), null) - rid.type = decoding.readVarUint(decoder) - return rid - } - return createID(user, decoding.readVarUint(decoder)) +export const readID = decoder => { + const client = decoding.readVarUint(decoder) + return client === isNullID ? null : createID(client, decoding.readVarUint(decoder)) } diff --git a/src/utils/OperationStore.js b/src/utils/OperationStore.js deleted file mode 100644 index 4530234d..00000000 --- a/src/utils/OperationStore.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @module utils - */ - -import { Tree } from 'lib0/tree.js' -import * as ID from '../utils/ID.js' -import { getStruct } from './structReferences.js' -import { GC } from '../structs/GC.js' -import * as Item from '../structs/Item.js' - -export class OperationStore extends Tree { - constructor (y) { - super() - this.y = y - } - logTable () { - const items = [] - this.iterate(null, null, item => { - if (item.constructor === GC) { - items.push({ - id: Item.stringifyItemID(item), - content: item._length, - deleted: 'GC' - }) - } else { - items.push({ - id: Item.stringifyItemID(item), - origin: item._origin === null ? '()' : Item.stringifyID(item._origin._lastId), - left: item._left === null ? '()' : Item.stringifyID(item._left._lastId), - right: Item.stringifyItemID(item._right), - right_origin: Item.stringifyItemID(item._right_origin), - parent: Item.stringifyItemID(item._parent), - parentSub: item._parentSub, - deleted: item._deleted, - content: JSON.stringify(item._content) - }) - } - }) - console.table(items) - } - get (id) { - let struct = this.find(id) - if (struct === null && id instanceof ID.RootID) { - const Constr = getStruct(id.type) - const y = this.y - struct = new Constr() - struct._id = id - struct._parent = y - y.transact(() => { - struct._integrate(y) - }) - this.put(struct) - } - return struct - } - // Use getItem for structs with _length > 1 - getItem (id) { - var item = this.findWithUpperBound(id) - if (item === null) { - return null - } - const itemID = item._id - if (id.user === itemID.user && id.clock < itemID.clock + item._length) { - return item - } else { - return null - } - } - // Return an insertion such that id is the first element of content - // This function manipulates an item, if necessary - getItemCleanStart (id) { - var ins = this.getItem(id) - if (ins === null || ins._length === 1) { - return ins - } - const insID = ins._id - if (insID.clock === id.clock) { - return ins - } else { - return ins._splitAt(this.y, id.clock - insID.clock) - } - } - // Return an insertion such that id is the last element of content - // This function manipulates an operation, if necessary - getItemCleanEnd (id) { - var ins = this.getItem(id) - if (ins === null || ins._length === 1) { - return ins - } - const insID = ins._id - if (insID.clock + ins._length - 1 === id.clock) { - return ins - } else { - ins._splitAt(this.y, id.clock - insID.clock + 1) - return ins - } - } -} diff --git a/src/utils/StateStore.js b/src/utils/StateStore.js deleted file mode 100644 index e44d1443..00000000 --- a/src/utils/StateStore.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @module utils - */ - -import * as ID from '../utils/ID.js' - -/** - */ -export class StateStore { - constructor (y) { - this.y = y - this.state = new Map() - } - logTable () { - const entries = [] - for (let [user, state] of this.state) { - entries.push({ - user, state - }) - } - console.table(entries) - } - getNextID (len) { - const user = this.y.userID - const state = this.getState(user) - this.setState(user, state + len) - return ID.createID(user, state) - } - updateRemoteState (struct) { - let user = struct._id.user - let userState = this.state.get(user) - while (struct !== null && struct._id.clock === userState) { - userState += struct._length - struct = this.y.os.get(ID.createID(user, userState)) - } - this.state.set(user, userState) - } - getState (user) { - let state = this.state.get(user) - if (state == null) { - return 0 - } - return state - } - setState (user, state) { - // TODO: modify missingi structs here - const beforeState = this.y._transaction.beforeState - if (!beforeState.has(user)) { - beforeState.set(user, this.getState(user)) - } - this.state.set(user, state) - } -} diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js new file mode 100644 index 00000000..cefc4c7b --- /dev/null +++ b/src/utils/StructStore.js @@ -0,0 +1,223 @@ +import { AbstractStruct } from '../structs/AbstractStruct.js' // eslint-disable-line +import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line +import { ItemType } from '../structs/ItemType.js' // eslint-disable-line +import { ID } from './ID.js' // eslint-disable-line +import { Transaction } from './Transaction.js' // eslint-disable-line +import * as map from 'lib0/map.js' +import * as math from 'lib0/math.js' + +export class StructStore { + constructor () { + /** + * @type {Map>} + */ + this.clients = new Map() + } +} + +/** + * Return the states as an array of {client,clock} pairs. + * Note that clock refers to the next expected clock id. + * + * @param {StructStore} store + * @return {Array<{client:number,clock:number}>} + */ +export const getStates = store => + map.map(store.clients, (structs, client) => { + const struct = structs[structs.length - 1] + return { + client, + clock: struct.id.clock + struct.length + } + }) + +/** + * @param {StructStore} store + */ +export const integretyCheck = store => { + store.clients.forEach(structs => { + for (let i = 1; i < structs.length; i++) { + const l = structs[i - 1] + const r = structs[i] + if (l.id.clock + l.length !== r.id.clock) { + throw new Error('StructStore failed integrety check') + } + } + }) +} + +/** + * @param {StructStore} store + * @param {AbstractStruct} struct + */ +export const addStruct = (store, struct) => { + map.setTfUndefined(store.clients, struct.id.client, () => []).push(struct) +} + +/** + * Expects that id is actually in store. This function throws or is an infinite loop otherwise. + * @param {Array} structs // ordered structs without holes + * @param {number} clock + * @return {number} + * @private + */ +export const findIndex = (structs, clock) => { + let left = 0 + let right = structs.length + while (left <= right) { + const midindex = math.floor((left + right) / 2) + const mid = structs[midindex] + const midclock = mid.id.clock + if (midclock <= clock) { + if (clock < midclock + mid.length) { + return midindex + } + left = midindex + } else { + right = midindex + } + } + throw new Error('ID does not exist') +} + +/** + * Expects that id is actually in store. This function throws or is an infinite loop otherwise. + * + * @param {StructStore} store + * @param {ID} id + * @return {AbstractStruct} + * @private + */ +const find = (store, id) => { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(id.client) + return structs[findIndex(structs, id.clock)] +} + +/** + * Expects that id is actually in store. This function throws or is an infinite loop otherwise. + * + * @param {StructStore} store + * @param {ID} id + * @return {ItemType} + */ +// @ts-ignore +export const getItemType = (store, id) => find(store, id) + +/** + * @param {Transaction} transaction + * @param {AbstractItem} struct + * @param {number} diff + */ +const splitStruct = (transaction, struct, diff) => { + const right = struct.splitAt(diff) + if (transaction.added.has(struct)) { + transaction.added.add(right) + } else if (transaction.deleted.has(struct)) { + transaction.deleted.add(right) + } + return right +} + +/** + * Expects that id is actually in store. This function throws or is an infinite loop otherwise. + * @param {StructStore} store + * @param {Transaction} transaction + * @param {ID} id + * @return {AbstractItem} + * + * @private + */ +export const getItemCleanStart = (store, transaction, id) => { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(id.client) + const index = findIndex(structs, id.clock) + /** + * @type {any} + */ + let struct = structs[index] + if (struct.id.clock < id.clock) { + struct = splitStruct(transaction, struct, id.clock - struct.id.clock) + structs.splice(index, 0, struct) + } + return struct +} + +/** + * Expects that id is actually in store. This function throws or is an infinite loop otherwise. + * @param {StructStore} store + * @param {Transaction} transaction + * @param {ID} id + * @return {AbstractItem} + * + * @private + */ +export const getItemCleanEnd = (store, transaction, id) => { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(id.client) + const index = findIndex(structs, id.clock) + const struct = structs[index] + if (id.clock !== struct.id.clock + struct.length - 1) { + structs.splice(index, 0, splitStruct(transaction, struct, id.clock - struct.id.clock + 1)) + } + return struct +} + +/** + * Expects that id is actually in store. This function throws or is an infinite loop otherwise. + * @param {StructStore} store + * @param {Transaction} transaction + * @param {number} client + * @param {number} clock + * @param {number} len + * @return {Array} + * + * @private + */ +export const getItemRange = (store, transaction, client, clock, len) => { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(client) + let index = findIndex(structs, clock) + let struct = structs[index] + let range = [] + if (struct.id.clock < clock) { + struct = splitStruct(transaction, struct, clock - struct.id.clock) + structs.splice(index, 0, struct) + } + while (struct.id.clock + struct.length <= clock + len) { + range.push(struct) + struct = structs[++index] + } + if (clock < struct.id.clock + struct.length) { + structs.splice(index, 0, splitStruct(transaction, struct, clock + len - struct.id.clock)) + range.push(struct) + } + return range +} + +/** + * Replace `item` with `newitem` in store + * @param {StructStore} store + * @param {AbstractStruct} struct + * @param {AbstractStruct} newStruct + */ +export const replace = (store, struct, newStruct) => { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(struct.id.client) + structs[findIndex(structs, struct.id)] = newStruct +} diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 3cea6b9d..49f3b8f8 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -3,6 +3,13 @@ */ import * as encoding from 'lib0/encoding.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line +import { Y } from './Y.js' // eslint-disable-line +import { YEvent } from './YEvent.js' // eslint-disable-line +import { ItemType } from '../structs/ItemType.js' // eslint-disable-line +import { getState } from './StructStore.js' +import { createID } from './ID.js' // eslint-disable-line /** * A transaction is created for every change on the Yjs model. It is possible @@ -28,42 +35,52 @@ import * as encoding from 'lib0/encoding.js' * */ export class Transaction { + /** + * @param {Y} y + */ constructor (y) { /** * @type {Y} The Yjs instance. */ this.y = y /** - * All new types that are added during a transaction. - * @type {Set} + * All new items that are added during a transaction. + * @type {Set} */ - this.newTypes = new Set() + this.added = new Set() /** - * All types that were directly modified (property added or child - * inserted/deleted). New types are not included in this Set. - * Maps from type to parentSubs (`item._parentSub = null` for YArray) - * @type {Map} + * Set of all deleted items + * @type {Set} */ - this.changedTypes = new Map() - // TODO: rename deletedTypes - /** - * Set of all deleted Types and Structs. - * @type {Set} - */ - this.deletedStructs = new Set() + this.deleted = new Set() /** * Saves the old state set of the Yjs instance. If a state was modified, * the original value is saved here. * @type {Map} */ this.beforeState = new Map() + /** + * All types that were directly modified (property added or child + * inserted/deleted). New types are not included in this Set. + * Maps from type to parentSubs (`item._parentSub = null` for YArray) + * @type {Map>} + */ + this.changed = new Map() /** * Stores the events for the types that observe also child elements. * It is mainly used by `observeDeep`. - * @type {Map>} + * @type {Map>} */ this.changedParentTypes = new Map() this.encodedStructsLen = 0 this.encodedStructs = encoding.createEncoder() } } + +/** + * @param {Transaction} transaction + */ +export const nextID = transaction => { + const y = transaction.y + return createID(y.clientID, getState(y.store, y.clientID)) +} diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index 4eae011c..7b03a5ab 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -126,7 +126,7 @@ export class UndoManager { this._redoing === false && this._lastTransactionWasUndo === false && lastUndoOp !== null && - (options.captureTimeout < 0 || reverseOperation.created - lastUndoOp.created <= options.captureTimeout) + ((options.captureTimeout < 0) || (reverseOperation.created.getTime() - lastUndoOp.created.getTime()) <= options.captureTimeout) ) { lastUndoOp.created = reverseOperation.created if (reverseOperation.toState !== null) { diff --git a/src/utils/Y.js b/src/utils/Y.js index ec2f90e9..80e5797a 100644 --- a/src/utils/Y.js +++ b/src/utils/Y.js @@ -1,10 +1,14 @@ -import { DeleteStore } from './DeleteStore.js' +import { DeleteStore } from './DeleteSet.js/index.js' // TODO: remove import { OperationStore } from './OperationStore.js' import { StateStore } from './StateStore.js' +import { StructStore } from './StructStore.js' import * as random from 'lib0/random.js' -import { createRootID } from './ID.js' +import * as map from 'lib0/map.js' import { Observable } from 'lib0/observable.js' import { Transaction } from './Transaction.js' +import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' // eslint-disable-line +import { AbstractType } from '../types/AbstractType.js' +import { YArray } from '../types/YArray.js' /** * Anything that can be encoded with `JSON.stringify` and can be decoded with @@ -28,25 +32,33 @@ export class Y extends Observable { constructor (conf = {}) { super() this.gcEnabled = conf.gc || false - this._contentReady = false - this.userID = random.uint32() - // TODO: This should be a Map so we can use encodables as keys - this._map = new Map() - this.ds = new DeleteStore() - this.os = new OperationStore(this) - this.ss = new StateStore(this) + this.clientID = random.uint32() + this.share = new Map() + this.store = new StructStore() + /** + * @type {Map>} + */ this._missingStructs = new Map() + /** + * @type {Array} + */ this._readyToIntegrate = [] + /** + * @type {Transaction | null} + */ this._transaction = null - this.connected = false - // for compatibility with isParentOf - this._parent = null this._hasUndoManager = false - this._deleted = false // for compatiblity of having this as a parent for types - this._id = null } - _beforeChange () {} - _callObserver (transaction, subs, remote) {} + /** + * @type {Transaction} + */ + get transaction () { + const t = this._transaction + if (t === null) { + throw new Error('All changes must happen inside a transaction') + } + return t + } /** * Changes that happen inside of a transaction are bundled. This means that * the observer fires _after_ the transaction is finished and that all changes @@ -59,8 +71,9 @@ export class Y extends Observable { * Defaults to false. */ transact (f, remote = false) { - let initialCall = this._transaction === null - if (initialCall) { + let initialCall = false + if (this._transaction === null) { + initialCall = true this._transaction = new Transaction(this) this.emit('beforeTransaction', [this, this._transaction, remote]) } @@ -74,9 +87,9 @@ export class Y extends Observable { const transaction = this._transaction this._transaction = null // emit change events on changed types - transaction.changedTypes.forEach((subs, type) => { - if (!type._deleted) { - type._callObserver(transaction, subs, remote) + transaction.changed.forEach((subs, itemtype) => { + if (!itemtype._deleted) { + itemtype.type._callObserver(transaction, subs, remote) } }) transaction.changedParentTypes.forEach((events, type) => { @@ -91,86 +104,57 @@ export class Y extends Observable { }) // we don't have to check for events.length // because there is no way events is empty.. - type._deepEventHandler.callEventListeners(transaction, events) + type.type._deepEventHandler.callEventListeners(transaction, events) } }) // when all changes & events are processed, emit afterTransaction event this.emit('afterTransaction', [this, transaction, remote]) } } - - /** - * Fake _start for root properties (y.set('name', type)) - * - * @private - */ - get _start () { - return null - } - - /** - * Fake _start for root properties (y.set('name', type)) - * - * @private - */ - set _start (start) {} - /** * Define a shared data type. * - * Multiple calls of `y.define(name, TypeConstructor)` yield the same result + * Multiple calls of `y.get(name, TypeConstructor)` yield the same result * and do not overwrite each other. I.e. - * `y.define(name, type) === y.define(name, type)` + * `y.define(name, Y.Array) === y.define(name, Y.Array)` * - * After this method is called, the type is also available on `y._map.get(name)`. + * After this method is called, the type is also available on `y.share.get(name)`. * * *Best Practices:* - * Either define all types right after the Yjs instance is created or always - * use `y.define(..)` when accessing a type. + * Define all types right after the Yjs instance is created and store them in a separate object. + * Also use the typed methods `getText(name)`, `getArray(name)`, .. * * @example - * // Option 1 * const y = new Y(..) - * y.define('myArray', YArray) - * y.define('myMap', YMap) - * // .. when accessing the type use y._map.get(name) - * y.share.myArray.insert(..) - * y.share.myMap.set(..) + * const appState = { + * document: y.getText('document') + * comments: y.getArray('comments') + * } * - * // Option2 - * const y = new Y(..) - * // .. when accessing the type use `y.define(..)` - * y.define('myArray', YArray).insert(..) - * y.define('myMap', YMap).set(..) + * @TODO: implement getText, getArray, .. * - * @param {String} name + * @param {string} name * @param {Function} TypeConstructor The constructor of the type definition - * @returns {any} The created type. Constructed with TypeConstructor + * @return {AbstractType} The created type. Constructed with TypeConstructor */ - define (name, TypeConstructor) { - let id = createRootID(name, TypeConstructor) - let type = this.os.get(id) - if (this._map.get(name) === undefined) { - this._map.set(name, type) - } else if (this._map.get(name) !== type) { - throw new Error('Type is already defined with a different constructor') + get (name, TypeConstructor = AbstractType) { + // @ts-ignore + const type = map.setTfUndefined(this.share, name, () => new TypeConstructor()) + const Constr = type.constructor + if (Constr !== TypeConstructor) { + if (Constr === AbstractType) { + const t = new Constr() + t._map = type._map + t._start = type._start + t._length = type._length + this.share.set(name, t) + return t + } else { + throw new Error(`Type with the name ${name} has already been defined with a different constructor`) + } } return type } - - /** - * Get a defined type. The type must be defined locally. First define the - * type with {@link define}. - * - * This returns the same value as `y.share[name]` - * - * @param {String} name The typename - * @return {any} - */ - get (name) { - return this._map.get(name) - } - /** * Disconnect from the room, and destroy all traces of this Yjs instance. */ diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index b644b87e..3555d733 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -1,3 +1,6 @@ +import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line + /** * @module utils */ @@ -7,17 +10,17 @@ */ export class YEvent { /** - * @param {Type} target The changed type. + * @param {AbstractType} target The changed type. */ constructor (target) { /** * The type on which this event was created on. - * @type {Type} + * @type {AbstractType} */ this.target = target /** * The current target on which the observe callback is called. - * @type {Type} + * @type {AbstractType} */ this.currentTarget = target } @@ -34,6 +37,48 @@ export class YEvent { * type === event.target // => true */ get path () { - return this.currentTarget.getPathTo(this.target) + // @ts-ignore _item is defined because target is integrated + return getPathTo(this.currentTarget, this.target._item) + } +} + +/** + * Compute the path from this type to the specified target. + * + * @example + * // `child` should be accessible via `type.get(path[0]).get(path[1])..` + * const path = type.getPathTo(child) + * // assuming `type instanceof YArray` + * console.log(path) // might look like => [2, 'key1'] + * child === type.get(path[0]).get(path[1]) + * + * @param {AbstractType} parent + * @param {AbstractItem} child target + * @return {Array} Path to the target + */ +const getPathTo = (parent, child) => { + const path = [] + while (true) { + const cparent = child.parent + if (child.parentSub !== null) { + // parent is map-ish + path.unshift(child.parentSub) + } else { + // parent is array-ish + let i = 0 + let c = cparent._start + while (c !== child && c !== null) { + if (!c.deleted) { + i++ + } + c = c.right + } + path.unshift(i) + } + if (parent === cparent) { + return path + } + // @ts-ignore parent._item cannot be null, because it is integrated + child = parent._item } } diff --git a/src/utils/defragmentItemContent.js b/src/utils/defragmentItemContent.js deleted file mode 100644 index 19c57b84..00000000 --- a/src/utils/defragmentItemContent.js +++ /dev/null @@ -1,61 +0,0 @@ - -/** - * @module utils - */ - -import * as ID from '../utils/ID.js' -import { ItemJSON } from '../structs/ItemJSON.js' -import { ItemString } from '../structs/ItemString.js' - -/** - * Try to merge all items in os with their successors. - * - * Some transformations (like delete) fragment items. - * Item(c: 'ab') + Delete(1,1) + Delete(0, 1) -> Item(c: 'a',deleted);Item(c: 'b',deleted) - * - * This functions merges the fragmented nodes together: - * Item(c: 'a',deleted);Item(c: 'b',deleted) -> Item(c: 'ab', deleted) - * - * TODO: The Tree implementation does not support deletions in-spot. - * This is why all deletions must be performed after the traversal. - * - */ -export const defragmentItemContent = y => { - const os = y.os - if (os.length < 2) { - return - } - let deletes = [] - let node = os.findSmallestNode() - let next = node.next() - while (next !== null) { - let a = node.val - let b = next.val - if ( - (a instanceof ItemJSON || a instanceof ItemString) && - a.constructor === b.constructor && - a._deleted === b._deleted && - a._right === b && - (ID.createID(a._id.user, a._id.clock + a._length)).equals(b._id) - ) { - a._right = b._right - if (a instanceof ItemJSON) { - a._content = a._content.concat(b._content) - } else if (a instanceof ItemString) { - a._content += b._content - } - // delete b later - deletes.push(b._id) - // do not iterate node! - // !(node = next) - } else { - // not able to merge node, get next node - node = next - } - // update next - next = next.next() - } - for (let i = deletes.length - 1; i >= 0; i--) { - os.delete(deletes[i]) - } -} diff --git a/src/utils/integrateRemoteStructs.js b/src/utils/integrateRemoteStructs.js index 4acfdde5..a066a98e 100644 --- a/src/utils/integrateRemoteStructs.js +++ b/src/utils/integrateRemoteStructs.js @@ -4,9 +4,9 @@ import { getStruct } from '../utils/structReferences.js' import * as decoding from 'lib0/decoding.js' -import { GC } from '../structs/GC.js' +import { GC } from '../structs/GC.js/index.js.js' import { Y } from '../utils/Y.js' // eslint-disable-line -import { Item } from '../structs/Item.js' // eslint-disable-line +import { Item } from '../structs/AbstractItem.js/index.js' // eslint-disable-line class MissingEntry { constructor (decoder, missing, struct) { diff --git a/src/utils/isParentOf.js b/src/utils/isParentOf.js index d4833209..26fb8431 100644 --- a/src/utils/isParentOf.js +++ b/src/utils/isParentOf.js @@ -3,7 +3,7 @@ */ import { Y } from '../utils/Y.js' // eslint-disable-line -import { Type } from '../structs/Type.js' // eslint-disable-line +import { Type } from '../types/AbstractType.js/index.js.js.js.js' // eslint-disable-line /** * Check if `parent` is a parent of `child`. diff --git a/src/utils/relativePosition.js b/src/utils/relativePosition.js index 9cf540be..507de6a9 100644 --- a/src/utils/relativePosition.js +++ b/src/utils/relativePosition.js @@ -3,7 +3,7 @@ */ import * as ID from './ID.js' -import { GC } from '../structs/GC.js' +import { GC } from '../structs/GC.js/index.js.js' // TODO: Implement function to describe ranges diff --git a/src/utils/snapshot.js b/src/utils/snapshot.js index ce121c7f..e7c7e6ac 100644 --- a/src/utils/snapshot.js +++ b/src/utils/snapshot.js @@ -1,7 +1,22 @@ +import { DeleteStore } from './DeleteSet' + +export class HistorySnapshot { + /** + * @param {DeleteStore} ds delete store + * @param {Map} sm state map + * @param {Map} userMap + */ + constructor (ds, sm, userMap) { + this.ds = new DeleteStore() + this.sm = sm + this.userMap = userMap + } +} /** - * * @param {Item} item - * @param {import("../protocols/history").HistorySnapshot} [snapshot] + * @param {HistorySnapshot} [snapshot] */ -export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (snapshot.sm.has(item._id.user) && snapshot.sm.get(item._id.user) > item._id.clock && !snapshot.ds.isDeleted(item._id)) +export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : ( + snapshot.sm.has(item._id.user) && (snapshot.sm.get(item._id.user) || 0) > item._id.clock && !snapshot.ds.isDeleted(item._id) +) diff --git a/src/utils/structEncoding.js b/src/utils/structEncoding.js index 8fb527dd..4ff92f5e 100644 --- a/src/utils/structEncoding.js +++ b/src/utils/structEncoding.js @@ -1,5 +1,30 @@ +import * as encoding from 'lib0/encoding.js' +import * as decoding from 'lib0/decoding.js' +import { getStructReference } from './structReferences.js' +import { ID, createID, writeID, writeNullID } from './ID.js' +import * as binary from 'lib0/binary.js' export const writeStructToTransaction = (transaction, struct) => { transaction.encodedStructsLen++ struct._toBinary(transaction.encodedStructs) } + +const structRefs = [ + ItemBinaryRef +] + +/** + * Read the next Item in a Decoder and fill this Item with the read data. + * + * This is called when data is received from a remote peer. + * + * @param {Y} y The Yjs instance that this Item belongs to. + * @param {decoding.Decoder} decoder The decoder object to read data from. + * @return {AbstractRef} + * + * @private + */ +export const read = decoder => { + const info = decoding.readUint8(decoder) + return new structRefs[binary.BITS5 & info](decoder, info) +} diff --git a/src/utils/structManipulation.js b/src/utils/structManipulation.js deleted file mode 100644 index 2ab26e36..00000000 --- a/src/utils/structManipulation.js +++ /dev/null @@ -1,35 +0,0 @@ - -import * as ID from '../utils/ID.js' - -/** - * @private - * Delete all items in an ID-range. - * Does not create delete operations! - * TODO: implement getItemCleanStartNode for better performance (only one lookup). - */ -export const deleteItemRange = (y, user, clock, range, gcChildren) => { - let item = y.os.getItemCleanStart(ID.createID(user, clock)) - if (item !== null) { - if (!item._deleted) { - item._splitAt(y, range) - item._delete(y, false, true) - } - let itemLen = item._length - range -= itemLen - clock += itemLen - if (range > 0) { - let node = y.os.findNode(ID.createID(user, clock)) - while (node !== null && node.val !== null && range > 0 && node.val._id.equals(ID.createID(user, clock))) { - const nodeVal = node.val - if (!nodeVal._deleted) { - nodeVal._splitAt(y, range) - nodeVal._delete(y, false, gcChildren) - } - const nodeLen = nodeVal._length - range -= nodeLen - clock += nodeLen - node = node.next() - } - } - } -} diff --git a/src/utils/structReferences.js b/src/utils/structReferences.js deleted file mode 100644 index 38bd6ac0..00000000 --- a/src/utils/structReferences.js +++ /dev/null @@ -1,31 +0,0 @@ - -/** - * @module utils - */ - -const structs = new Map() -const references = new Map() - -/** - * Register a new Yjs types. The same type must be defined with the same - * reference on all clients! - * - * @param {Number} reference - * @param {Function} structConstructor - * - * @public - */ -export const registerStruct = (reference, structConstructor) => { - structs.set(reference, structConstructor) - references.set(structConstructor, reference) -} - -/** - * @private - */ -export const getStruct = structs.get.bind(structs) // reference => structs.get(reference) - -/** - * @private - */ -export const getStructReference = references.get.bind(references) // typeConstructor => references.get(typeConstructor) diff --git a/tests/index.js b/tests/index.js index bb332e3f..671e6a79 100644 --- a/tests/index.js +++ b/tests/index.js @@ -7,8 +7,9 @@ import * as array from './y-array.tests.js' import * as map from './y-map.tests.js' import * as text from './y-text.tests.js' import * as xml from './y-xml.tests.js' +import * as perf from './perf.js' if (isBrowser) { log.createVConsole(document.body) } -runTests({ deleteStore, map, array, text, xml }) +runTests({ deleteStore, map, array, text, xml, perf }) diff --git a/tests/perf.js b/tests/perf.js new file mode 100644 index 00000000..7f9571f6 --- /dev/null +++ b/tests/perf.js @@ -0,0 +1,99 @@ +import * as t from 'lib0/testing.js' + +class Item { + constructor (c) { + this.c = c + } +} + +const objectsToCreate = 10000000 + +export const testItemHoldsAll = tc => { + const items = [] + for (let i = 0; i < objectsToCreate; i++) { + switch (i % 3) { + case 0: + items.push(new Item(i)) + break + case 1: + items.push(new Item(i + '')) + break + case 2: + items.push(new Item({ x: i })) + break + default: + throw new Error() + } + } + const call = [] + items.forEach(item => { + switch (item.c.constructor) { + case Number: + call.push(item.c + '') + break + case String: + call.push(item.c) + break + case Object: + call.push(item.c.x + '') + break + default: + throw new Error() + } + }) +} + +class CItem { } + +class CItemNumber { + constructor (i) { + this.c = i + } + toString () { + return this.c + '' + } +} + +class CItemString { + constructor (s) { + this.c = s + } + toString () { + return this.c + } +} + +class CItemObject { + constructor (o) { + this.c = o + } + toString () { + return this.c.x + } +} + +/* + +export const testDifferentItems = tc => { + const items = [] + for (let i = 0; i < objectsToCreate; i++) { + switch (i % 3) { + case 0: + items.push(new CItemNumber(i)) + break + case 1: + items.push(new CItemString(i + '')) + break + case 2: + items.push(new CItemObject({ x: i })) + break + default: + throw new Error() + } + } + const call = [] + items.forEach(item => { + call.push(item.toString()) + }) +} +*/ \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0fa098ba..dfedd65e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ @@ -54,7 +54,9 @@ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - "maxNodeModuleJsDepth": 5 + "maxNodeModuleJsDepth": 5, + "typeRoots": ["./src/utils/typedefs.js"], + // "types": ["./src/utils/typedefs.js"] }, "files": ["./src/index.js"] }