From 415de1cc4cd08363391e5436b871b42a3f30028f Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Wed, 3 Apr 2019 02:30:44 +0200 Subject: [PATCH] all YArray.tests type fixes --- src/index.js | 2 +- src/structs/AbstractItem.js | 8 +- src/structs/AbstractStruct.js | 10 +++ src/structs/GC.js | 10 +++ src/structs/ItemDeleted.js | 11 +++ src/structs/ItemJSON.js | 18 +++- src/structs/ItemString.js | 13 ++- src/structs/ItemType.js | 5 +- src/types/AbstractType.js | 8 +- src/types/YArray.js | 44 +--------- src/types/YMap.js | 23 ++--- src/types/YText.js | 22 +++-- src/utils/DeleteSet.js | 4 +- src/utils/Y.js | 130 ++++++++++++++++++++++++--- src/utils/YEvent.js | 31 ++++++- tests/testHelper.js | 160 ++++++++++++++++++++-------------- tests/y-array.tests.js | 64 ++++++++------ 17 files changed, 383 insertions(+), 180 deletions(-) diff --git a/src/index.js b/src/index.js index 91f17d2d..ca2fd9cf 100644 --- a/src/index.js +++ b/src/index.js @@ -17,5 +17,5 @@ export { YXmlHook as XmlHook } from './types/YXmlHook.js' export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.js' export { createRelativePosition, createRelativePositionByOffset, createAbsolutePosition, compareRelativePositions, writeRelativePosition, readRelativePosition, AbsolutePosition, RelativePosition } from './utils/relativePosition.js' -export { ID, createID } from './utils/ID.js' +export { ID, createID, compareIDs } from './utils/ID.js' export { isParentOf } from './utils/isParentOf.js' diff --git a/src/structs/AbstractItem.js b/src/structs/AbstractItem.js index d9f1df89..9b8be18a 100644 --- a/src/structs/AbstractItem.js +++ b/src/structs/AbstractItem.js @@ -17,6 +17,7 @@ import { AbstractRef, AbstractStruct } from './AbstractStruct.js' // eslint-disa import * as error from 'lib0/error.js' import { replaceStruct, addStruct } from '../utils/StructStore.js' import { addToDeleteSet } from '../utils/DeleteSet.js' +import { ItemDeleted } from './ItemDeleted.js' /** * Split leftItem into two items @@ -408,9 +409,14 @@ export class AbstractItem extends AbstractStruct { /** * @param {Y} y + * @return {GC|ItemDeleted} */ gc (y) { - replaceStruct(y.store, this, new GC(this.id, this.length)) + const r = this.parent._item !== null && this.parent._item.deleted + ? new GC(this.id, this.length) + : new ItemDeleted(this.id, this.left, this.right, this.parent, this.parentSub, this.length) + replaceStruct(y.store, this, r) + return r } /** diff --git a/src/structs/AbstractStruct.js b/src/structs/AbstractStruct.js index 7669f681..c214d30a 100644 --- a/src/structs/AbstractStruct.js +++ b/src/structs/AbstractStruct.js @@ -17,6 +17,16 @@ export class AbstractStruct { */ this.id = id } + /** + * Merge this struct with the item to the right. + * This method is already assuming that `this.id.clock + this.length === this.id.clock`. + * Also this method does *not* remove right from StructStore! + * @param {AbstractStruct} right + * @return {boolean} wether this merged with right + */ + mergeWith (right) { + return false + } /** * @type {number} */ diff --git a/src/structs/GC.js b/src/structs/GC.js index 048d0f21..25ce963b 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -3,6 +3,7 @@ */ import { AbstractRef, AbstractStruct } from './AbstractStruct.js' import { ID, readID, createID, writeID } from '../utils/ID.js' // eslint-disable-line +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' import * as encoding from 'lib0/encoding.js' @@ -26,6 +27,15 @@ export class GC extends AbstractStruct { return true } + /** + * @param {AbstractStruct} right + * @return {boolean} + */ + mergeWith (right) { + this.length += right.length + return true + } + /** * @param {encoding.Encoder} encoder * @param {number} offset diff --git a/src/structs/ItemDeleted.js b/src/structs/ItemDeleted.js index fb1a83ce..e1e42751 100644 --- a/src/structs/ItemDeleted.js +++ b/src/structs/ItemDeleted.js @@ -37,6 +37,17 @@ export class ItemDeleted extends AbstractItem { copy (id, left, right, parent, parentSub) { return new ItemDeleted(id, left, right, parent, parentSub, this.length) } + /** + * @param {ItemDeleted} right + * @return {boolean} + */ + mergeWith (right) { + if (right.origin === this && this.right === right) { + this.length += right.length + return true + } + return false + } /** * @param {encoding.Encoder} encoder * @param {number} offset diff --git a/src/structs/ItemJSON.js b/src/structs/ItemJSON.js index a362df65..8f5f59ba 100644 --- a/src/structs/ItemJSON.js +++ b/src/structs/ItemJSON.js @@ -25,6 +25,9 @@ export class ItemJSON extends AbstractItem { */ constructor (id, left, right, parent, parentSub, content) { super(id, left, right, parent, parentSub) + /** + * @type {Array} + */ this.content = content } /** @@ -56,6 +59,17 @@ export class ItemJSON extends AbstractItem { right.content = this.content.splice(diff) return right } + /** + * @param {ItemJSON} right + * @return {boolean} + */ + mergeWith (right) { + if (right.origin === this && this.right === right) { + this.content = this.content.concat(right.content) + return true + } + return false + } /** * @param {encoding.Encoder} encoder * @param {number} offset @@ -63,8 +77,8 @@ export class ItemJSON extends AbstractItem { write (encoder, offset) { super.write(encoder, offset, structJSONRefNumber) const len = this.content.length - encoding.writeVarUint(encoder, len) - for (let i = 0; i < len; i++) { + encoding.writeVarUint(encoder, len - offset) + for (let i = offset; i < len; i++) { const c = this.content[i] encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c)) } diff --git a/src/structs/ItemString.js b/src/structs/ItemString.js index 12716f9f..4f3d81a6 100644 --- a/src/structs/ItemString.js +++ b/src/structs/ItemString.js @@ -60,13 +60,24 @@ export class ItemString extends AbstractItem { this.string = this.string.slice(0, diff) return right } + /** + * @param {ItemString} right + * @return {boolean} + */ + mergeWith (right) { + if (right.origin === this && this.right === right) { + this.string += right.string + return true + } + return false + } /** * @param {encoding.Encoder} encoder * @param {number} offset */ write (encoder, offset) { super.write(encoder, offset, structStringRefNumber) - encoding.writeVarString(encoder, this.string) + encoding.writeVarString(encoder, offset === 0 ? this.string : this.string.slice(offset)) } } diff --git a/src/structs/ItemType.js b/src/structs/ItemType.js index e053fc90..f63fb58e 100644 --- a/src/structs/ItemType.js +++ b/src/structs/ItemType.js @@ -18,6 +18,8 @@ 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 +import { GC } from './GC.js' // eslint-disable-line +import { ItemDeleted } from './ItemDeleted.js' // eslint-disable-line /** * @param {Y} y @@ -130,10 +132,11 @@ export class ItemType extends AbstractItem { /** * @param {Y} y + * @return {ItemDeleted|GC} */ gc (y) { this.gcChildren(y) - super.gc(y) + return super.gc(y) } } diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index d94024a3..a7fb65a1 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -95,7 +95,7 @@ export class AbstractType { * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { - this._callEventHandler(transaction, new YEvent(this)) + this._callEventHandler(transaction, new YEvent(this, transaction)) } /** @@ -125,7 +125,7 @@ export class AbstractType { /** * Observe all events that are created on this type. * - * @param {Function} f Observer function + * @param {function(YEvent):void} f Observer function */ observe (f) { this._eventHandler.addEventListener(f) @@ -223,6 +223,7 @@ export const typeArrayMap = (type, f) => { /** * @param {AbstractType} type + * @return {{next:function():{done:boolean,value:any|undefined}}} */ export const typeArrayCreateIterator = type => { let n = type._start @@ -242,7 +243,8 @@ export const typeArrayCreateIterator = type => { // check if we reached the end, no need to check currentContent, because it does not exist if (n === null) { return { - done: true + done: true, + value: undefined } } // currentContent could exist from the last iteration diff --git a/src/types/YArray.js b/src/types/YArray.js index 21c43814..752c0736 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -18,50 +18,8 @@ export class YArrayEvent extends YEvent { * @param {Transaction} transaction The transaction object */ constructor (yarray, transaction) { - super(yarray) + super(yarray, transaction) this._transaction = transaction - this._addedElements = null - this._removedElements = null - } - - /** - * Child elements that were added in this transaction. - * - * @return {Set} - */ - get addedElements () { - if (this._addedElements === null) { - const target = this.target - const transaction = this._transaction - const addedElements = new Set() - transaction.added.forEach(type => { - if (type.parent === target && !transaction.deleted.has(type)) { - addedElements.add(type) - } - }) - this._addedElements = addedElements - } - return this._addedElements - } - - /** - * Child elements that were removed in this transaction. - * - * @return {Set} - */ - get removedElements () { - if (this._removedElements === null) { - const target = this.target - const transaction = this._transaction - const removedElements = new Set() - transaction.deleted.forEach(struct => { - if (struct.parent === target && !transaction.added.has(struct)) { - removedElements.add(struct) - } - }) - this._removedElements = removedElements - } - return this._removedElements } } diff --git a/src/types/YMap.js b/src/types/YMap.js index 6ad378f3..8dedd044 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -2,7 +2,6 @@ * @module types */ -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' @@ -11,20 +10,23 @@ import { Transaction } from '../utils/Transaction.js' // eslint-disable-line import * as iterator from 'lib0/iterator.js' /** + * @template T * Event that describes the changes on a YMap. */ export class YMapEvent extends YEvent { /** - * @param {YMap} ymap The YArray that changed. + * @param {YMap} ymap The YArray that changed. + * @param {Transaction} transaction * @param {Set} subs The keys that changed. */ - constructor (ymap, subs) { - super(ymap) + constructor (ymap, transaction, subs) { + super(ymap, transaction) this.keysChanged = subs } } /** + * @template T number|string|Object|Array|ArrayBuffer * A shared Map implementation. */ export class YMap extends AbstractType { @@ -62,17 +64,17 @@ export class YMap extends AbstractType { * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { - this._callEventHandler(transaction, new YMapEvent(this, parentSubs)) + this._callEventHandler(transaction, new YMapEvent(this, transaction, parentSubs)) } /** * Transforms this Shared Type to a JSON object. * - * @return {Object} + * @return {Object} */ toJSON () { /** - * @type {Object} + * @type {Object} */ const map = {} for (let [key, item] of this._map) { @@ -94,7 +96,7 @@ export class YMap extends AbstractType { /** * Returns the value for each element in the YMap Type. * - * @return {Iterator|Array>} + * @return {Iterator} */ entries () { return iterator.iteratorMap(createMapIterator(this._map), v => v[1].getContent()[0]) @@ -124,7 +126,7 @@ export class YMap extends AbstractType { * Adds or updates an element with a specified key and value. * * @param {string} key The key of the element to add to this YMap - * @param {Object | string | number | AbstractType | ArrayBuffer } value The value of the element to add + * @param {T} value The value of the element to add */ set (key, value) { if (this._y !== null) { @@ -142,9 +144,10 @@ export class YMap extends AbstractType { * Returns a specified element from this YMap. * * @param {string} key - * @return {Object|number|Array|string|ArrayBuffer|AbstractType|undefined} + * @return {T|undefined} */ get (key) { + // @ts-ignore return typeMapGet(this, key) } diff --git a/src/types/YText.js b/src/types/YText.js index 6f834fc5..830fa106 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -355,8 +355,6 @@ class YTextEvent extends YArrayEvent { * @type {Array<{delete:number|undefined,retain:number|undefined,insert:string|undefined,attributes:Object}>} */ const delta = [] - const added = this.addedElements - const removed = this.removedElements const currentAttributes = new Map() // saves all current attributes for insert const oldAttributes = new Map() let item = this.target._start @@ -413,19 +411,19 @@ class YTextEvent extends YArrayEvent { while (item !== null) { switch (item.constructor) { case ItemEmbed: - if (added.has(item)) { + if (this.adds(item)) { addOp() action = 'insert' // @ts-ignore item is ItemFormat insert = item.embed addOp() - } else if (removed.has(item)) { + } else if (this.deletes(item)) { if (action !== 'delete') { addOp() action = 'delete' } deleteLen += 1 - } else if (item.deleted === false) { + } else if (!item.deleted) { if (action !== 'retain') { addOp() action = 'retain' @@ -434,20 +432,20 @@ class YTextEvent extends YArrayEvent { } break case ItemString: - if (added.has(item)) { + if (this.adds(item)) { if (action !== 'insert') { addOp() action = 'insert' } // @ts-ignore insert += item.string - } else if (removed.has(item)) { + } else if (this.deletes(item)) { if (action !== 'delete') { addOp() action = 'delete' } deleteLen += item.length - } else if (item.deleted === false) { + } else if (!item.deleted) { if (action !== 'retain') { addOp() action = 'retain' @@ -456,7 +454,7 @@ class YTextEvent extends YArrayEvent { } break case ItemFormat: - if (added.has(item)) { + if (this.adds(item)) { // @ts-ignore item is ItemFormat const curVal = currentAttributes.get(item.key) || null // @ts-ignore item is ItemFormat @@ -475,7 +473,7 @@ class YTextEvent extends YArrayEvent { } else { item.delete(transaction) } - } else if (removed.has(item)) { + } else if (this.deletes(item)) { // @ts-ignore item is ItemFormat oldAttributes.set(item.key, item.value) // @ts-ignore item is ItemFormat @@ -488,7 +486,7 @@ class YTextEvent extends YArrayEvent { // @ts-ignore item is ItemFormat attributes[item.key] = curVal } - } else if (item.deleted === false) { + } else if (!item.deleted) { // @ts-ignore item is ItemFormat oldAttributes.set(item.key, item.value) // @ts-ignore item is ItemFormat @@ -512,7 +510,7 @@ class YTextEvent extends YArrayEvent { } } } - if (item.deleted === false) { + if (!item.deleted) { if (action === 'insert') { addOp() } diff --git a/src/utils/DeleteSet.js b/src/utils/DeleteSet.js index 2cee8ccf..3b1f1137 100644 --- a/src/utils/DeleteSet.js +++ b/src/utils/DeleteSet.js @@ -45,7 +45,7 @@ export class DeleteSet { * @param {number} clock * @return {number|null} */ -export const findIndexSS = (dis, clock) => { +export const findIndexDS = (dis, clock) => { let left = 0 let right = dis.length while (left <= right) { @@ -71,7 +71,7 @@ export const findIndexSS = (dis, clock) => { */ export const isDeleted = (ds, id) => { const dis = ds.clients.get(id.client) - return dis !== undefined && findIndexSS(dis, id.clock) !== null + return dis !== undefined && findIndexDS(dis, id.clock) !== null } /** diff --git a/src/utils/Y.js b/src/utils/Y.js index 96fb1c50..ce9aac04 100644 --- a/src/utils/Y.js +++ b/src/utils/Y.js @@ -1,10 +1,19 @@ -import { StructStore } from './StructStore.js' +import { StructStore, findIndexSS } from './StructStore.js' import * as random from 'lib0/random.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 { AbstractItem } from '../structs/AbstractItem.js' +import { sortAndMergeDeleteSet } from './DeleteSet.js' +import * as math from 'lib0/math.js' +import { GC } from '../structs/GC.js' // eslint-disable-line +import { ItemDeleted } from '../structs/ItemDeleted.js' // eslint-disable-line +import { YArray } from '../types/YArray.js' +import { YText } from '../types/YText.js' +import { YMap } from '../types/YMap.js' +import { YXmlFragment } from '../types/YXmlElement.js' /** * A Yjs instance handles the state of shared data. @@ -54,16 +63,13 @@ export class Y extends Observable { * other peers. * * @param {function(Transaction):void} f The function that should be executed as a transaction - * @param {?Boolean} remote Optional. Whether this transaction is initiated by - * a remote peer. This should not be set manually! - * Defaults to false. */ - transact (f, remote = false) { + transact (f) { let initialCall = false if (this._transaction === null) { initialCall = true this._transaction = new Transaction(this) - this.emit('beforeTransaction', [this, this._transaction, remote]) + this.emit('beforeTransaction', [this, this._transaction]) } try { f(this._transaction) @@ -76,7 +82,7 @@ export class Y extends Observable { // only call event listeners / observers if anything changed const transactionChangedContent = transaction.changedParentTypes.size !== 0 if (transactionChangedContent) { - this.emit('beforeObserverCalls', [this, this._transaction, remote]) + this.emit('beforeObserverCalls', [this, this._transaction]) // emit change events on changed types transaction.changed.forEach((subs, itemtype) => { itemtype._callObserver(transaction, subs) @@ -95,11 +101,80 @@ export class Y extends Observable { type._deepEventHandler.callEventListeners(transaction, events) }) // when all changes & events are processed, emit afterTransaction event - this.emit('afterTransaction', [this, transaction, remote]) + this.emit('afterTransaction', [this, transaction]) // 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 + const store = transaction.y.store + const ds = transaction.deleteSet + // replace deleted items with ItemDeleted / GC + sortAndMergeDeleteSet(ds) + /** + * @type {Set} + */ + const replacedItems = new Set() + for (const [client, deleteItems] of ds.clients) { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(client) + for (let di = 0; di < deleteItems.length; di++) { + const deleteItem = deleteItems[di] + for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) { + const struct = structs[si] + if (deleteItem.clock + deleteItem.len < struct.id.clock) { + break + } + if (struct.deleted && struct instanceof AbstractItem) { + // check if we can GC + replacedItems.add(struct.gc(this)) + } + } + } + } + /** + * @param {Array} structs + * @param {number} pos + */ + const tryToMergeWithLeft = (structs, pos) => { + const left = structs[pos - 1] + const right = structs[pos] + if (left.deleted === right.deleted && left.constructor === right.constructor) { + if (left.mergeWith(right)) { + structs.splice(pos, 1) + } + } + } + // on all affected store.clients props, try to merge + for (const [client, clock] of transaction.stateUpdates) { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(client) + // we iterate from right to left so we can safely remove entries + for (let i = structs.length - 1; i >= math.max(findIndexSS(structs, clock), 1); i--) { + tryToMergeWithLeft(structs, i) + } + } + // try to merge replacedItems + for (const replacedItem of replacedItems) { + const id = replacedItem.id + const client = id.client + const clock = id.clock + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(client) + const replacedStructPos = findIndexSS(structs, clock) + if (replacedStructPos + 1 < structs.length) { + tryToMergeWithLeft(structs, replacedStructPos + 1) + } + if (replacedStructPos > 0) { + tryToMergeWithLeft(structs, replacedStructPos) + } + } + this.emit('afterTransactionCleanup', [this, transaction]) } } } @@ -148,6 +223,39 @@ export class Y extends Observable { } return type } + /** + * @template T + * @param {string} name + * @return {YArray} + */ + getArray (name) { + // @ts-ignore + return this.get(name, YArray) + } + /** + * @param {string} name + * @return {YText} + */ + getText (name) { + // @ts-ignore + return this.get(name, YText) + } + /** + * @param {string} name + * @return {YMap} + */ + getMap (name) { + // @ts-ignore + return this.get(name, YMap) + } + /** + * @param {string} name + * @return {YXmlFragment} + */ + getXmlFragment (name) { + // @ts-ignore + return this.get(name, YXmlFragment) + } /** * 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 3555d733..47bf89f9 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -1,5 +1,8 @@ import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { Transaction } from './Transaction.js' // eslint-disable-line +import { AbstractStruct } from '../structs/AbstractStruct.js' // eslint-disable-line +import { isDeleted } from './DeleteSet.js' /** * @module utils @@ -11,8 +14,9 @@ import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line export class YEvent { /** * @param {AbstractType} target The changed type. + * @param {Transaction} transaction */ - constructor (target) { + constructor (target, transaction) { /** * The type on which this event was created on. * @type {AbstractType} @@ -23,6 +27,11 @@ export class YEvent { * @type {AbstractType} */ this.currentTarget = target + /** + * The transaction that triggered this event. + * @type {Transaction} + */ + this.transaction = transaction } /** @@ -40,6 +49,26 @@ export class YEvent { // @ts-ignore _item is defined because target is integrated return getPathTo(this.currentTarget, this.target._item) } + + /** + * Check if a struct is deleted by this event. + * + * @param {AbstractStruct} struct + * @return {boolean} + */ + deletes (struct) { + return isDeleted(this.transaction.deleteSet, struct.id) + } + + /** + * Check if a struct is added by this event. + * + * @param {AbstractStruct} struct + * @return {boolean} + */ + adds (struct) { + return struct.id.clock > (this.transaction.stateUpdates.get(struct.id.client) || 0) + } } /** diff --git a/tests/testHelper.js b/tests/testHelper.js index 12677bb8..8b4e0898 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -5,6 +5,9 @@ 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 { createDeleteSetFromStructStore, DeleteSet } from '../src/utils/DeleteSet.js' // eslint-disable-line +import { getStates, StructStore } from '../src/utils/StructStore.js' // eslint-disable-line +import { AbstractItem } from '../src/structs/AbstractItem.js' // eslint-disable-line /** * @param {TestYInstance} y @@ -35,6 +38,7 @@ const broadcastMessage = (y, m) => { export class TestYInstance extends Y.Y { /** * @param {TestConnector} testConnector + * @param {number} clientID */ constructor (testConnector, clientID) { super() @@ -109,6 +113,9 @@ export class TestYInstance extends Y.Y { * I think it makes sense. Deal with it. */ export class TestConnector { + /** + * @param {prng.PRNG} gen + */ constructor (gen) { /** * @type {Set} @@ -197,6 +204,9 @@ export class TestConnector { * @return {boolean} Whether it was possible to reconnect a random connection. */ reconnectRandom () { + /** + * @type {Array} + */ const reconnectable = [] this.allConns.forEach(conn => { if (!this.onlineConns.has(conn)) { @@ -214,7 +224,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}} + * @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:Y.XmlFragment,xml1:Y.XmlFragment,xml2:Y.XmlFragment}} */ export const init = (tc, { users = 5 } = {}) => { /** @@ -239,21 +249,6 @@ export const init = (tc, { users = 5 } = {}) => { return result } -/** - * @param {any} constructor - * @param {ID} a - * @param {ID} b - * @param {string} path - * @param {any} next - */ -const customOSCompare = (constructor, a, b, path, next) => { - switch (constructor) { - case Y.ID: - return compareIDs(a, b) - } - return next(constructor, a, b, path, next) -} - /** * 1. reconnect and flush all * 2. user 0 gc @@ -266,59 +261,22 @@ const customOSCompare = (constructor, a, b, path, next) => { export const compare = users => { users.forEach(u => u.connect()) while (users[0].tc.flushAllMessages()) {} - var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val))) - var userMapValues = users.map(u => u.define('map', Y.Map).toJSON()) - var userXmlValues = users.map(u => u.define('xml', Y.XmlElement).toString()) - var userTextValues = users.map(u => u.define('text', Y.Text).toDelta()) - var data = users.map(u => { - defragmentItemContent(u) - var data = {} - let ops = [] - u.os.iterate(null, null, op => { - let json - if (op.constructor === Y.GC) { - json = { - type: 'GC', - id: op._id, - length: op._length, - content: null - } - } else { - json = { - id: op._id, - left: op._left === null ? null : op._left._lastId, - right: op._right === null ? null : op._right._id, - length: op._length, - deleted: op._deleted, - parent: op._parent._id, - content: null - } - } - if (op instanceof Y.ItemJSON || op instanceof Y.ItemString) { - json.content = op._content - } - ops.push(json) - }) - data.os = ops - data.ds = getDeleteSet(u) - const ss = {} - u.ss.state.forEach((clock, user) => { - ss[user] = clock - }) - data.ss = ss - return data - }) - for (var i = 0; i < data.length - 1; i++) { - // t.describe(`Comparing user${i} with user${i + 1}`) - t.compare(userArrayValues[i].length, users[i].get('array').length) + const userArrayValues = users.map(u => u.getArray('array').toJSON().map(val => JSON.stringify(val))) + const userMapValues = users.map(u => u.getMap('map').toJSON()) + const userXmlValues = users.map(u => u.getXmlFragment('xml').toString()) + const userTextValues = users.map(u => u.getText('text').toDelta()) + for (var i = 0; i < users.length - 1; i++) { + t.describe(`Comparing user${i} with user${i + 1}`) + t.compare(userArrayValues[i].length, users[i].getArray('array').length) t.compare(userArrayValues[i], userArrayValues[i + 1]) t.compare(userMapValues[i], userMapValues[i + 1]) t.compare(userXmlValues[i], userXmlValues[i + 1]) - t.compare(userTextValues[i].map(a => a.insert).join('').length, users[i].get('text').length) + // @ts-ignore + t.compare(userTextValues[i].map(a => a.insert).join('').length, users[i].getText('text').length) t.compare(userTextValues[i], userTextValues[i + 1]) - t.compare(data[i].os, data[i + 1].os, null, customOSCompare) - t.compare(data[i].ds, data[i + 1].ds, null, customOSCompare) - t.compare(data[i].ss, data[i + 1].ss, null, customOSCompare) + t.compare(getStates(users[i].store), getStates(users[i + 1].store)) + compareDS(createDeleteSetFromStructStore(users[i].store), createDeleteSetFromStructStore(users[i + 1].store)) + compareStructStores(users[i].store, users[i + 1].store) } users.forEach(user => t.assert(user._missingStructs.size === 0) @@ -326,6 +284,76 @@ export const compare = users => { users.map(u => u.destroy()) } +/** + * @param {AbstractItem?} a + * @param {AbstractItem?} b + * @return {boolean} + */ +export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id)) + +/** + * @param {StructStore} ss1 + * @param {StructStore} ss2 + */ +export const compareStructStores = (ss1, ss2) => { + t.assert(ss1.clients.size === ss2.clients.size) + for (const [client, structs1] of ss1.clients) { + const structs2 = ss2.clients.get(client) + t.assert(structs2 !== undefined && structs1.length === structs2.length) + for (let i = 0; i < structs1.length; i++) { + const s1 = structs1[i] + // @ts-ignore + const s2 = structs2[i] + // checks for abstract struct + if ( + s1.constructor !== s2.constructor || + !Y.compareIDs(s1.id, s2.id) || + s1.deleted !== s2.deleted || + s1.length !== s2.length + ) { + t.fail('Structs dont match') + } + if (s1 instanceof AbstractItem) { + if ( + !(s2 instanceof AbstractItem) || + !compareItemIDs(s1.left, s2.left) || + !compareItemIDs(s1.right, s2.right) || + !compareItemIDs(s1.origin, s2.origin) || + !compareItemIDs(s1.rightOrigin, s2.rightOrigin) || + s1.parentSub !== s2.parentSub + ) { + t.fail('Items dont match') + } + } + } + } +} + +/** + * @param {DeleteSet} ds1 + * @param {DeleteSet} ds2 + */ +export const compareDS = (ds1, ds2) => { + t.assert(ds1.clients.size === ds2.clients.size) + for (const [client, deleteItems1] of ds1.clients) { + const deleteItems2 = ds2.clients.get(client) + t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length) + for (let i = 0; i < deleteItems1.length; i++) { + const di1 = deleteItems1[i] + // @ts-ignore + const di2 = deleteItems2[i] + if (di1.clock !== di2.clock || di1.len !== di2.len) { + t.fail('DeleteSets dont match') + } + } + } +} + +/** + * @param {t.TestCase} tc + * @param {Array} mods + * @param {number} iterations + */ export const applyRandomTests = (tc, mods, iterations) => { const gen = tc.prng const result = init(tc, { users: 5 }) @@ -350,7 +378,7 @@ export const applyRandomTests = (tc, mods, iterations) => { } let user = prng.oneOf(gen, users) var test = prng.oneOf(gen, mods) - test(t, user, gen) + test(user, gen) } compare(users) return result diff --git a/tests/y-array.tests.js b/tests/y-array.tests.js index f5f44750..ab950806 100644 --- a/tests/y-array.tests.js +++ b/tests/y-array.tests.js @@ -1,4 +1,4 @@ -import { init, compare, applyRandomTests } from './testHelper.js' +import { init, compare, applyRandomTests, TestYInstance } from './testHelper.js' // eslint-disable-line import * as Y from '../src/index.js' import * as t from 'lib0/testing.js' import * as prng from 'lib0/prng.js' @@ -119,6 +119,10 @@ export const testInsertThenMergeDeleteOnSync = tc => { compare(users) } +/** + * @param {Object} is + * @param {Object} should + */ const compareEvent = (is, should) => { for (var key in should) { t.assert( @@ -134,7 +138,10 @@ const compareEvent = (is, should) => { */ export const testInsertAndDeleteEvents = tc => { const { array0, users } = init(tc, { users: 2 }) - let event + /** + * @type {Object} + */ + let event = {} array0.observe(e => { event = e }) @@ -158,11 +165,14 @@ export const testInsertAndDeleteEvents = tc => { */ export const testInsertAndDeleteEventsForTypes = tc => { const { array0, users } = init(tc, { users: 2 }) - let event + /** + * @type {Object} + */ + let event = {} array0.observe(e => { event = e }) - array0.insert(0, [Y.Array]) + array0.insert(0, [new Y.Array()]) compareEvent(event, { remote: false }) @@ -178,11 +188,14 @@ export const testInsertAndDeleteEventsForTypes = tc => { */ export const testInsertAndDeleteEventsForTypes2 = tc => { const { array0, users } = init(tc, { users: 2 }) + /** + * @type {Array>} + */ let events = [] array0.observe(e => { events.push(e) }) - array0.insert(0, ['hi', Y.Map]) + array0.insert(0, ['hi', new Y.Map()]) compareEvent(events[0], { remote: false }) @@ -252,7 +265,7 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => { */ export const testIteratingArrayContainingTypes = tc => { const y = new Y.Y() - const arr = y.define('arr', Y.Array) + const arr = y.getArray('arr') const numItems = 10 for (let i = 0; i < numItems; i++) { const map = new Y.Map() @@ -269,9 +282,12 @@ export const testIteratingArrayContainingTypes = tc => { let _uniqueNumber = 0 const getUniqueNumber = () => _uniqueNumber++ +/** + * @type {Array} + */ const arrayTransactions = [ - function insert (tc, user, gen) { - const yarray = user.define('array', Y.Array) + function insert (user, gen) { + const yarray = user.getArray('array') var uniqueNumber = getUniqueNumber() var content = [] var len = prng.int31(gen, 1, 4) @@ -281,38 +297,34 @@ const arrayTransactions = [ var pos = prng.int31(gen, 0, yarray.length) yarray.insert(pos, content) }, - function insertTypeArray (tc, user, gen) { - const yarray = user.define('array', Y.Array) + function insertTypeArray (user, gen) { + const yarray = user.getArray('array') var pos = prng.int31(gen, 0, yarray.length) - yarray.insert(pos, [Y.Array]) + yarray.insert(pos, [new Y.Array()]) var array2 = yarray.get(pos) array2.insert(0, [1, 2, 3, 4]) }, - function insertTypeMap (tc, user, gen) { - const yarray = user.define('array', Y.Array) + function insertTypeMap (user, gen) { + const yarray = user.getArray('array') var pos = prng.int31(gen, 0, yarray.length) - yarray.insert(pos, [Y.Map]) + yarray.insert(pos, [new Y.Map()]) var map = yarray.get(pos) map.set('someprop', 42) map.set('someprop', 43) map.set('someprop', 44) }, - function _delete (tc, user, gen) { - const yarray = user.define('array', Y.Array) + function _delete (user, gen) { + const yarray = user.getArray('array') var length = yarray.length if (length > 0) { var somePos = prng.int31(gen, 0, length - 1) var delLength = prng.int31(gen, 1, Math.min(2, length - somePos)) - if (yarray instanceof Y.Array) { - if (prng.bool(gen)) { - var type = yarray.get(somePos) - if (type.length > 0) { - somePos = prng.int31(gen, 0, type.length - 1) - delLength = prng.int31(gen, 0, Math.min(2, type.length - somePos)) - type.delete(somePos, delLength) - } - } else { - yarray.delete(somePos, delLength) + if (prng.bool(gen)) { + var type = yarray.get(somePos) + if (type.length > 0) { + somePos = prng.int31(gen, 0, type.length - 1) + delLength = prng.int31(gen, 0, Math.min(2, type.length - somePos)) + type.delete(somePos, delLength) } } else { yarray.delete(somePos, delLength)