diff --git a/package.json b/package.json index 5419c0e8..860c6316 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "module": "./dist/yjs.mjs'", "sideEffects": false, "scripts": { - "test": "npm run dist && node ./dist/tests.js --repitition-time 50 --production", + "test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production", "test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000", "dist": "rm -rf dist examples/build && rollup -c", "watch": "rollup -wc", diff --git a/rollup.config.js b/rollup.config.js index f80cb31f..d9af5846 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -64,7 +64,7 @@ export default [{ }, { name: 'Y', file: 'dist/yjs.mjs', - format: 'esm', + format: 'es', sourcemap: true }], external: id => /^lib0\//.test(id) diff --git a/src/index.js b/src/index.js index d8d70b32..91f17d2d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ export { Y } from './utils/Y.js' -export { UndoManager } from './utils/UndoManager.js' +// export { UndoManager } from './utils/UndoManager.js' export { Transaction } from './utils/Transaction.js' export { ItemJSON } from './structs/ItemJSON.js' export { ItemString } from './structs/ItemString.js' @@ -16,8 +16,6 @@ export { YXmlText as XmlText } from './types/YXmlText.js' export { YXmlHook as XmlHook } from './types/YXmlHook.js' export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.js' -export { getRelativePosition, fromRelativePosition, equal as equalRelativePosition } from './utils/relativePosition.js' - +export { createRelativePosition, createRelativePositionByOffset, createAbsolutePosition, compareRelativePositions, writeRelativePosition, readRelativePosition, AbsolutePosition, RelativePosition } from './utils/relativePosition.js' export { ID, createID } from './utils/ID.js' -export { integrateRemoteStructs } from './utils/integrateRemoteStructs.js' export { isParentOf } from './utils/isParentOf.js' diff --git a/src/structs/AbstractItem.js b/src/structs/AbstractItem.js index 64b130af..d9f1df89 100644 --- a/src/structs/AbstractItem.js +++ b/src/structs/AbstractItem.js @@ -2,7 +2,7 @@ * @module structs */ -import { readID, createID, writeID, writeNullID, ID } from '../utils/ID.js' // eslint-disable-line +import { readID, createID, writeID, ID } from '../utils/ID.js' // eslint-disable-line import { GC } from './GC.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' @@ -16,6 +16,7 @@ import * as binary from 'lib0/binary.js' import { AbstractRef, AbstractStruct } from './AbstractStruct.js' // eslint-disable-line import * as error from 'lib0/error.js' import { replaceStruct, addStruct } from '../utils/StructStore.js' +import { addToDeleteSet } from '../utils/DeleteSet.js' /** * Split leftItem into two items @@ -51,13 +52,7 @@ export const splitItem = (transaction, leftItem, diff) => { foundOrigins.add(o) o = o.right } - const right = leftItem.splitAt(transaction, diff) - if (transaction.added.has(leftItem)) { - transaction.added.add(right) - } else if (transaction.deleted.has(leftItem)) { - transaction.deleted.add(right) - } - return rightItem + return leftItem.splitAt(transaction, diff) } /** @@ -230,7 +225,6 @@ export class AbstractItem extends AbstractStruct { if (parent !== null) { maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub) } - transaction.added.add(this) // @ts-ignore if (parent._item.deleted || (left !== null && parentSub !== null)) { // delete if parent is deleted or if this is not the current attribute value of parent @@ -341,15 +335,11 @@ export class AbstractItem extends AbstractStruct { /** * Computes the last content address of this Item. - * + * TODO: do still need this? * @private */ get lastId () { - /** - * @type {any} - */ - const id = this.id - return createID(id.user, id.clock + this.length - 1) + return createID(this.id.client, this.id.clock + this.length - 1) } /** @@ -406,8 +396,8 @@ export class AbstractItem extends AbstractStruct { parent._length -= this.length } this.deleted = true + addToDeleteSet(transaction.deleteSet, this.id, this.length) maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub) - transaction.deleted.add(this) } } @@ -437,18 +427,26 @@ export class AbstractItem extends AbstractStruct { * This is called when this Item is sent to a remote peer. * * @param {encoding.Encoder} encoder The encoder to write data to. + * @param {number} offset * @param {number} encodingRef * @private */ - write (encoder, encodingRef) { + write (encoder, offset, 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 (offset === 0) { + writeID(encoder, this.id) + if (this.origin !== null) { + writeID(encoder, this.origin.lastId) + } + } else { + writeID(encoder, createID(this.id.client, this.id.clock + offset)) + if (this.origin !== null) { + writeID(encoder, createID(this.id.client, this.id.clock + offset - 1)) + } } if (this.rightOrigin !== null) { writeID(encoder, this.rightOrigin.id) @@ -470,10 +468,10 @@ export class AbstractItem extends AbstractStruct { if (ykey === null) { throw error.unexpectedCase() } - writeNullID(encoder) + encoding.writeVarUint(encoder, 1) // write parentYKey encoding.writeVarString(encoder, ykey) } else { - // neither origin nor right is defined + encoding.writeVarUint(encoder, 0) // write parent id // @ts-ignore _item is defined because parent is integrated writeID(encoder, parent._item.id) } @@ -487,19 +485,11 @@ export class AbstractItem extends AbstractStruct { export class AbstractItemRef extends AbstractRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super() - const id = readID(decoder) - if (id === null) { - throw error.unexpectedCase() - } - /** - * The uniqe identifier of this type. - * @type {ID} - */ - this.id = id + constructor (decoder, id, info) { + super(id) /** * The item that was originally to the left of this item. * @type {ID | null} @@ -511,18 +501,19 @@ export class AbstractItemRef extends AbstractRef { */ 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 + const hasParentYKey = decoding.readVarUint(decoder) === 1 /** * If parent = null and neither left nor right are defined, then we know that `parent` is child of `y` * and we read the next string as parentYKey. * It indicates how we store/retrieve parent from `y.share` * @type {string|null} */ - this.parentYKey = canCopyParentInfo && this.parent === null ? decoding.readVarString(decoder) : null + this.parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null + /** + * The parent type. + * @type {ID | null} + */ + this.parent = canCopyParentInfo && !hasParentYKey ? 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 @@ -531,11 +522,22 @@ export class AbstractItemRef extends AbstractRef { * @type {String | null} */ this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null + const missing = this._missing + if (this.left !== null) { + missing.push(this.left) + } + if (this.right !== null) { + missing.push(this.right) + } + if (this.parent !== null) { + missing.push(this.parent) + } } /** + * @param {Transaction} transaction * @return {Array} */ - getMissing () { + getMissing (transaction) { return [ createID(this.id.client, this.id.clock - 1), this.left, diff --git a/src/structs/AbstractStruct.js b/src/structs/AbstractStruct.js index c7ab8397..7669f681 100644 --- a/src/structs/AbstractStruct.js +++ b/src/structs/AbstractStruct.js @@ -23,22 +23,50 @@ export class AbstractStruct { get length () { throw error.methodUnimplemented() } + /** + * @type {boolean} + */ + get deleted () { + throw error.methodUnimplemented() + } /** * @param {encoding.Encoder} encoder The encoder to write data to. + * @param {number} offset * @param {number} encodingRef * @private */ - write (encoder, encodingRef) { + write (encoder, offset, encodingRef) { + throw error.methodUnimplemented() + } + /** + * @param {Transaction} transaction + */ + integrate (transaction) { throw error.methodUnimplemented() } } export class AbstractRef { /** + * @param {ID} id + */ + constructor (id) { + /** + * @type {Array} + */ + this._missing = [] + /** + * The uniqe identifier of this type. + * @type {ID} + */ + this.id = id + } + /** + * @param {Transaction} transaction * @return {Array} */ - getMissing () { - return [] + getMissing (transaction) { + return this._missing } /** * @param {Transaction} transaction diff --git a/src/structs/GC.js b/src/structs/GC.js index c5a1ef42..048d0f21 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -28,10 +28,15 @@ export class GC extends AbstractStruct { /** * @param {encoding.Encoder} encoder + * @param {number} offset */ - write (encoder) { + write (encoder, offset) { encoding.writeUint8(encoder, structGCRefNumber) - writeID(encoder, this.id) + if (offset === 0) { + writeID(encoder, this.id) + } else { + writeID(encoder, createID(this.id.client, this.id.clock + offset)) + } encoding.writeVarUint(encoder, this.length) } } @@ -39,14 +44,11 @@ export class GC extends AbstractStruct { export class GCRef extends AbstractRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super() - const id = readID(decoder) - if (id === null) { - throw new Error('expected id') - } + constructor (decoder, id, info) { + super(id) /** * @type {ID} */ diff --git a/src/structs/ItemBinary.js b/src/structs/ItemBinary.js index 1fd22ec5..908cd300 100644 --- a/src/structs/ItemBinary.js +++ b/src/structs/ItemBinary.js @@ -43,9 +43,10 @@ export class ItemBinary extends AbstractItem { } /** * @param {encoding.Encoder} encoder + * @param {number} offset */ - write (encoder) { - super.write(encoder, structBinaryRefNumber) + write (encoder, offset) { + super.write(encoder, offset, structBinaryRefNumber) encoding.writePayload(encoder, this.content) } } @@ -53,10 +54,11 @@ export class ItemBinary extends AbstractItem { export class ItemBinaryRef extends AbstractItemRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super(decoder, info) + constructor (decoder, id, info) { + super(decoder, id, info) /** * @type {ArrayBuffer} */ diff --git a/src/structs/ItemDeleted.js b/src/structs/ItemDeleted.js index ca50eac6..fb1a83ce 100644 --- a/src/structs/ItemDeleted.js +++ b/src/structs/ItemDeleted.js @@ -8,9 +8,9 @@ import { AbstractItem, 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 { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line export const structDeletedRefNumber = 2 @@ -39,20 +39,22 @@ export class ItemDeleted extends AbstractItem { } /** * @param {encoding.Encoder} encoder + * @param {number} offset */ - write (encoder) { - super.write(encoder, structDeletedRefNumber) - encoding.writeVarUint(encoder, this.length) + write (encoder, offset) { + super.write(encoder, offset, structDeletedRefNumber) + encoding.writeVarUint(encoder, this.length - offset) } } export class ItemDeletedRef extends AbstractItemRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super(decoder, info) + constructor (decoder, id, info) { + super(decoder, id, info) /** * @type {number} */ diff --git a/src/structs/ItemEmbed.js b/src/structs/ItemEmbed.js index 533e0f45..390453a2 100644 --- a/src/structs/ItemEmbed.js +++ b/src/structs/ItemEmbed.js @@ -39,9 +39,10 @@ export class ItemEmbed extends AbstractItem { } /** * @param {encoding.Encoder} encoder + * @param {number} offset */ - write (encoder) { - super.write(encoder, structEmbedRefNumber) + write (encoder, offset) { + super.write(encoder, offset, structEmbedRefNumber) encoding.writeVarString(encoder, JSON.stringify(this.embed)) } } @@ -49,10 +50,11 @@ export class ItemEmbed extends AbstractItem { export class ItemEmbedRef extends AbstractItemRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super(decoder, info) + constructor (decoder, id, info) { + super(decoder, id, info) /** * @type {ArrayBuffer} */ diff --git a/src/structs/ItemFormat.js b/src/structs/ItemFormat.js index 67322cc0..c47a5b45 100644 --- a/src/structs/ItemFormat.js +++ b/src/structs/ItemFormat.js @@ -44,9 +44,10 @@ export class ItemFormat extends AbstractItem { } /** * @param {encoding.Encoder} encoder + * @param {number} offset */ - write (encoder) { - super.write(encoder, structFormatRefNumber) + write (encoder, offset) { + super.write(encoder, offset, structFormatRefNumber) encoding.writeVarString(encoder, this.key) encoding.writeVarString(encoder, JSON.stringify(this.value)) } @@ -55,10 +56,11 @@ export class ItemFormat extends AbstractItem { export class ItemFormatRef extends AbstractItemRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super(decoder, info) + constructor (decoder, id, info) { + super(decoder, id, info) /** * @type {string} */ diff --git a/src/structs/ItemJSON.js b/src/structs/ItemJSON.js index 543875b3..a362df65 100644 --- a/src/structs/ItemJSON.js +++ b/src/structs/ItemJSON.js @@ -58,9 +58,10 @@ export class ItemJSON extends AbstractItem { } /** * @param {encoding.Encoder} encoder + * @param {number} offset */ - write (encoder) { - super.write(encoder, structJSONRefNumber) + write (encoder, offset) { + super.write(encoder, offset, structJSONRefNumber) const len = this.content.length encoding.writeVarUint(encoder, len) for (let i = 0; i < len; i++) { @@ -73,10 +74,11 @@ export class ItemJSON extends AbstractItem { export class ItemJSONRef extends AbstractItemRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super(decoder, info) + constructor (decoder, id, info) { + super(decoder, id, info) const len = decoding.readVarUint(decoder) const cs = [] for (let i = 0; i < len; i++) { diff --git a/src/structs/ItemString.js b/src/structs/ItemString.js index b29e0693..12716f9f 100644 --- a/src/structs/ItemString.js +++ b/src/structs/ItemString.js @@ -62,9 +62,10 @@ export class ItemString extends AbstractItem { } /** * @param {encoding.Encoder} encoder + * @param {number} offset */ - write (encoder) { - super.write(encoder, structStringRefNumber) + write (encoder, offset) { + super.write(encoder, offset, structStringRefNumber) encoding.writeVarString(encoder, this.string) } } @@ -72,10 +73,11 @@ export class ItemString extends AbstractItem { export class ItemStringRef extends AbstractItemRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super(decoder, info) + constructor (decoder, id, info) { + super(decoder, id, info) /** * @type {string} */ diff --git a/src/structs/ItemType.js b/src/structs/ItemType.js index 40a8316b..e053fc90 100644 --- a/src/structs/ItemType.js +++ b/src/structs/ItemType.js @@ -73,37 +73,41 @@ export class ItemType extends AbstractItem { return new ItemType(id, left, right, parent, parentSub, this.type._copy()) } /** - * @param {encoding.Encoder} encoder + * @param {Transaction} transaction */ - write (encoder) { - super.write(encoder, structTypeRefNumber) + integrate (transaction) { + this.type._integrate(transaction, this) + } + /** + * @param {encoding.Encoder} encoder + * @param {number} offset + */ + write (encoder, offset) { + super.write(encoder, offset, 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) { + delete (transaction) { const y = transaction.y - super.delete(transaction, createDelete, gcChildren) + super.delete(transaction) transaction.changed.delete(this.type) + transaction.changedParentTypes.delete(this.type) // delete map types for (let value of this.type._map.values()) { if (!value.deleted) { - value.delete(transaction, false, gcChildren) + value.delete(transaction) } } // delete array types let t = this.type._start while (t !== null) { if (!t.deleted) { - t.delete(transaction, false, gcChildren) + t.delete(transaction) } t = t.right } @@ -133,13 +137,14 @@ export class ItemType extends AbstractItem { } } -export class ItemBinaryRef extends AbstractItemRef { +export class ItemTypeRef extends AbstractItemRef { /** * @param {decoding.Decoder} decoder + * @param {ID} id * @param {number} info */ - constructor (decoder, info) { - super(decoder, info) + constructor (decoder, id, info) { + super(decoder, id, info) const typeRef = decoding.readVarUint(decoder) /** * @type {AbstractType} diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 9fde2e1f..d94024a3 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -14,7 +14,8 @@ import { isVisible, Snapshot } from '../utils/Snapshot.js' // eslint-disable-lin import { ItemJSON } from '../structs/ItemJSON.js' import { ItemBinary } from '../structs/ItemBinary.js' import { ID, createID } from '../utils/ID.js' // eslint-disable-line -import { getItemCleanStart } from '../utils/StructStore.js' +import { getItemCleanStart, getItemCleanEnd } from '../utils/StructStore.js' +import * as iterator from 'lib0/iterator.js' /** * Abstract Yjs Type class @@ -203,6 +204,23 @@ export const typeArrayForEach = (type, f) => { } } +/** + * @template C,R + * @param {AbstractType} type + * @param {function(C,number,AbstractType):R} f + * @return {Array} + */ +export const typeArrayMap = (type, f) => { + /** + * @type {Array} + */ + const result = [] + typeArrayForEach(type, (c, i) => { + result.push(f(c, i, type)) + }) + return result +} + /** * @param {AbstractType} type */ @@ -351,6 +369,37 @@ export const typeArrayInsertGenerics = (transaction, parent, index, content) => throw new Error('Index exceeds array range') } +/** + * @param {Transaction} transaction + * @param {AbstractType} parent + * @param {number} index + * @param {number} length + */ +export const typeArrayDelete = (transaction, parent, index, length) => { + let n = parent._start + for (; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index <= n.length) { + if (index < n.length) { + n = getItemCleanStart(transaction.y.store, transaction, createID(n.id.client, n.id.clock + index)) + } + break + } + index -= n.length + } + } + while (length > 0 && n !== null) { + if (!n.deleted) { + if (length < n.length) { + getItemCleanEnd(transaction.y.store, transaction, createID(n.id.client, n.id.clock + length)) + } + n.delete(transaction) + length -= n.length + } + n = n.right + } +} + /** * @param {Transaction} transaction * @param {AbstractType} parent @@ -400,6 +449,23 @@ export const typeMapGet = (parent, key) => { return val !== undefined && !val.deleted ? val.getContent()[0] : undefined } +/** + * @param {AbstractType} parent + * @return {Object|number|Array|string|ArrayBuffer|AbstractType|undefined>} + */ +export const typeMapGetAll = (parent) => { + /** + * @type {Object} + */ + let res = {} + for (const [key, value] of parent._map) { + if (!value.deleted) { + res[key] = value.getContent()[0] + } + } + return res +} + /** * @param {AbstractType} parent * @param {string} key @@ -423,3 +489,9 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => { } return v !== null && isVisible(v, snapshot) ? v.getContent()[0] : undefined } + +/** + * @param {Map} map + * @return {Iterator<[string,AbstractItem]>} + */ +export const createMapIterator = map => iterator.iteratorFilter(map.entries(), entry => !entry[1].deleted) diff --git a/src/types/YArray.js b/src/types/YArray.js index a011efe4..21c43814 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -4,7 +4,7 @@ import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line import { ItemType } from '../structs/ItemType.js' // eslint-disable-line -import { AbstractType, typeArrayGet, typeArrayToArray, typeArrayForEach, typeArrayCreateIterator, typeArrayInsertGenerics, typeArrayDelete } from './AbstractType.js' +import { AbstractType, typeArrayGet, typeArrayToArray, typeArrayForEach, typeArrayCreateIterator, typeArrayInsertGenerics, typeArrayDelete, typeArrayMap } from './AbstractType.js' import { YEvent } from '../utils/YEvent.js' import { Transaction } from '../utils/Transaction.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' // eslint-disable-line @@ -140,20 +140,14 @@ export class YArray extends AbstractType { * Returns an Array with the result of calling a provided function on every * element of this YArray. * - * @template M + * @template T,M * @param {function(T,number,YArray):M} f Function that produces an element of the new Array * @return {Array} A new array with each element being the result of the * callback function */ map (f) { - /** - * @type {Array} - */ - const result = [] - this.forEach((c, i) => { - result.push(f(c, i, this)) - }) - return result + // @ts-ignore + return typeArrayMap(this, f) } /** diff --git a/src/types/YMap.js b/src/types/YMap.js index 9df8b638..6ad378f3 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -2,36 +2,13 @@ * @module types */ -import { AbstractType, typeMapDelete, typeMapSet, typeMapGet, typeMapHas } from './AbstractType.js' +import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line +import { AbstractType, typeMapDelete, typeMapSet, typeMapGet, typeMapHas, createMapIterator } from './AbstractType.js' import { ItemType } from '../structs/ItemType.js' // eslint-disable-line import { YEvent } from '../utils/YEvent.js' import * as decoding from 'lib0/decoding.js' // eslint-disable-line import { Transaction } from '../utils/Transaction.js' // eslint-disable-line - -class YMapIterator { - /** - * @param {Array} vals - */ - constructor (vals) { - this.vals = vals - this.i = 0 - } - [Symbol.iterator] () { - return this - } - next () { - let value - let done = true - if (this.i < this.vals.length) { - value = this.vals[this.i] - done = false - } - return { - value, - done - } - } -} +import * as iterator from 'lib0/iterator.js' /** * Event that describes the changes on a YMap. @@ -109,26 +86,18 @@ export class YMap extends AbstractType { /** * Returns the keys for each element in the YMap Type. * - * @return {YMapIterator} + * @return {Iterator} */ keys () { - const keys = [] - for (let [key, value] of this._map) { - if (value.deleted) { - keys.push(key) - } - } - return new YMapIterator(keys) + return iterator.iteratorMap(createMapIterator(this._map), v => v[0]) } - + /** + * Returns the value for each element in the YMap Type. + * + * @return {Iterator|Array>} + */ entries () { - const entries = [] - for (let [key, value] of this._map) { - if (value.deleted) { - entries.push([key, value.getContent()[0]]) - } - } - return new YMapIterator(entries) + return iterator.iteratorMap(createMapIterator(this._map), v => v[1].getContent()[0]) } [Symbol.iterator] () { diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js index 65e3adf5..d98c3f4f 100644 --- a/src/types/YXmlElement.js +++ b/src/types/YXmlElement.js @@ -7,8 +7,12 @@ import { YMap } from './YMap.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 { YArray } from './YArray.js' import { YXmlEvent } from './YXmlEvent.js' +import { ItemType } from '../structs/ItemType.js' // eslint-disable-line +import { YXmlText } from './YXmlText.js' // eslint-disable-line +import { YXmlHook } from './YXmlHook.js' // eslint-disable-line +import { AbstractType, typeArrayMap, typeArrayForEach, typeMapGet, typeMapGetAll } from './AbstractType.js' +import { Snapshot } from '../utils/Snapshot.js' // eslint-disable-line /** * Define the elements to which a set of CSS queries apply. @@ -42,16 +46,16 @@ import { YXmlEvent } from './YXmlEvent.js' export class YXmlTreeWalker { /** * @param {YXmlFragment | YXmlElement} root - * @param {function} f + * @param {function(AbstractType):boolean} f */ constructor (root, f) { this._filter = f || (() => true) this._root = root /** - * @type {YXmlFragment | YXmlElement} + * @type {ItemType | null} */ - this._currentNode = root - this._firstCall = true + // @ts-ignore + this._currentNode = root._start } [Symbol.iterator] () { return this @@ -59,45 +63,40 @@ export class YXmlTreeWalker { /** * Get the next node. * - * @return {YXmlElement} The next node. + * @return {IteratorResult} The next node. * * @public */ next () { let n = this._currentNode - if (this._firstCall) { - this._firstCall = false - if (!n._deleted && this._filter(n)) { - return { value: n, done: false } - } + if (n === null) { + // @ts-ignore return undefined if done=true (the expected result) + return { value: undefined, done: true } } + const nextValue = n do { - if (!n._deleted && (n.constructor === YXmlElement || n.constructor === YXmlFragment) && n._start !== null) { + if (!n.deleted && (n.constructor === YXmlElement || n.constructor === YXmlFragment) && n.type._start !== null) { // walk down in the tree - n = n._start + // @ts-ignore + n = n.type._start } else { // walk right or up in the tree - while (n !== this._root) { - if (n._right !== null) { - n = n._right + while (n !== null) { + if (n.right !== null) { + // @ts-ignore + n = n.right break + } else if (n.parent === this._root) { + n = null + } else { + n = n.parent._item } - n = n._parent - } - if (n === this._root) { - n = null } } - if (n === this._root) { - break - } - } while (n !== null && (n._deleted || !this._filter(n))) + } while (n !== null && (n.deleted || !this._filter(n.type))) this._currentNode = n - if (n === null) { - return { done: true } - } else { - return { value: n, done: false } - } + // @ts-ignore + return { value: nextValue.type, done: false } } } @@ -109,7 +108,7 @@ export class YXmlTreeWalker { * * @public */ -export class YXmlFragment extends YArray { +export class YXmlFragment extends AbstractType { /** * Create a subtree of childNodes. * @@ -120,7 +119,7 @@ export class YXmlFragment extends YArray { * nop(node) * } * - * @param {Function} filter Function that is called on each child element and + * @param {function(AbstractType):boolean} filter Function that is called on each child element and * returns a Boolean indicating whether the child * is to be included in the subtree. * @return {YXmlTreeWalker} A subtree and a position within it. @@ -142,12 +141,13 @@ export class YXmlFragment extends YArray { * - attribute * * @param {CSS_Selector} query The query on the children. - * @return {YXmlElement} The first element that matches the query or null. + * @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null. * * @public */ querySelector (query) { query = query.toUpperCase() + // @ts-ignore const iterator = new YXmlTreeWalker(this, element => element.nodeName === query) const next = iterator.next() if (next.done) { @@ -164,12 +164,13 @@ export class YXmlFragment extends YArray { * TODO: Does not yet support all queries. Currently only query by tagName. * * @param {CSS_Selector} query The query on the children - * @return {Array} The elements that match this query. + * @return {Array} The elements that match this query. * * @public */ querySelectorAll (query) { query = query.toUpperCase() + // @ts-ignore return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query)) } @@ -194,7 +195,7 @@ export class YXmlFragment extends YArray { * @return {string} The string representation of all children. */ toDomString () { - return this.map(xml => xml.toDomString()).join('') + return typeArrayMap(this, xml => xml.toDomString()).join('') } /** @@ -205,10 +206,10 @@ export class YXmlFragment extends YArray { * nodejs) * @param {Object} [hooks={}] Optional property to customize how hooks * are presented in the DOM - * @param {DomBinding} [binding] You should not set this property. This is + * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. - * @return {DocumentFragment} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ @@ -217,20 +218,11 @@ export class YXmlFragment extends YArray { if (binding !== undefined) { binding._createAssociation(fragment, this) } - this.forEach(xmlType => { + typeArrayForEach(this, xmlType => { fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null) }) return fragment } - /** - * Transform this YXml Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - _logString () { - return logItemHelper('YXml', this) - } } /** @@ -249,27 +241,11 @@ export class YXmlElement extends YXmlFragment { /** * Creates an Item with the same effect as this Item (without position effect) * + * @return {YXmlElement} * @private */ _copy () { - let struct = super._copy() - struct.nodeName = this.nodeName - return struct - } - - /** - * 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. - * - * @private - * @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) { - const missing = super._fromBinary(y, decoder) - this.nodeName = decoding.readVarString(decoder) - return missing + return new YXmlElement(this.nodeName) } /** @@ -281,31 +257,10 @@ export class YXmlElement extends YXmlFragment { * @private * @param {encoding.Encoder} encoder The encoder to write data to. */ - _toBinary (encoder) { - super._toBinary(encoder) + _write (encoder) { encoding.writeVarString(encoder, this.nodeName) } - /** - * 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. - * - * * Checks for nodeName - * * Sets domFilter - * - * @private - * @param {Transaction} transaction The Yjs instance - */ - _integrate (transaction) { - if (this.nodeName === null) { - throw new Error('nodeName must be defined!') - } - super._integrate(transaction) - } - toString () { return this.toDomString() } @@ -365,37 +320,25 @@ export class YXmlElement extends YXmlFragment { * * @param {String} attributeName The attribute name that identifies the * queried value. - * @param {HistorySnapshot} [snapshot] * @return {String} The queried attribute value. * * @public */ - getAttribute (attributeName, snapshot) { - return YMap.prototype.get.call(this, attributeName, snapshot) + getAttribute (attributeName) { + // @ts-ignore + return typeMapGet(this, attributeName) } /** * Returns all attribute name/value pairs in a JSON Object. * - * @param {HistorySnapshot} [snapshot] + * @param {Snapshot} [snapshot] * @return {Object} A JSON Object that describes the attributes. * * @public */ getAttributes (snapshot) { - const obj = {} - if (snapshot === undefined) { - for (let [key, value] of this._map) { - if (!value._deleted) { - obj[key] = value._content[0] - } - } - } else { - YMap.prototype.keys.call(this, snapshot).forEach(key => { - obj[key] = YMap.prototype.get.call(this, key, snapshot) - }) - } - return obj + return typeMapGetAll(this) } // TODO: outsource the binding property. /** @@ -406,10 +349,10 @@ export class YXmlElement extends YXmlFragment { * nodejs) * @param {Object} [hooks={}] Optional property to customize how hooks * are presented in the DOM - * @param {DomBinding} [binding] You should not set this property. This is + * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. - * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ @@ -419,7 +362,7 @@ export class YXmlElement extends YXmlFragment { for (let key in attrs) { dom.setAttribute(key, attrs[key]) } - this.forEach(yxml => { + typeArrayForEach(this, yxml => { dom.appendChild(yxml.toDom(_document, hooks, binding)) }) if (binding !== undefined) { @@ -429,5 +372,13 @@ export class YXmlElement extends YXmlFragment { } } +/** + * @param {decoding.Decoder} decoder + * @return {YXmlElement} + */ export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder)) -export const readYXmlFragment = decoder => new YXmlFragment() \ No newline at end of file +/** + * @param {decoding.Decoder} decoder + * @return {YXmlFragment} + */ +export const readYXmlFragment = decoder => new YXmlFragment() diff --git a/src/types/YXmlEvent.js b/src/types/YXmlEvent.js index d73ed4d4..c5ffed98 100644 --- a/src/types/YXmlEvent.js +++ b/src/types/YXmlEvent.js @@ -17,11 +17,10 @@ export class YXmlEvent extends YEvent { * @param {AbstractType} target The target on which the event is created. * @param {Set} subs The set of changed attributes. `null` is included if the * child list changed. - * @param {Boolean} remote Whether this change was created by a remote peer. * @param {Transaction} transaction The transaction instance with wich the * change was created. */ - constructor (target, subs, remote, transaction) { + constructor (target, subs, transaction) { super(target) /** * The transaction instance for the computed change. @@ -38,11 +37,6 @@ export class YXmlEvent extends YEvent { * @type {Set} */ this.attributesChanged = new Set() - /** - * Whether this change was created by a remote peer. - * @type {Boolean} - */ - this.remote = remote subs.forEach((sub) => { if (sub === null) { this.childListChanged = true diff --git a/src/utils/DeleteSet.js b/src/utils/DeleteSet.js index f09d4c85..2cee8ccf 100644 --- a/src/utils/DeleteSet.js +++ b/src/utils/DeleteSet.js @@ -1,6 +1,7 @@ import * as map from 'lib0/map.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' +import * as math from 'lib0/math.js' import { StructStore, getItemRange } from './StructStore.js' // eslint-disable-line import { Transaction } from './Transaction.js' // eslint-disable-line import { ID } from './ID.js' // eslint-disable-line @@ -39,13 +40,38 @@ export class DeleteSet { } } +/** + * @param {Array} dis + * @param {number} clock + * @return {number|null} + */ +export const findIndexSS = (dis, clock) => { + let left = 0 + let right = dis.length + while (left <= right) { + const midindex = math.floor((left + right) / 2) + const mid = dis[midindex] + const midclock = mid.clock + if (midclock <= clock) { + if (clock < midclock + mid.len) { + return midindex + } + left = midindex + } else { + right = midindex + } + } + return null +} + /** * @param {DeleteSet} ds * @param {ID} id * @return {boolean} */ export const isDeleted = (ds, id) => { - + const dis = ds.clients.get(id.client) + return dis !== undefined && findIndexSS(dis, id.clock) !== null } /** @@ -75,15 +101,12 @@ export const sortAndMergeDeleteSet = ds => { } /** - * @param {Transaction} transaction + * @param {DeleteSet} ds + * @param {ID} id + * @param {number} length */ -export const createDeleteSetFromTransaction = transaction => { - const ds = new DeleteSet() - transaction.deleted.forEach(item => { - map.setIfUndefined(ds.clients, item.id.client, () => []).push(new DeleteItem(item.id.clock, item.length)) - }) - sortAndMergeDeleteSet(ds) - return ds +export const addToDeleteSet = (ds, id, length) => { + map.setIfUndefined(ds.clients, id.client, () => []).push(new DeleteItem(id.clock, length)) } /** diff --git a/src/utils/ID.js b/src/utils/ID.js index e3e37925..6c1ba33e 100644 --- a/src/utils/ID.js +++ b/src/utils/ID.js @@ -4,6 +4,8 @@ import * as decoding from 'lib0/decoding.js' import * as encoding from 'lib0/encoding.js' +import * as error from 'lib0/error.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line export class ID { /** @@ -46,14 +48,19 @@ export class ID { } } +/** + * @param {ID} a + * @param {ID} b + * @return {boolean} + */ +export const compareIDs = (a, b) => a === b || (a !== null && b !== null && a.client === b.client && a.clock === b.clock) + /** * @param {number} client * @param {number} clock */ export const createID = (client, clock) => new ID(client, clock) -const isNullID = 0xFFFFFF - /** * @param {encoding.Encoder} encoder * @param {ID} id @@ -63,21 +70,31 @@ export const writeID = (encoder, id) => { encoding.writeVarUint(encoder, id.clock) } -/** - * @param {encoding.Encoder} encoder - */ -export const writeNullID = (encoder) => - encoding.writeVarUint(encoder, isNullID) - /** * Read ID. * * If first varUint read is 0xFFFFFF a RootID is returned. * * Otherwise an ID is returned * * @param {decoding.Decoder} decoder - * @return {ID | null} + * @return {ID} */ -export const readID = decoder => { - const client = decoding.readVarUint(decoder) - return client === isNullID ? null : createID(client, decoding.readVarUint(decoder)) +export const readID = decoder => + createID(decoding.readVarUint(decoder), decoding.readVarUint(decoder)) + +/** + * The top types are mapped from y.share.get(keyname) => type. + * `type` does not store any information about the `keyname`. + * This function finds the correct `keyname` for `type` and throws otherwise. + * + * @param {AbstractType} type + * @return {string} + */ +export const findRootTypeKey = type => { + // @ts-ignore _y must be defined, otherwise unexpected case + for (let [key, value] of type._y.share) { + if (value === type) { + return key + } + } + throw error.unexpectedCase() } diff --git a/src/utils/Snapshot.js b/src/utils/Snapshot.js index 8419cf07..bebf987d 100644 --- a/src/utils/Snapshot.js +++ b/src/utils/Snapshot.js @@ -18,6 +18,6 @@ export class Snapshot { * @param {AbstractItem} item * @param {Snapshot} [snapshot] */ -export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : ( - snapshot.sm.has(item.id.client) && (snapshot.sm.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item._id) +export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : ( + snapshot.sm.has(item.id.client) && (snapshot.sm.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id) ) diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js index 45ef22e3..771a1045 100644 --- a/src/utils/StructStore.js +++ b/src/utils/StructStore.js @@ -5,6 +5,7 @@ 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' +import * as error from 'lib0/error.js' export class StructStore { constructor () { @@ -68,13 +69,13 @@ export const addStruct = (store, struct) => { } /** - * Expects that id is actually in store. This function throws or is an infinite loop otherwise. - * @param {Array} structs // ordered structs without holes + * Perform a binary search on a sorted array + * @param {Array} structs * @param {number} clock * @return {number} * @private */ -export const findIndex = (structs, clock) => { +export const findIndexSS = (structs, clock) => { let left = 0 let right = structs.length while (left <= right) { @@ -90,7 +91,7 @@ export const findIndex = (structs, clock) => { right = midindex } } - throw new Error('ID does not exist') + throw error.unexpectedCase() } /** @@ -101,13 +102,13 @@ export const findIndex = (structs, clock) => { * @return {AbstractStruct} * @private */ -const find = (store, id) => { +export const find = (store, id) => { /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(id.client) - return structs[findIndex(structs, id.clock)] + return structs[findIndexSS(structs, id.clock)] } /** @@ -135,14 +136,13 @@ export const getItemCleanStart = (store, transaction, id) => { */ // @ts-ignore const structs = store.clients.get(id.client) - const index = findIndex(structs, id.clock) + const index = findIndexSS(structs, id.clock) /** * @type {AbstractItem} */ let struct = structs[index] if (struct.id.clock < id.clock) { - struct.splitAt() - struct = splitStruct(transaction, struct, id.clock - struct.id.clock) + struct = struct.splitAt(transaction, id.clock - struct.id.clock) structs.splice(index, 0, struct) } return struct @@ -163,10 +163,10 @@ export const getItemCleanEnd = (store, transaction, id) => { */ // @ts-ignore const structs = store.clients.get(id.client) - const index = findIndex(structs, id.clock) + const index = findIndexSS(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)) + structs.splice(index, 0, struct.splitAt(transaction, id.clock - struct.id.clock + 1)) } return struct } @@ -188,11 +188,11 @@ export const getItemRange = (store, transaction, client, clock, len) => { */ // @ts-ignore const structs = store.clients.get(client) - let index = findIndex(structs, clock) + let index = findIndexSS(structs, clock) let struct = structs[index] let range = [] if (struct.id.clock < clock) { - struct = splitStruct(transaction, struct, clock - struct.id.clock) + struct = struct.splitAt(transaction, clock - struct.id.clock) structs.splice(index, 0, struct) } while (struct.id.clock + struct.length <= clock + len) { @@ -200,7 +200,7 @@ export const getItemRange = (store, transaction, client, clock, len) => { struct = structs[++index] } if (clock < struct.id.clock + struct.length) { - structs.splice(index, 0, splitStruct(transaction, struct, clock + len - struct.id.clock)) + structs.splice(index, 0, struct.splitAt(transaction, clock + len - struct.id.clock)) range.push(struct) } return range @@ -218,5 +218,12 @@ export const replaceStruct = (store, struct, newStruct) => { */ // @ts-ignore const structs = store.clients.get(struct.id.client) - structs[findIndex(structs, struct.id.clock)] = newStruct + structs[findIndexSS(structs, struct.id.clock)] = newStruct } + +/** + * @param {StructStore} store + * @param {ID} id + * @return {boolean} + */ +export const exists = (store, id) => id.clock < getState(store, id.client) diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 264e60ad..aba2c4e4 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -10,7 +10,7 @@ import { YEvent } from './YEvent.js' // eslint-disable-line import { ItemType } from '../structs/ItemType.js' // eslint-disable-line import { writeStructsFromTransaction } from './structEncoding.js' import { createID } from './ID.js' // eslint-disable-line -import { createDeleteSetFromTransaction, writeDeleteSet } from './DeleteSet.js' +import { writeDeleteSet, DeleteSet, sortAndMergeDeleteSet } from './DeleteSet.js' import { getState } from './StructStore.js' /** @@ -46,15 +46,10 @@ export class Transaction { */ this.y = y /** - * All new items that are added during a transaction. - * @type {Set} + * Describes the set of deleted items by ids + * @type {DeleteSet} */ - this.added = new Set() - /** - * Set of all deleted items - * @type {Set} - */ - this.deleted = new Set() + this.deleteSet = new DeleteSet() /** * If a state was modified, the original value is saved here. * Use `stateUpdates` to compute the original state before the transaction, @@ -87,7 +82,8 @@ export class Transaction { if (this._updateMessage === null) { const encoder = encoding.createEncoder() writeStructsFromTransaction(encoder, this) - writeDeleteSet(encoder, createDeleteSetFromTransaction(this)) + sortAndMergeDeleteSet(this.deleteSet) + writeDeleteSet(encoder, this.deleteSet) this._updateMessage = encoder } return this._updateMessage diff --git a/src/utils/Y.js b/src/utils/Y.js index e5ce347b..96fb1c50 100644 --- a/src/utils/Y.js +++ b/src/utils/Y.js @@ -71,20 +71,20 @@ export class Y extends Observable { console.error(e) } if (initialCall) { - this.emit('beforeObserverCalls', [this, this._transaction, remote]) const transaction = this._transaction this._transaction = null - // emit change events on changed types - transaction.changed.forEach((subs, itemtype) => { - if (!itemtype._item.deleted) { - itemtype.type._callObserver(transaction, subs, remote) - } - }) - transaction.changedParentTypes.forEach((events, type) => { - if (!type._deleted) { + // only call event listeners / observers if anything changed + const transactionChangedContent = transaction.changedParentTypes.size !== 0 + if (transactionChangedContent) { + this.emit('beforeObserverCalls', [this, this._transaction, remote]) + // emit change events on changed types + transaction.changed.forEach((subs, itemtype) => { + itemtype._callObserver(transaction, subs) + }) + transaction.changedParentTypes.forEach((events, type) => { events = events .filter(event => - !event.target._deleted + event.target._item === null || !event.target._item.deleted ) events .forEach(event => { @@ -92,11 +92,15 @@ export class Y extends Observable { }) // we don't have to check for events.length // because there is no way events is empty.. - type.type._deepEventHandler.callEventListeners(transaction, events) - } - }) - // when all changes & events are processed, emit afterTransaction event - this.emit('afterTransaction', [this, transaction, remote]) + type._deepEventHandler.callEventListeners(transaction, events) + }) + // when all changes & events are processed, emit afterTransaction event + this.emit('afterTransaction', [this, transaction, remote]) + // transaction cleanup + // todo: replace deleted items with ItemDeleted + // todo: replace items with deleted parent with ItemGC + // todo: on all affected store.clients props, try to merge + } } } /** @@ -120,6 +124,7 @@ export class Y extends Observable { * } * * @TODO: implement getText, getArray, .. + * @TODO: Decide wether to use define() or get() and then use it consistently * * @param {string} name * @param {Function} TypeConstructor The constructor of the type definition @@ -127,7 +132,7 @@ export class Y extends Observable { */ get (name, TypeConstructor = AbstractType) { // @ts-ignore - const type = map.setTfUndefined(this.share, name, () => new TypeConstructor()) + const type = map.setIfUndefined(this.share, name, () => new TypeConstructor()) const Constr = type.constructor if (Constr !== TypeConstructor) { if (Constr === AbstractType) { diff --git a/src/utils/integrateRemoteStructs.js b/src/utils/integrateRemoteStructs.js deleted file mode 100644 index 9d790740..00000000 --- a/src/utils/integrateRemoteStructs.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * @module utils - */ - -import * as decoding from 'lib0/decoding.js' -import { GC } from '../structs/GC.js' -import { Y } from '../utils/Y.js' // eslint-disable-line - -class MissingEntry { - constructor (decoder, missing, struct) { - this.decoder = decoder - this.missing = missing.length - this.struct = struct - } -} - -/** - * @private - * Integrate remote struct - * When a remote struct is integrated, other structs might be ready to ready to - * integrate. - * @param {Y} y - * @param {Item} struct - */ -function _integrateRemoteStructHelper (y, struct) { - const id = struct._id - if (id === undefined) { - struct._integrate(y) - } else { - if (y.ss.getState(id.user) > id.clock) { - return - } - if (!y.gcEnabled || struct.constructor === GC || (struct._parent.constructor !== GC && struct._parent._deleted === false)) { - // Is either a GC or Item with an undeleted parent - // save to integrate - struct._integrate(y) - } else { - // Is an Item. parent was deleted. - struct._gc(y) - } - let msu = y._missingStructs.get(id.user) - if (msu != null) { - let clock = id.clock - const finalClock = clock + struct._length - for (;clock < finalClock; clock++) { - const missingStructs = msu.get(clock) - if (missingStructs !== undefined) { - missingStructs.forEach(missingDef => { - missingDef.missing-- - if (missingDef.missing === 0) { - y._readyToIntegrate.push(missingDef) - } - }) - msu.delete(clock) - } - } - if (msu.size === 0) { - y._missingStructs.delete(id.user) - } - } - } -} - -/** - * @param {decoding.Decoder} decoder - * @param {Y} y - */ -export const integrateRemoteStructs = (decoder, y) => { - const len = decoding.readUint32(decoder) - for (let i = 0; i < len; i++) { - let reference = decoding.readVarUint(decoder) - let Constr = getStruct(reference) - let struct = new Constr() - let decoderPos = decoder.pos - let missing = struct._fromBinary(y, decoder) - if (missing.length === 0) { - while (struct !== null) { - _integrateRemoteStructHelper(y, struct) - struct = null - if (y._readyToIntegrate.length > 0) { - const missingDef = y._readyToIntegrate.shift() - const decoder = missingDef.decoder - let oldPos = decoder.pos - let missing = missingDef.struct._fromBinary(y, decoder) - decoder.pos = oldPos - if (missing.length === 0) { - struct = missingDef.struct - } else { - throw new Error('Missing should be empty') - } - } - } - } else { - let _decoder = decoding.createDecoder(decoder.arr.buffer) - _decoder.pos = decoderPos - let missingEntry = new MissingEntry(_decoder, missing, struct) - let missingStructs = y._missingStructs - for (let i = missing.length - 1; i >= 0; i--) { - let m = missing[i] - if (!missingStructs.has(m.user)) { - missingStructs.set(m.user, new Map()) - } - let msu = missingStructs.get(m.user) - if (!msu.has(m.clock)) { - msu.set(m.clock, []) - } - let mArray = msu = msu.get(m.clock) - mArray.push(missingEntry) - } - } - } -} diff --git a/src/utils/relativePosition.js b/src/utils/relativePosition.js index 9cf540be..a8c7b2ba 100644 --- a/src/utils/relativePosition.js +++ b/src/utils/relativePosition.js @@ -3,9 +3,13 @@ */ import * as ID from './ID.js' -import { GC } from '../structs/GC.js' - -// TODO: Implement function to describe ranges +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line +import * as encoding from 'lib0/encoding.js' +import * as decoding from 'lib0/decoding.js' +import * as error from 'lib0/error.js' +import { find, exists, getItemType, StructStore } from './StructStore.js' // eslint-disable-line +import { Y } from './Y.js' // eslint-disable-line /** * A relative position that is based on the Yjs model. In contrast to an @@ -18,9 +22,7 @@ import { GC } from '../structs/GC.js' * {@link getRelativePosition} and it can be transformed to an absolute position * with {@link fromRelativePosition}. * - * Pro tip: Use this to implement shared cursor locations in YText or YXml! - * The relative position is {@link encodable}, so you can send it to other - * clients. + * One of the properties must be defined. * * @example * // Current cursor position is at position 10 @@ -33,98 +35,220 @@ import { GC } from '../structs/GC.js' * absolutePosition.type // => yText * console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3 * - * @typedef {encodable} RelativePosition */ +export class RelativePosition { + /** + * @param {ID.ID|null} type + * @param {string|null} tname + * @param {ID.ID|null} item + */ + constructor (type, tname, item) { + /** + * @type {ID.ID|null} + */ + this.type = type + /** + * @type {string|null} + */ + this.tname = tname + /** + * @type {ID.ID | null} + */ + this.item = item + } +} + +export class AbsolutePosition { + /** + * @param {AbstractType} type + * @param {number} offset + */ + constructor (type, offset) { + /** + * @type {AbstractType} + */ + this.type = type + /** + * @type {number} + */ + this.offset = offset + } +} + +/** + * @param {AbstractType} type + * @param {number} offset + */ +export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset) + +/** + * @param {AbstractType} type + * @param {ID.ID|null} item + */ +export const createRelativePosition = (type, item) => { + let typeid = null + let tname = null + if (type._item === null) { + tname = ID.findRootTypeKey(type) + } else { + typeid = type._item.id + } + return new RelativePosition(typeid, tname, item) +} /** * Create a relativePosition based on a absolute position. * - * @param {YType} type The base type (e.g. YText or YArray). - * @param {Integer} offset The absolute position. + * @param {AbstractType} type The base type (e.g. YText or YArray). + * @param {number} offset The absolute position. + * @return {RelativePosition} */ -export const getRelativePosition = (type, offset) => { - // TODO: rename to createRelativePosition +export const createRelativePositionByOffset = (type, offset) => { let t = type._start while (t !== null) { - if (!t._deleted && t._countable) { - if (t._length > offset) { - return [t._id.user, t._id.clock + offset] + if (!t.deleted && t.countable) { + if (t.length > offset) { + // case 1: found position somewhere in the linked list + return createRelativePosition(type, ID.createID(t.id.client, t.id.clock + offset)) } - offset -= t._length + offset -= t.length } - t = t._right + t = t.right } - return ['endof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null] + return createRelativePosition(type, null) } /** - * @typedef {Object} AbsolutePosition The result of {@link fromRelativePosition} - * @property {YType} type The type on which to apply the absolute position. - * @property {number} offset The absolute offset.r + * @param {encoding.Encoder} encoder + * @param {RelativePosition} rpos */ +export const writeRelativePosition = (encoder, rpos) => { + const { type, tname, item } = rpos + if (item !== null) { + encoding.writeVarUint(encoder, 0) + ID.writeID(encoder, item) + } else if (tname !== null) { + // case 2: found position at the end of the list and type is stored in y.share + encoding.writeUint8(encoder, 1) + encoding.writeVarString(encoder, tname) + } else if (type !== null) { + // case 3: found position at the end of the list and type is attached to an item + encoding.writeUint8(encoder, 2) + ID.writeID(encoder, type) + } else { + throw error.unexpectedCase() + } + return encoder +} /** - * Transforms a relative position back to a relative position. - * - * @param {Y} y The Yjs instance in which to query for the absolute position. - * @param {RelativePosition} rpos The relative position. - * @return {AbsolutePosition} The absolute position in the Yjs model - * (type + offset). + * @param {decoding.Decoder} decoder + * @param {Y} y + * @param {StructStore} store + * @return {RelativePosition|null} */ -export const fromRelativePosition = (y, rpos) => { - if (rpos === null) { +export const readRelativePosition = (decoder, y, store) => { + let type = null + let tname = null + let itemID = null + switch (decoding.readVarUint(decoder)) { + case 0: + // case 1: found position somewhere in the linked list + itemID = ID.readID(decoder) + break + case 1: + // case 2: found position at the end of the list and type is stored in y.share + tname = decoding.readVarString(decoder) + break + case 2: { + // case 3: found position at the end of the list and type is attached to an item + type = ID.readID(decoder) + } + } + return new RelativePosition(type, tname, itemID) +} + +/** + * @param {RelativePosition} rpos + * @param {StructStore} store + * @param {Y} y + * @return {AbsolutePosition|null} + */ +export const toAbsolutePosition = (rpos, store, y) => { + const rightID = rpos.item + const typeID = rpos.type + const tname = rpos.tname + let type = null + let offset = 0 + if (rightID !== null) { + if (!exists(store, rightID)) { + return null + } + const right = find(store, rightID) + if (!(right instanceof AbstractItem)) { + return null + } + offset = right.deleted ? 0 : rightID.clock - right.id.clock + let n = right.left + while (n !== null) { + if (!n.deleted && n.countable) { + offset += n.length + } + n = n.left + } + type = right.parent + } else { + if (tname !== null) { + type = y.get(tname) + } else if (typeID !== null) { + type = getItemType(store, typeID).type + } else { + throw error.unexpectedCase() + } + offset = type._length + } + if (type._item !== null && type._item.deleted) { return null } - if (rpos[0] === 'endof') { - let id - if (rpos[3] === null) { - id = ID.createID(rpos[1], rpos[2]) - } else { - id = ID.createRootID(rpos[3], rpos[4]) - } - let type = y.os.get(id) - if (type === null) { - return null - } - while (type._redone !== null) { - type = type._redone - } - if (type === null || type.constructor === GC) { - return null - } - return { - type, - offset: type.length - } - } else { - let offset = 0 - let struct = y.os.findNodeWithUpperBound(ID.createID(rpos[0], rpos[1])).val - if (struct === null || struct._id.user === ID.RootFakeUserID) { - return null // TODO: support fake ids? - } - const diff = rpos[1] - struct._id.clock - while (struct._redone !== null) { - struct = struct._redone - } - const parent = struct._parent - if (struct.constructor === GC || parent._deleted) { - return null - } - if (!struct._deleted && struct._countable) { - offset = diff - } - struct = struct._left - while (struct !== null) { - if (!struct._deleted && struct._countable) { - offset += struct._length - } - struct = struct._left - } - return { - type: parent, - offset: offset - } - } + return createAbsolutePosition(type, offset) } -export const equal = (posa, posb) => posa === posb || (posa !== null && posb !== null && posa.length === posb.length && posa.every((v, i) => v === posb[i])) +/** + * Transforms an absolute to a relative position. + * + * @param {AbsolutePosition} apos The absolute position. + * @param {Y} y The Yjs instance in which to query for the absolute position. + * @return {RelativePosition} The absolute position in the Yjs model + * (type + offset). + */ +export const toRelativePosition = (apos, y) => { + const type = apos.type + if (type._length === apos.offset) { + return createRelativePosition(type, null) + } else { + let offset = apos.offset + let n = type._start + while (n !== null) { + if (!n.deleted && n.countable) { + if (n.length > offset) { + return createRelativePosition(type, ID.createID(n.id.client, n.id.clock + offset)) + } + offset -= n.length + } + n = n.right + } + } + throw error.unexpectedCase() +} + +/** + * @param {RelativePosition|null} a + * @param {RelativePosition|null} b + */ +export const compareRelativePositions = (a, b) => a === b || ( + a !== null && b !== null && ( + (a.item !== null && b.item !== null && ID.compareIDs(a.item, b.item)) || + (a.tname !== null && a.tname === b.tname) || + (a.type !== null && b.type !== null && ID.compareIDs(a.type, b.type)) + ) +) diff --git a/src/utils/structEncoding.js b/src/utils/structEncoding.js index f2743e8a..c85145fe 100644 --- a/src/utils/structEncoding.js +++ b/src/utils/structEncoding.js @@ -1,45 +1,147 @@ import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' -import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' +import * as map from 'lib0/map.js' +import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' // eslint-disable-line import * as binary from 'lib0/binary.js' -import { Transaction } from './Transaction.js' -import { findIndex } from './StructStore.js' +import { Transaction } from './Transaction.js' // eslint-disable-line +import { findIndexSS, exists, StructStore } from './StructStore.js' // eslint-disable-line +import { writeID, createID, readID, ID } from './ID.js' // eslint-disable-line +import * as iterator from 'lib0/iterator.js' +import { ItemBinaryRef } from '../structs/ItemBinary.js' +import { GCRef } from '../structs/GC.js' +import { ItemDeletedRef } from '../structs/ItemDeleted.js' +import { ItemEmbedRef } from '../structs/ItemEmbed.js' +import { ItemFormatRef } from '../structs/ItemFormat.js' +import { ItemJSONRef } from '../structs/ItemJSON.js' +import { ItemStringRef } from '../structs/ItemString.js' +import { ItemTypeRef } from '../structs/ItemType.js' + +/** + * @typedef {Map} StateMap + */ const structRefs = [ - ItemBinaryRef + ItemBinaryRef, + GCRef, + ItemDeletedRef, + ItemEmbedRef, + ItemFormatRef, + ItemJSONRef, + ItemStringRef, + ItemTypeRef ] +/** + * @param {decoding.Decoder} decoder + * @param {number} structsLen + * @param {ID} nextID + * @return {Iterator} + */ +const createStructReaderIterator = (decoder, structsLen, nextID) => iterator.createIterator(() => { + let done = false + let value + if (structsLen === 0) { + done = true + } else { + const info = decoding.readUint8(decoder) + value = new structRefs[binary.BITS5 & info](decoder, nextID, info) + nextID = createID(nextID.client, nextID.clock) + } + return { done, value } +}) + +/** + * @param {encoding.Encoder} encoder + * @param {Transaction} transaction + */ +export const writeStructsFromTransaction = (encoder, transaction) => writeStructs(encoder, transaction.y.store, transaction.stateUpdates) + +/** + * @param {encoding.Encoder} encoder + * @param {StructStore} store + * @param {StateMap} sm + */ +export const writeStructs = (encoder, store, sm) => { + const encoderUserPosMap = map.create() + // write # states that were updated + encoding.writeVarUint(encoder, sm.size) + sm.forEach((client, clock) => { + // write first id + writeID(encoder, createID(client, clock)) + encoderUserPosMap.set(client, encoding.length(encoder)) + // write diff to pos where structs are written + // We will fill out this value later *) + encoding.writeUint32(encoder, 0) + }) + sm.forEach((client, clock) => { + const decPos = encoderUserPosMap.get(client) + encoding.setUint32(encoder, decPos, encoding.length(encoder) - decPos) + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(client) + const startNewStructs = findIndexSS(structs, clock) + // write # encoded structs + encoding.writeVarUint(encoder, structs.length - startNewStructs) + const firstStruct = structs[startNewStructs] + // write first struct with an offset (may be 0) + firstStruct.write(encoder, clock - firstStruct.id.clock, 0) + for (let i = startNewStructs + 1; i < structs.length; i++) { + structs[i].write(encoder, 0, 0) + } + }) +} + /** * 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 {decoding.Decoder} decoder The decoder object to read data from. - * @return {AbstractRef} + * @param {Transaction} transaction + * @param {StructStore} store * * @private */ -export const read = decoder => { - const info = decoding.readUint8(decoder) - return new structRefs[binary.BITS5 & info](decoder, info) -} - -/** - * @param {encoding.Encoder} encoder - * @param {Transaction} transaction - */ -export const writeStructsFromTransaction = (encoder, transaction) => { - const stateUpdates = transaction.stateUpdates - const y = transaction.y - encoding.writeVarUint(encoder, stateUpdates.size) - stateUpdates.forEach((clock, client) => { - /** - * @type {Array} - */ - // @ts-ignore - const structs = y.store.clients.get(client) - for (let i = findIndex(structs, clock); i < structs.length; i++) { - structs[i].write(encoder, 0) +export const readStructs = (decoder, transaction, store) => { + /** + * @type {Map>} + */ + const structReaders = new Map() + const clientStateUpdates = decoding.readVarUint(decoder) + for (let i = 0; i < clientStateUpdates; i++) { + const nextID = readID(decoder) + const decoderPos = decoder.pos + decoding.readUint32(decoder) + const structReaderDecoder = decoding.clone(decoder, decoderPos) + const numberOfStructs = decoding.readVarUint(structReaderDecoder) + structReaders.set(nextID.client, createStructReaderIterator(structReaderDecoder, numberOfStructs, nextID)) + } + /** + * @type {Array} + */ + const stack = [] + for (const it of structReaders.values()) { + // todo try for in of it + for (let res = it.next(); !res.done; res = it.next()) { + stack.push(res.value) + while (stack.length > 0) { + const ref = stack[stack.length - 1] + const m = ref._missing + while (m.length > 0) { + const nextMissing = m[m.length - 1] + if (!exists(store, nextMissing)) { + // @ts-ignore must not be undefined, otherwise unexpected case + stack.push(structReaders.get(nextMissing.client).next().value) + break + } + ref._missing.pop() + } + if (m.length === 0) { + ref.toStruct(transaction).integrate(transaction) + stack.pop() + } + } } - }) + } } diff --git a/tests/index.js b/tests/index.js index 610b7b42..f4345138 100644 --- a/tests/index.js +++ b/tests/index.js @@ -6,9 +6,8 @@ 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({ map, array, text, xml, perf }) +runTests({ map, array, text, xml }) diff --git a/tests/perf.js b/tests/perf.js deleted file mode 100644 index 7f9571f6..00000000 --- a/tests/perf.js +++ /dev/null @@ -1,99 +0,0 @@ -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/tests/testHelper.js b/tests/testHelper.js index b1901c1e..12677bb8 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -5,7 +5,6 @@ import { createMutex } from 'lib0/mutex.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import * as syncProtocol from 'y-protocols/sync.js' -import { defragmentItemContent } from '../src/utils/defragmentItemContent.js' /** * @param {TestYInstance} y @@ -13,11 +12,9 @@ import { defragmentItemContent } from '../src/utils/defragmentItemContent.js' */ const afterTransaction = (y, transaction) => { y.mMux(() => { - if (transaction.encodedStructsLen > 0) { - const encoder = encoding.createEncoder() - syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs) - broadcastMessage(y, encoding.toBuffer(encoder)) - } + const encoder = encoding.createEncoder() + syncProtocol.writeUpdate(encoder, transaction.updateMessage) + broadcastMessage(y, encoding.toBuffer(encoder)) }) } @@ -217,6 +214,7 @@ export class TestConnector { /** * @param {t.TestCase} tc * @param {{users?:number}} conf + * @return {{testConnector:TestConnector,users:Array,array0:Y.Array,array1:Y.Array,array2:Y.Array,map0:Y.Map,map1:Y.Map,map2:Y.Map,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:YXmlFragment,xml1:YXmlFragment,xml2:YXmlFragment}} */ export const init = (tc, { users = 5 } = {}) => { /** @@ -231,50 +229,27 @@ export const init = (tc, { users = 5 } = {}) => { for (let i = 0; i < users; i++) { const y = testConnector.createY(i) result.users.push(y) - result['array' + i] = y.define('array', Y.Array) - result['map' + i] = y.define('map', Y.Map) - result['xml' + i] = y.define('xml', Y.XmlElement) - result['text' + i] = y.define('text', Y.Text) + result['array' + i] = y.get('array', Y.Array) + result['map' + i] = y.get('map', Y.Map) + result['xml' + i] = y.get('xml', Y.XmlElement) + result['text' + i] = y.get('text', Y.Text) } testConnector.syncAll() + // @ts-ignore return result } /** - * Convert DS to a proper DeleteSet of Map. - * - * @param {Y.Y} y - * @return {Object>} + * @param {any} constructor + * @param {ID} a + * @param {ID} b + * @param {string} path + * @param {any} next */ -const getDeleteSet = y => { - /** - * @type {Object>} - */ - var ds = {} - y.ds.iterate(null, null, n => { - var user = n._id.user - var counter = n._id.clock - var len = n.len - var gc = n.gc - var dv = ds[user] - if (dv === void 0) { - dv = [] - ds[user] = dv - } - dv.push([counter, len, gc]) - }) - return ds -} - const customOSCompare = (constructor, a, b, path, next) => { switch (constructor) { case Y.ID: - case Y.RootID: - if (a.equals(b)) { - return true - } else { - return false - } + return compareIDs(a, b) } return next(constructor, a, b, path, next) } diff --git a/tests/y-array.tests.js b/tests/y-array.tests.js index 8791f6d5..f5f44750 100644 --- a/tests/y-array.tests.js +++ b/tests/y-array.tests.js @@ -3,6 +3,9 @@ import * as Y from '../src/index.js' import * as t from 'lib0/testing.js' import * as prng from 'lib0/prng.js' +/** + * @param {t.TestCase} tc + */ export const testDeleteInsert = tc => { const { users, array0 } = init(tc, { users: 2 }) array0.delete(0, 0) @@ -16,6 +19,9 @@ export const testDeleteInsert = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testInsertThreeElementsTryRegetProperty = tc => { const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, [1, 2, 3]) @@ -25,6 +31,9 @@ export const testInsertThreeElementsTryRegetProperty = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testConcurrentInsertWithThreeConflicts = tc => { var { users, array0, array1, array2 } = init(tc, { users: 3 }) array0.insert(0, [0]) @@ -33,6 +42,9 @@ export const testConcurrentInsertWithThreeConflicts = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testConcurrentInsertDeleteWithThreeConflicts = tc => { const { testConnector, users, array0, array1, array2 } = init(tc, { users: 3 }) array0.insert(0, ['x', 'y', 'z']) @@ -44,6 +56,9 @@ export const testConcurrentInsertDeleteWithThreeConflicts = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testInsertionsInLateSync = tc => { const { testConnector, users, array0, array1, array2 } = init(tc, { users: 3 }) array0.insert(0, ['x', 'y']) @@ -59,6 +74,9 @@ export const testInsertionsInLateSync = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testDisconnectReallyPreventsSendingMessages = tc => { var { testConnector, users, array0, array1 } = init(tc, { users: 3 }) array0.insert(0, ['x', 'y']) @@ -74,6 +92,9 @@ export const testDisconnectReallyPreventsSendingMessages = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testDeletionsInLateSync = tc => { const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, ['x', 'y']) @@ -85,6 +106,9 @@ export const testDeletionsInLateSync = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testInsertThenMergeDeleteOnSync = tc => { const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, ['x', 'y', 'z']) @@ -105,6 +129,9 @@ const compareEvent = (is, should) => { } } +/** + * @param {t.TestCase} tc + */ export const testInsertAndDeleteEvents = tc => { const { array0, users } = init(tc, { users: 2 }) let event @@ -126,6 +153,9 @@ export const testInsertAndDeleteEvents = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testInsertAndDeleteEventsForTypes = tc => { const { array0, users } = init(tc, { users: 2 }) let event @@ -143,6 +173,9 @@ export const testInsertAndDeleteEventsForTypes = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testInsertAndDeleteEventsForTypes2 = tc => { const { array0, users } = init(tc, { users: 2 }) let events = [] @@ -162,6 +195,9 @@ export const testInsertAndDeleteEventsForTypes2 = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testGarbageCollector = tc => { const { testConnector, users, array0 } = init(tc, { users: 3 }) array0.insert(0, ['x', 'y', 'z']) @@ -173,6 +209,9 @@ export const testGarbageCollector = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testEventTargetIsSetCorrectlyOnLocal = tc => { const { array0, users } = init(tc, { users: 3 }) /** @@ -187,6 +226,9 @@ export const testEventTargetIsSetCorrectlyOnLocal = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testEventTargetIsSetCorrectlyOnRemote = tc => { const { testConnector, array0, array1, users } = init(tc, { users: 3 }) /** @@ -205,6 +247,9 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testIteratingArrayContainingTypes = tc => { const y = new Y.Y() const arr = y.define('arr', Y.Array) @@ -276,61 +321,106 @@ const arrayTransactions = [ } ] +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests20 = tc => { applyRandomTests(tc, arrayTransactions, 20) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests40 = tc => { applyRandomTests(tc, arrayTransactions, 40) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests42 = tc => { applyRandomTests(tc, arrayTransactions, 42) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests43 = tc => { applyRandomTests(tc, arrayTransactions, 43) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests44 = tc => { applyRandomTests(tc, arrayTransactions, 44) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests45 = tc => { applyRandomTests(tc, arrayTransactions, 45) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests46 = tc => { applyRandomTests(tc, arrayTransactions, 46) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests300 = tc => { applyRandomTests(tc, arrayTransactions, 300) } -/* TODO: implement something like difficutly in lib0 - +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests400 = tc => { + t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 400) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests500 = tc => { + t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 500) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests600 = tc => { + t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 600) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests1000 = tc => { + t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 1000) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests1800 = tc => { + t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 1800) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYarrayTests10000 = tc => { + t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 10000) } -*/ diff --git a/tests/y-map.tests.js b/tests/y-map.tests.js index c8949d6d..db6211e6 100644 --- a/tests/y-map.tests.js +++ b/tests/y-map.tests.js @@ -3,6 +3,9 @@ import * as Y from '../src/index.js' import * as t from 'lib0/testing.js' import * as prng from 'lib0/prng.js' +/** + * @param {t.TestCase} tc + */ export const testBasicMapTests = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) users[2].disconnect() @@ -38,6 +41,9 @@ export const testBasicMapTests = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testGetAndSetOfMapProperty = tc => { const { testConnector, users, map0 } = init(tc, { users: 2 }) map0.set('stuff', 'stuffy') @@ -56,6 +62,9 @@ export const testGetAndSetOfMapProperty = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testYmapSetsYmap = tc => { const { users, map0 } = init(tc, { users: 2 }) const map = map0.set('Map', new Y.Map()) @@ -65,6 +74,9 @@ export const testYmapSetsYmap = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testYmapSetsYarray = tc => { const { users, map0 } = init(tc, { users: 2 }) const array = map0.set('Array', new Y.Array()) @@ -74,6 +86,9 @@ export const testYmapSetsYarray = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testGetAndSetOfMapPropertySyncs = tc => { const { testConnector, users, map0 } = init(tc, { users: 2 }) map0.set('stuff', 'stuffy') @@ -86,6 +101,9 @@ export const testGetAndSetOfMapPropertySyncs = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testGetAndSetOfMapPropertyWithConflict = tc => { const { testConnector, users, map0, map1 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') @@ -98,6 +116,9 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testGetAndSetAndDeleteOfMapProperty = tc => { const { testConnector, users, map0, map1 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') @@ -111,6 +132,9 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') @@ -125,6 +149,9 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => { const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 }) map0.set('stuff', 'c0') @@ -145,6 +172,9 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testObserveDeepProperties = tc => { const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 }) const _map1 = map1.set('map', new Y.Map()) @@ -176,6 +206,9 @@ export const testObserveDeepProperties = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testObserversUsingObservedeep = tc => { const { users, map0 } = init(tc, { users: 2 }) const pathes = [] @@ -201,6 +234,9 @@ const compareEvent = (t, is, should) => { } } +/** + * @param {t.TestCase} tc + */ export const testThrowsAddAndUpdateAndDeleteEvents = tc => { const { users, map0 } = init(tc, { users: 2 }) let event @@ -229,6 +265,9 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => { const { users, map0 } = init(tc, { users: 3 }) let event @@ -240,6 +279,9 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc => { const { users, map0, map1, testConnector } = init(tc, { users: 3 }) let event @@ -274,61 +316,106 @@ const mapTransactions = [ } ] +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests20 = tc => { applyRandomTests(tc, mapTransactions, 20) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests40 = tc => { applyRandomTests(tc, mapTransactions, 40) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests42 = tc => { applyRandomTests(tc, mapTransactions, 42) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests43 = tc => { applyRandomTests(tc, mapTransactions, 43) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests44 = tc => { applyRandomTests(tc, mapTransactions, 44) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests45 = tc => { applyRandomTests(tc, mapTransactions, 45) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests46 = tc => { applyRandomTests(tc, mapTransactions, 46) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests300 = tc => { applyRandomTests(tc, mapTransactions, 300) } -/* TODO: implement something like difficutly in lib0 - +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests400 = tc => { + t.skip(!t.production) applyRandomTests(tc, mapTransactions, 400) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests500 = tc => { + t.skip(!t.production) applyRandomTests(tc, mapTransactions, 500) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests600 = tc => { + t.skip(!t.production) applyRandomTests(tc, mapTransactions, 600) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests1000 = tc => { + t.skip(!t.production) applyRandomTests(tc, mapTransactions, 1000) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests1800 = tc => { + t.skip(!t.production) applyRandomTests(tc, mapTransactions, 1800) } +/** + * @param {t.TestCase} tc + */ export const testRepeatGeneratingYmapTests10000 = tc => { + t.skip(!t.production) applyRandomTests(tc, mapTransactions, 10000) } -*/ diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index 28062a1c..aebd6e56 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -1,6 +1,9 @@ import { init, compare } from './testHelper.js' import * as t from 'lib0/testing.js' +/** + * @param {t.TestCase} tc + */ export const testBasicInsertAndDelete = tc => { const { users, text0 } = init(tc, { users: 2 }) let delta @@ -26,6 +29,9 @@ export const testBasicInsertAndDelete = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testBasicFormat = tc => { const { users, text0 } = init(tc, { users: 2 }) let delta diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js index d3ea9d28..60d0748b 100644 --- a/tests/y-xml.tests.js +++ b/tests/y-xml.tests.js @@ -2,6 +2,9 @@ import { init, compare } from './testHelper.js' import * as Y from '../src/index.js' import * as t from 'lib0/testing.js' +/** + * @param {t.TestCase} tc + */ export const testSetProperty = tc => { const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 }) xml0.setAttribute('height', '10') @@ -11,6 +14,9 @@ export const testSetProperty = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testEvents = tc => { const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 }) let event = { attributesChanged: new Set() } @@ -48,6 +54,9 @@ export const testEvents = tc => { compare(users) } +/** + * @param {t.TestCase} tc + */ export const testTreewalker = tc => { const { users, xml0 } = init(tc, { users: 3 }) let paragraph1 = new Y.XmlElement('p') diff --git a/tsconfig.json b/tsconfig.json index e33bdc3a..884d76d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -58,5 +58,6 @@ "typeRoots": ["./src/utils/typedefs.js"], // "types": ["./src/utils/typedefs.js"] }, - "exclude": ["./dist/**"] + "include": ["./src/**/*", "./tests/**/*"], + "exclude": ["../lib0/**/*", "node_modules"] }