diff --git a/package.json b/package.json index 3e01023f..5419c0e8 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", + "test": "npm run dist && 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/src/structs/AbstractItem.js b/src/structs/AbstractItem.js index a9c73563..69288c31 100644 --- a/src/structs/AbstractItem.js +++ b/src/structs/AbstractItem.js @@ -2,63 +2,29 @@ * @module structs */ -import { readID, createID, writeID, writeNullID, ID, createNextID } from '../utils/ID.js' // eslint-disable-line -import { Delete } from '../Delete.js' -import { writeStructToTransaction } from '../utils/structEncoding.js' +import { readID, createID, writeID, writeNullID, 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' import { ItemType } from './ItemType.js' // eslint-disable-line -import { AbstractType } from '../types/AbstractType.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line import { Y } from '../utils/Y.js' // eslint-disable-line -import { Transaction } from '../utils/Transaction.js' // eslint-disable-line +import { Transaction, nextID } from '../utils/Transaction.js' // eslint-disable-line import * as maplib from 'lib0/map.js' import * as set from 'lib0/set.js' import * as binary from 'lib0/binary.js' import { AbstractRef, AbstractStruct } from './AbstractStruct.js' // eslint-disable-line import * as error from 'lib0/error.js' - -/** - * Stringify an item id. - * - * @param { ID } id - * @return {string} - */ -export const stringifyID = id => `(${id.client},${id.clock})` - -/** - * Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent). - * - * @param {AbstractItem | null} item - * @return {string} - */ -export const stringifyItemID = item => - item === null ? '()' : (item.id != null ? stringifyID(item.id) : 'y') - -/** - * Helper utility to convert an item to a readable format. - * - * @param {String} name The name of the item class (YText, ItemString, ..). - * @param {AbstractItem} item The item instance. - * @param {String} [append] Additional information to append to the returned - * string. - * @return {String} A readable string that represents the item object. - * - */ -export const logItemHelper = (name, item, append) => { - const left = item.left !== null ? stringifyID(item.left.lastId) : '()' - const origin = item.origin !== null ? stringifyID(item.origin.lastId) : '()' - return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item.right)},parent:${stringifyItemID(item.parent)},parentSub:${item.parentSub}${append !== undefined ? ' - ' + append : ''})` -} +import { replaceStruct, addStruct } from '../utils/StructStore.js' /** * Split leftItem into two items + * @param {Transaction} transaction * @param {AbstractItem} leftItem - * @param {Y} y * @param {number} diff - * @return {any} + * @return {AbstractItem} */ -export const splitItem = (leftItem, diff) => { +export const splitItem = (transaction, leftItem, diff) => { const id = leftItem.id // create rightItem const rightItem = leftItem.copy(createID(id.client, id.clock + diff), leftItem, leftItem.rightOrigin, leftItem.parent, leftItem.parentSub) @@ -85,6 +51,13 @@ export const splitItem = (leftItem, diff) => { foundOrigins.add(o) o = o.right } + const right = leftItem.splitAt(diff) + if (transaction.added.has(leftItem)) { + transaction.added.add(right) + } else if (transaction.deleted.has(leftItem)) { + transaction.deleted.add(right) + } + return rightItem } /** @@ -106,7 +79,7 @@ export class AbstractItem extends AbstractStruct { parent = right.parent parentSub = right.parentSub } else if (parent === null) { - error.throwUnexpectedCase() + throw error.unexpectedCase() } super(id) /** @@ -163,7 +136,6 @@ export class AbstractItem extends AbstractStruct { * @param {Transaction} transaction */ integrate (transaction) { - const y = transaction.y const id = this.id const parent = this.parent const parentSub = this.parentSub @@ -171,12 +143,6 @@ export class AbstractItem extends AbstractStruct { const left = this.left const right = this.right // integrate - const parentType = parent !== null ? parent.type : maplib.setTfUndefined(y.share, parentSub, () => new AbstractType()) - if (y.ss.getState(id.client) !== id.clock) { - throw new Error('Expected other operation') - } - y.ss.setState(id.client, id.clock + length) - transaction.added.add(this) /* # $this has to find a unique position between origin and the next known character # case 1: $origin equals $o.origin: the $creator parameter decides if left or right @@ -200,10 +166,10 @@ export class AbstractItem extends AbstractStruct { // set o to the first conflicting item if (left !== null) { o = left.right - } else if (this.parentSub !== null) { - o = parentType._map.get(parentSub) || null + } else if (parentSub !== null) { + o = parent._map.get(parentSub) || null } else { - o = parentType._start + o = parent._start } const conflictingItems = new Set() const itemsBeforeOrigin = new Set() @@ -244,31 +210,31 @@ export class AbstractItem extends AbstractStruct { } else { let r if (parentSub !== null) { - const pmap = parentType._map + const pmap = parent._map r = pmap.get(parentSub) || null pmap.set(parentSub, this) } else { - r = parentType._start - parentType._start = this + r = parent._start + parent._start = this } this.right = r if (r !== null) { - r._left = this + r.left = this } } // adjust the length of parent if (parentSub === null && this.countable) { - parentType._length += length + parent._length += length } - if (parent !== null && parent.deleted) { + addStruct(transaction.y.store, this) + if (parent !== null) { + maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub) + } + transaction.added.add(this) + // @ts-ignore + if (parent._item.deleted) { this.delete(transaction, false, true) } - y.os.put(this) - if (parent !== null) { - maplib.setTfUndefined(transaction.changed, parent, set.create).add(parentSub) - } - - writeStructToTransaction(y._transaction, this) } /** @@ -277,7 +243,7 @@ export class AbstractItem extends AbstractStruct { */ get next () { let n = this.right - while (n !== null && n._deleted) { + while (n !== null && n.deleted) { n = n.right } return n @@ -289,7 +255,7 @@ export class AbstractItem extends AbstractStruct { */ get prev () { let n = this.left - while (n !== null && n._deleted) { + while (n !== null && n.deleted) { n = n.left } return n @@ -301,7 +267,7 @@ export class AbstractItem extends AbstractStruct { * @param {ID} id * @param {AbstractItem|null} left * @param {AbstractItem|null} right - * @param {ItemType|null} parent + * @param {AbstractType} parent * @param {string|null} parentSub * @return {AbstractItem} */ @@ -312,12 +278,12 @@ export class AbstractItem extends AbstractStruct { /** * Redoes the effect of this operation. * - * @param {Y} y The Yjs instance. + * @param {Transaction} transaction The Yjs instance. * @param {Set} redoitems * * @private */ - redo (y, redoitems) { + redo (transaction, redoitems) { if (this.redone !== null) { return this.redone } @@ -337,12 +303,11 @@ export class AbstractItem extends AbstractStruct { // Is a map item. Insert at the start left = null right = parent.type._map.get(this.parentSub) - right._delete(y) } // make sure that parent is redone if (parent._deleted === true && parent.redone === null) { // try to undo parent if it will be undone anyway - if (!redoitems.has(parent) || !parent.redo(y, redoitems)) { + if (!redoitems.has(parent) || !parent.redo(transaction, redoitems)) { return false } } @@ -365,7 +330,8 @@ export class AbstractItem extends AbstractStruct { right = right._right } } - this.redone = this.copy(createNextID(y), left, right, parent, this.parentSub) + this.redone = this.copy(nextID(transaction), left, right, parent, this.parentSub) + this.redone.integrate(transaction) return true } @@ -401,6 +367,8 @@ export class AbstractItem extends AbstractStruct { } /** + * Do not call directly. Always split via StructStore! + * * Splits this Item so that another Item can be inserted in-between. * This must be overwritten if _length > 1 * Returns right part after split @@ -411,10 +379,11 @@ export class AbstractItem extends AbstractStruct { * * This method should only be cally by StructStore. * + * @param {Transaction} transaction * @param {number} diff * @return {AbstractItem} */ - splitAt (diff) { + splitAt (transaction, diff) { throw new Error('unimplemented') } @@ -430,32 +399,13 @@ export class AbstractItem extends AbstractStruct { */ delete (transaction, createDelete = true, gcChildren) { if (!this.deleted) { - const y = transaction.y const parent = this.parent - const len = this.length // adjust the length of parent if (this.countable && this.parentSub === null) { - if (parent !== null) { - // parent is y - y.get(this.) - - } else { - transaction.y.get(this.parentSub) - } - } - if (parent.length !== undefined && this.countable) { - parent.length -= len - } - this._deleted = true - y.ds.mark(this.id, this.length, false) - let del = new Delete(this.id, len) - if (createDelete) { - // broadcast and persists Delete - del.integrate(y, true) - } - if (parent !== null) { - maplib.setTfUndefined(transaction.changed, parent, set.create).add(this.parentSub) + parent._length -= this.length } + this.deleted = true + maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub) transaction.deleted.add(this) } } @@ -469,13 +419,14 @@ export class AbstractItem extends AbstractStruct { * @param {Y} y */ gc (y) { - if (this.id !== null) { - y.os.replace(this, new GC(this.id, this.length)) - } + replaceStruct(y.store, this, new GC(this.id, this.length)) } + /** + * @return {Array} + */ getContent () { - throw new Error('Must implement') // TODO: create function in lib0 + throw error.methodUnimplemented() } /** @@ -502,11 +453,28 @@ export class AbstractItem extends AbstractStruct { writeID(encoder, this.rightOrigin.id) } if (this.origin === null && this.rightOrigin === null) { - if (this.parent === null) { + const parent = this.parent + if (parent._item === null) { + // parent type on y._map + // find the correct key + // @ts-ignore we know that y exists + const map = parent._y.share + let ykey = null + for (const [key, type] of map) { + if (type === parent) { + ykey = key + break + } + } + if (ykey === null) { + throw error.unexpectedCase() + } writeNullID(encoder) + encoding.writeVarString(encoder, ykey) } else { // neither origin nor right is defined - writeID(encoder, this.parent.id) + // @ts-ignore _item is defined because parent is integrated + writeID(encoder, parent._item.id) } if (this.parentSub !== null) { encoding.writeVarString(encoder, this.parentSub) @@ -524,7 +492,7 @@ export class AbstractItemRef extends AbstractRef { super() const id = readID(decoder) if (id === null) { - throw new Error('id must not be null') + throw error.unexpectedCase() } /** * The uniqe identifier of this type. @@ -547,6 +515,13 @@ export class AbstractItemRef extends AbstractRef { * @type {ID | null} */ this.parent = canCopyParentInfo ? readID(decoder) : null + /** + * 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 /** * 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 diff --git a/src/structs/AbstractStruct.js b/src/structs/AbstractStruct.js index 11b73964..c7ab8397 100644 --- a/src/structs/AbstractStruct.js +++ b/src/structs/AbstractStruct.js @@ -1,6 +1,8 @@ import { Y } from '../utils/Y.js' // eslint-disable-line -import { ID } from '../utils/ID.js' // eslint-disable-line +import { ID, createID } from '../utils/ID.js' // eslint-disable-line import { Transaction } from '../utils/Transaction.js' // eslint-disable-line +import * as encoding from 'lib0/encoding.js' // eslint-disable-line +import * as error from 'lib0/error.js' // eslint-disable-next-line export class AbstractStruct { @@ -19,7 +21,15 @@ export class AbstractStruct { * @type {number} */ get length () { - throw new Error('unimplemented') + throw error.methodUnimplemented() + } + /** + * @param {encoding.Encoder} encoder The encoder to write data to. + * @param {number} encodingRef + * @private + */ + write (encoder, encodingRef) { + throw error.methodUnimplemented() } } @@ -34,5 +44,7 @@ export class AbstractRef { * @param {Transaction} transaction * @return {AbstractStruct} */ - toStruct (transaction) { throw new Error('Must be defined') } + toStruct (transaction) { + throw error.methodUnimplemented() + } } diff --git a/src/structs/GC.js b/src/structs/GC.js index f6bce3bd..c5a1ef42 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -22,6 +22,10 @@ export class GC extends AbstractStruct { this.length = length } + get deleted () { + return true + } + /** * @param {encoding.Encoder} encoder */ diff --git a/src/structs/ItemBinary.js b/src/structs/ItemBinary.js index 25c1dad7..1fd22ec5 100644 --- a/src/structs/ItemBinary.js +++ b/src/structs/ItemBinary.js @@ -4,11 +4,11 @@ // TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency -import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js' +import { AbstractItem, AbstractItemRef } from './AbstractItem.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { ID } from '../utils/ID.js' // eslint-disable-line -import { ItemType } from './ItemType.js' // eslint-disable-line import { Y } from '../utils/Y.js' // eslint-disable-line import { Transaction } from '../utils/Transaction.js' // eslint-disable-line import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' @@ -20,7 +20,7 @@ export class ItemBinary extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub * @param {ArrayBuffer} content */ @@ -28,25 +28,19 @@ export class ItemBinary extends AbstractItem { super(id, left, right, parent, parentSub) this.content = content } + getContent () { + return [this.content] + } /** * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub */ copy (id, left, right, parent, parentSub) { return new ItemBinary(id, left, right, parent, parentSub, this.content) } - /** - * Transform this Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - logString () { - return logItemHelper('ItemBinary', this) - } /** * @param {encoding.Encoder} encoder */ @@ -73,12 +67,14 @@ export class ItemBinaryRef extends AbstractItemRef { * @return {ItemBinary} */ toStruct (transaction) { - const store = transaction.y.store + const y = transaction.y + const store = y.store return new ItemBinary( this.id, this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.right === null ? null : getItemCleanStart(store, transaction, this.right), - this.parent === null ? null : getItemType(store, this.parent), + // @ts-ignore + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type, this.parentSub, this.content ) diff --git a/src/structs/ItemDeleted.js b/src/structs/ItemDeleted.js index fb6d34ec..ca50eac6 100644 --- a/src/structs/ItemDeleted.js +++ b/src/structs/ItemDeleted.js @@ -4,53 +4,45 @@ // TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency -import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js' +import { 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' export const structDeletedRefNumber = 2 -export class ItemBinary extends AbstractItem { +export class ItemDeleted extends AbstractItem { /** * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub - * @param {ArrayBuffer} content + * @param {number} length */ - constructor (id, left, right, parent, parentSub, content) { + constructor (id, left, right, parent, parentSub, length) { super(id, left, right, parent, parentSub) - this.content = content + this.length = length } /** * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub */ copy (id, left, right, parent, parentSub) { - return new ItemBinary(id, left, right, parent, parentSub, this.content) - } - /** - * Transform this Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - logString () { - return logItemHelper('ItemBinary', this) + return new ItemDeleted(id, left, right, parent, parentSub, this.length) } /** * @param {encoding.Encoder} encoder */ write (encoder) { super.write(encoder, structDeletedRefNumber) - encoding.writePayload(encoder, this.content) + encoding.writeVarUint(encoder, this.length) } } @@ -62,22 +54,25 @@ export class ItemDeletedRef extends AbstractItemRef { constructor (decoder, info) { super(decoder, info) /** - * @type {ArrayBuffer} + * @type {number} */ - this.content = decoding.readPayload(decoder) + this.length = decoding.readVarUint(decoder) } /** - * @param {Y} y - * @return {ItemBinary} + * @param {Transaction} transaction + * @return {ItemDeleted} */ - toStruct (y) { - return new ItemBinary( + toStruct (transaction) { + const y = transaction.y + const store = y.store + return new ItemDeleted( this.id, - this.left === null ? null : y.os.getItemCleanEnd(this.left), - this.right === null ? null : y.os.getItemCleanStart(this.right), - this.parent === null ? null : y.os.getItem(this.parent), + this.left === null ? null : getItemCleanEnd(store, transaction, this.left), + this.right === null ? null : getItemCleanStart(store, transaction, this.right), + // @ts-ignore + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type, this.parentSub, - this.content + this.length ) } } diff --git a/src/structs/ItemEmbed.js b/src/structs/ItemEmbed.js index 46ff63d3..533e0f45 100644 --- a/src/structs/ItemEmbed.js +++ b/src/structs/ItemEmbed.js @@ -2,7 +2,8 @@ * @module structs */ -import { AbstractItem, AbstractItemRef, logItemHelper } from './AbstractItem.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { AbstractItem, AbstractItemRef } from './AbstractItem.js' import { ItemType } from './ItemType.js' // eslint-disable-line import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' @@ -18,7 +19,7 @@ export class ItemEmbed extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub * @param {Object} embed */ @@ -30,28 +31,12 @@ export class ItemEmbed extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub */ copy (id, left, right, parent, parentSub) { return new ItemEmbed(id, left, right, parent, parentSub, this.embed) } - /** - * Transform this Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - logString () { - return logItemHelper('ItemEmbed', this) - } - /** - * @type {number} - */ - get _length () { - return 1 - } - /** * @param {encoding.Encoder} encoder */ @@ -78,12 +63,14 @@ export class ItemEmbedRef extends AbstractItemRef { * @return {ItemEmbed} */ toStruct (transaction) { - const store = transaction.y.store + const y = transaction.y + const store = y.store return new ItemEmbed( this.id, this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.right === null ? null : getItemCleanStart(store, transaction, this.right), - this.parent === null ? null : getItemType(store, this.parent), + // @ts-ignore + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent), this.parentSub, this.embed ) diff --git a/src/structs/ItemFormat.js b/src/structs/ItemFormat.js index c7dbe753..67322cc0 100644 --- a/src/structs/ItemFormat.js +++ b/src/structs/ItemFormat.js @@ -2,7 +2,8 @@ * @module structs */ -import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { AbstractItem, AbstractItemRef } from './AbstractItem.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { Y } from '../utils/Y.js' // eslint-disable-line @@ -18,7 +19,7 @@ export class ItemFormat extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub * @param {string} key * @param {any} value @@ -32,25 +33,13 @@ export class ItemFormat extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub */ copy (id, left, right, parent, parentSub) { return new ItemFormat(id, left, right, parent, parentSub, this.key, this.value) } - /** - * Transform this Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - logString () { - return logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`) - } - get _length () { - return 1 - } - get _countable () { + get countable () { return false } /** @@ -81,12 +70,14 @@ export class ItemFormatRef extends AbstractItemRef { * @return {ItemFormat} */ toStruct (transaction) { - const store = transaction.y.store + const y = transaction.y + const store = y.store return new ItemFormat( this.id, this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.right === null ? null : getItemCleanStart(store, transaction, this.right), - this.parent === null ? null : getItemType(store, this.parent), + // @ts-ignore + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent), this.parentSub, this.key, this.value diff --git a/src/structs/ItemJSON.js b/src/structs/ItemJSON.js index 179af5a4..543875b3 100644 --- a/src/structs/ItemJSON.js +++ b/src/structs/ItemJSON.js @@ -2,7 +2,8 @@ * @module structs */ -import { AbstractItem, logItemHelper, AbstractItemRef, splitItem } from './AbstractItem.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { AbstractItem, AbstractItemRef, splitItem } from './AbstractItem.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { Y } from '../utils/Y.js' // eslint-disable-line @@ -18,7 +19,7 @@ export class ItemJSON extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub * @param {Array} content */ @@ -30,32 +31,28 @@ export class ItemJSON extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub */ copy (id, left, right, parent, parentSub) { return new ItemJSON(id, left, right, parent, parentSub, this.content) } - /** - * Transform this Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - logString () { - return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this.content)}`) - } get length () { return this.content.length } + getContent () { + return this.content + } /** + * @param {Transaction} transaction * @param {number} diff */ - splitAt (diff) { + splitAt (transaction, diff) { /** * @type {ItemJSON} */ - const right = splitItem(this, diff) + // @ts-ignore + const right = splitItem(transaction, this, diff) right.content = this.content.splice(diff) return right } @@ -97,12 +94,14 @@ export class ItemJSONRef extends AbstractItemRef { * @return {ItemJSON} */ toStruct (transaction) { - const store = transaction.y.store + const y = transaction.y + const store = y.store return new ItemJSON( this.id, this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.right === null ? null : getItemCleanStart(store, transaction, this.right), - this.parent === null ? null : getItemType(store, this.parent), + // @ts-ignore + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent), this.parentSub, this.content ) diff --git a/src/structs/ItemString.js b/src/structs/ItemString.js index b1b2b9d4..b29e0693 100644 --- a/src/structs/ItemString.js +++ b/src/structs/ItemString.js @@ -1,8 +1,8 @@ /** * @module structs */ - -import { AbstractItem, logItemHelper, AbstractItemRef, splitItem } from './AbstractItem.js' +import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line +import { AbstractItem, AbstractItemRef, splitItem } from './AbstractItem.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { Y } from '../utils/Y.js' // eslint-disable-line @@ -18,48 +18,46 @@ export class ItemString extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub * @param {string} string */ constructor (id, left, right, parent, parentSub, string) { super(id, left, right, parent, parentSub) + /** + * @type {string} + */ this.string = string } /** * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub */ copy (id, left, right, parent, parentSub) { return new ItemString(id, left, right, parent, parentSub, this.string) } - /** - * Transform this Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - logString () { - return logItemHelper('ItemString', this, `content:"${this.string}"`) + getContent () { + return this.string.split('') } get length () { return this.string.length } - splitAt (y, diff) { - if (diff === 0) { - return this - } else if (diff >= this.string.length) { - return this.right - } + /** + * @param {Transaction} transaction + * @param {number} diff + * @return {ItemString} + */ + splitAt (transaction, diff) { /** * @type {ItemString} */ - const right = splitItem(this, y, diff) + // @ts-ignore + const right = splitItem(transaction, this, diff) right.string = this.string.slice(diff) - right.string = this.string.slice(0, diff) + this.string = this.string.slice(0, diff) return right } /** @@ -88,12 +86,14 @@ export class ItemStringRef extends AbstractItemRef { * @return {ItemString} */ toStruct (transaction) { - const store = transaction.y.store + const y = transaction.y + const store = y.store return new ItemString( this.id, this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.right === null ? null : getItemCleanStart(store, transaction, this.right), - this.parent === null ? null : getItemType(store, this.parent), + // @ts-ignore + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent), this.parentSub, this.string ) diff --git a/src/structs/ItemType.js b/src/structs/ItemType.js index 1620d159..40a8316b 100644 --- a/src/structs/ItemType.js +++ b/src/structs/ItemType.js @@ -6,9 +6,8 @@ import { ID } from '../utils/ID.js' // eslint-disable-line import { Y } from '../utils/Y.js' // eslint-disable-line -import { Transaction } from '../utils/Transaction.js' // eslint-disable-line import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line -import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js' +import { AbstractItem, AbstractItemRef } from './AbstractItem.js' import * as encoding from 'lib0/encoding.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' import { readYArray } from '../types/YArray.js' @@ -20,12 +19,14 @@ import { readYXmlText } from '../types/YXmlText.js' import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js' import { Transaction } from '../utils/Transaction.js' // eslint-disable-line - +/** + * @param {Y} y + * @param {AbstractItem | null} item + */ const gcChildren = (y, item) => { while (item !== null) { - item._delete(y, false, true) - item._gc(y) - item = item._right + item.gc(y) + item = item.right } } @@ -49,7 +50,7 @@ export class ItemType extends AbstractItem { * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub * @param {AbstractType} type */ @@ -57,26 +58,20 @@ export class ItemType extends AbstractItem { super(id, left, right, parent, parentSub) this.type = type } + getContent () { + return [this.type] + } /** * @param {ID} id * @param {AbstractItem | null} left * @param {AbstractItem | null} right - * @param {ItemType | null} parent + * @param {AbstractType} parent * @param {string | null} parentSub * @return {AbstractItem} TODO, returns itemtype */ copy (id, left, right, parent, parentSub) { return new ItemType(id, left, right, parent, parentSub, this.type._copy()) } - /** - * Transform this Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - logString () { - return logItemHelper('ItemType', this) - } /** * @param {encoding.Encoder} encoder */ @@ -97,20 +92,20 @@ export class ItemType extends AbstractItem { delete (transaction, createDelete, gcChildren = transaction.y.gcEnabled) { const y = transaction.y super.delete(transaction, createDelete, gcChildren) - transaction.changed.delete(this) + transaction.changed.delete(this.type) // delete map types for (let value of this.type._map.values()) { - if (!value._deleted) { - value._delete(y, false, gcChildren) + if (!value.deleted) { + value.delete(transaction, false, gcChildren) } } // delete array types let t = this.type._start while (t !== null) { - if (!t._deleted) { - t._delete(y, false, gcChildren) + if (!t.deleted) { + t.delete(transaction, false, gcChildren) } - t = t._right + t = t.right } if (gcChildren) { this.gcChildren(y) @@ -156,12 +151,14 @@ export class ItemBinaryRef extends AbstractItemRef { * @return {ItemType} */ toStruct (transaction) { - const store = transaction.y.store + const y = transaction.y + const store = y.store return new ItemType( this.id, this.left === null ? null : getItemCleanEnd(store, transaction, this.left), this.right === null ? null : getItemCleanStart(store, transaction, this.right), - this.parent === null ? null : getItemType(store, this.parent), + // @ts-ignore + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent), this.parentSub, this.type ) diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 7a2f591c..0f3a7069 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -9,6 +9,12 @@ import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line import { ItemType } from '../structs/ItemType.js' // eslint-disable-line import { Encoder } from 'lib0/encoding.js' // eslint-disable-line import { Transaction, nextID } from '../utils/Transaction.js' // eslint-disable-line +import * as map from 'lib0/map.js' +import { isVisible, Snapshot } from '../utils/Snapshot.js' // eslint-disable-line +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' /** * Restructure children as if they were inserted one after another @@ -118,10 +124,13 @@ export class AbstractType { } /** - * Creates YArray Event and calls observers. + * Creates YEvent and calls observers. * @private + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ - _callObserver (transaction, parentSubs, remote) { + _callObserver (transaction, parentSubs) { this._callEventHandler(transaction, new YEvent(this)) } @@ -129,38 +138,23 @@ export class AbstractType { * Call event listeners with an event. This will also add an event to all * parents (for `.observeDeep` handlers). * @private + * + * @param {Transaction} transaction + * @param {any} event */ _callEventHandler (transaction, event) { const changedParentTypes = transaction.changedParentTypes this._eventHandler.callEventListeners(transaction, event) /** - * @type {any} + * @type {AbstractType} */ let type = this - while (type !== this._y) { - let events = changedParentTypes.get(type) - if (events === undefined) { - events = [] - changedParentTypes.set(type, events) + while (true) { + map.setIfUndefined(changedParentTypes, type, () => []).push(event) + if (type._item === null) { + break } - events.push(event) - type = type._parent - } - } - - /** - * Helper method to transact if the y instance is available. - * - * TODO: Currently event handlers are not thrown when a type is not registered - * with a Yjs instance. - * @private - */ - _transact (f) { - const y = this._y - if (y !== null) { - y.transact(f) - } else { - f(y) + type = type._item.parent } } @@ -211,8 +205,19 @@ export class AbstractType { * @param {AbstractType} type * @return {Array} */ -export const typeToArray = type => { - +export const typeArrayToArray = type => { + const cs = [] + let n = type._start + while (n !== null) { + if (n.countable && !n.deleted) { + const c = n.getContent() + for (let i = 0; i < c.length; i++) { + cs.push(c[i]) + } + } + n = n.right + } + return cs } /** @@ -220,18 +225,176 @@ export const typeToArray = type => { * * @param {AbstractType} type * @param {function(any,number,AbstractType):void} f A function to execute on every element of this YArray. - * @param {HistorySnapshot} [snapshot] */ -export const typeForEach = (type, f, snapshot) => { +export const typeArrayForEach = (type, f) => { let index = 0 let n = type._start while (n !== null) { - if (isVisible(n, snapshot) && n._countable) { + if (n.countable && !n.deleted) { const c = n.getContent() for (let i = 0; i < c.length; i++) { f(c[i], index++, type) } } - n = n._right + n = n.right + } +} + +/** + * @param {AbstractType} type + */ +export const typeArrayCreateIterator = type => { + let n = type._start + /** + * @type {Array|null} + */ + let currentContent = null + let currentContentIndex = 0 + return { + next: () => { + // find some content + if (currentContent === null) { + while (n !== null && n.deleted) { + n = n.right + } + } + // check if we reached the end, no need to check currentContent, because it does not exist + if (n === null) { + return { + done: true + } + } + // currentContent could exist from the last iteration + if (currentContent === null) { + // we found n, so we can set currentContent + currentContent = n.getContent() + currentContentIndex = 0 + } + const value = currentContent[currentContentIndex++] + // check if we need to empty currentContent + if (currentContent.length <= currentContentIndex) { + currentContent = null + } + return { + done: false, + value + } + } + } +} + +/** + * Executes a provided function on once on overy element of this YArray. + * Operates on a snapshotted state of the document. + * + * @param {AbstractType} type + * @param {function(any,number,AbstractType):void} f A function to execute on every element of this YArray. + * @param {Snapshot} snapshot + */ +export const typeArrayForEachSnapshot = (type, f, snapshot) => { + let index = 0 + let n = type._start + while (n !== null) { + if (n.countable && isVisible(n, snapshot)) { + const c = n.getContent() + for (let i = 0; i < c.length; i++) { + f(c[i], index++, type) + } + } + n = n.right + } +} + +/** + * @param {AbstractType} type + * @param {number} index + * @return {any} + */ +export const typeArrayGet = (type, index) => { + for (let n = type._start; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index < n.length) { + return n.getContent()[index] + } + index -= n.length + } + } +} + +/** + * @param {Transaction} transaction + * @param {AbstractType} parent + * @param {AbstractItem?} referenceItem + * @param {Array|Array|number|string|ArrayBuffer>} content + */ +export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem, content) => { + let left = referenceItem + const right = referenceItem === null ? parent._start : referenceItem.right + /** + * @type {Array} + */ + let jsonContent = [] + content.forEach(c => { + switch (c.constructor) { + case Object: + case Array: + case String: + jsonContent.push(c) + break + default: + if (jsonContent.length > 0) { + const item = new ItemJSON(nextID(transaction), left, right, parent, null, jsonContent) + item.integrate(transaction) + jsonContent = [] + } + switch (c.constructor) { + case ArrayBuffer: + // @ts-ignore c is definitely an ArrayBuffer + new ItemBinary(nextID(transaction), left, right, parent, null, c).integrate(transaction) + break + default: + if (c instanceof AbstractType) { + new ItemType(nextID(transaction), left, right, parent, null, c).integrate(transaction) + } else { + throw new Error('Unexpected content type in insert operation') + } + } + } + }) +} + +/** + * @param {Transaction} transaction + * @param {AbstractType} parent + * @param {number} index + * @param {Array|Array|number|string|ArrayBuffer>} content + */ +export const typeArrayInsertGenerics = (transaction, parent, index, content) => { + if (index === 0) { + typeArrayInsertGenericsAfter(transaction, parent, null, content) + } + for (let n = parent._start; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index <= n.length) { + if (index < n.length) { + getItemCleanStart(transaction.y.store, transaction, createID(n.id.client, n.id.clock + index)) + } + return typeArrayInsertGenericsAfter(transaction, parent, n, content) + } + index -= n.length + } + } + throw new Error('Index exceeds array range') +} + +/** + * @param {Transaction} transaction + * @param {AbstractType} parent + * @param {string} key + */ +export const typeMapDelete = (transaction, parent, key) => { + const c = parent._map.get(key) + if (c !== undefined) { + c.delete(transaction) } } diff --git a/src/types/YArray.js b/src/types/YArray.js index 3f6e23aa..294b88d2 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -2,27 +2,27 @@ * @module types */ -import { AbstractType } from './AbstractType.js' -import { ItemJSON } from '../structs/ItemJSON.js' -import { ItemString } from '../structs/ItemString.js' -import { ItemBinary } from '../structs/ItemBinary.js' -import { stringifyItemID, logItemHelper } from '../structs/AbstractItem.js' // eslint-disable-line +import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line +import { ItemType } from '../structs/ItemType.js' // eslint-disable-line +import { AbstractType, typeArrayGet, typeArrayToArray, typeArrayForEach, typeArrayCreateIterator, typeArrayInsertGenerics } from './AbstractType.js' import { YEvent } from '../utils/YEvent.js' import { Transaction } from '../utils/Transaction.js' // eslint-disable-line -import { isVisible, HistorySnapshot } from '../utils/snapshot.js' // eslint-disable-line +import { getItemCleanStart, getItemCleanEnd } from '../utils/StructStore.js' +import { createID } from '../utils/ID.js' +import * as decoding from 'lib0/decoding.js' // eslint-disable-line /** * Event that describes the changes on a YArray + * + * @template T */ export class YArrayEvent extends YEvent { /** - * @param {YArray} yarray The changed type - * @param {Boolean} remote Whether the changed was caused by a remote peer + * @param {YArray} yarray The changed type * @param {Transaction} transaction The transaction object */ - constructor (yarray, remote, transaction) { + constructor (yarray, transaction) { super(yarray) - this.remote = remote this._transaction = transaction this._addedElements = null this._removedElements = null @@ -31,7 +31,7 @@ export class YArrayEvent extends YEvent { /** * Child elements that were added in this transaction. * - * @return {Set} + * @return {Set} */ get addedElements () { if (this._addedElements === null) { @@ -39,7 +39,7 @@ export class YArrayEvent extends YEvent { const transaction = this._transaction const addedElements = new Set() transaction.added.forEach(type => { - if (type._parent === target && !transaction.deleted.has(type)) { + if (type.parent === target && !transaction.deleted.has(type)) { addedElements.add(type) } }) @@ -51,7 +51,7 @@ export class YArrayEvent extends YEvent { /** * Child elements that were removed in this transaction. * - * @return {Set} + * @return {Set} */ get removedElements () { if (this._removedElements === null) { @@ -59,7 +59,7 @@ export class YArrayEvent extends YEvent { const transaction = this._transaction const removedElements = new Set() transaction.deleted.forEach(struct => { - if (struct._parent === target && !transaction.added.has(struct)) { + if (struct.parent === target && !transaction.added.has(struct)) { removedElements.add(struct) } }) @@ -71,144 +71,106 @@ export class YArrayEvent extends YEvent { /** * A shared Array implementation. + * @template T */ export class YArray extends AbstractType { constructor () { super() - this.length = 0 + /** + * @type {Array?} + */ + this._prelimContent = [] } /** - * Creates YArray Event and calls observers. + * Integrate this type into the Yjs instance. * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Transaction} transaction The Yjs instance + * @param {ItemType} item * @private */ - _callObserver (transaction, parentSubs, remote) { - this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction)) + _integrate (transaction, item) { + super._integrate(transaction, item) + // @ts-ignore + this.insert(0, this._prelimContent) + this._prelimContent = null + } + get length () { + return this._prelimContent === null ? this._length : this._prelimContent.length + } + /** + * Creates YArrayEvent and calls observers. + * @private + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver (transaction, parentSubs) { + this._callEventHandler(transaction, new YArrayEvent(this, transaction)) } /** * Returns the i-th element from a YArray. * * @param {number} index The index of the element to return from the YArray - * @return {any} + * @return {T} */ get (index) { - let n = this._start - while (n !== null) { - if (!n._deleted && n._countable) { - if (index < n._length) { - switch (n.constructor) { - case ItemJSON: - case ItemString: - return n._content[index] - default: - return n - } - } - index -= n._length - } - n = n._right - } + return typeArrayGet(this, index) } /** * Transforms this YArray to a JavaScript Array. * - * @param {Object} [snapshot] - * @return {Array} + * @return {Array} */ - toArray (snapshot) { - return this.map(c => c, snapshot) + toArray () { + return typeArrayToArray(this) } /** * Transforms this Shared Type to a JSON object. * - * @return {Array} + * @return {Array} */ toJSON () { - return this.map(c => { - if (c instanceof AbstractType) { - return c.toJSON() - } - return c - }) + return this.map(c => c instanceof AbstractType ? c.toJSON() : c) } /** * Returns an Array with the result of calling a provided function on every * element of this YArray. * - * @param {Function} f Function that produces an element of the new Array - * @param {HistorySnapshot} [snapshot] - * @return {Array} A new array with each element being the result of the + * @template 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, snapshot) { - const res = [] + map (f) { + /** + * @type {Array} + */ + const result = [] this.forEach((c, i) => { - res.push(f(c, i, this)) - }, snapshot) - return res + result.push(f(c, i, this)) + }) + return result } /** * Executes a provided function on once on overy element of this YArray. * - * @param {Function} f A function to execute on every element of this YArray. - * @param {HistorySnapshot} [snapshot] + * @param {function(T,number):void} f A function to execute on every element of this YArray. */ - forEach (f, snapshot) { - let index = 0 - let n = this._start - while (n !== null) { - if (isVisible(n, snapshot) && n._countable) { - if (n instanceof Type) { - f(n, index++, this) - } else if (n.constructor === ItemBinary) { - f(n._content, index++, this) - } else { - const content = n._content - const contentLen = content.length - for (let i = 0; i < contentLen; i++) { - index++ - f(content[i], index, this) - } - } - } - n = n._right - } + forEach (f) { + typeArrayForEach(this, f) } [Symbol.iterator] () { - return { - next: function () { - while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) { - // item is deleted or itemElement does not exist (is deleted) - this._item = this._item._right - this._itemElement = 0 - } - if (this._item === null) { - return { - done: true - } - } - let content - if (this._item instanceof Type) { - content = this._item - this._item = this._item._right - } else { - content = this._item._content[this._itemElement++] - } - return { - value: content, - done: false - } - }, - _item: this._start, - _itemElement: 0, - _count: 0 - } + return typeArrayCreateIterator(this) } /** @@ -218,121 +180,37 @@ export class YArray extends AbstractType { * @param {number} length The number of elements to remove. Defaults to 1. */ delete (index, length = 1) { - this._y.transact(() => { - let item = this._start - let count = 0 - while (item !== null && length > 0) { - if (!item._deleted && item._countable) { - if (count <= index && index < count + item._length) { - const diffDel = index - count - item = item._splitAt(this._y, diffDel) - item._splitAt(this._y, length) - length -= item._length - item._delete(this._y) - count += diffDel - } else { - count += item._length + if (this._y !== null) { + this._y.transact(transaction => { + const store = transaction.y.store + let item = this._start + let count = 0 + while (item !== null && length > 0) { + if (!item.deleted && item.countable) { + if (count <= index && index < count + item.length) { + const diffDel = index - count + if (diffDel > 0) { + item = getItemCleanStart(store, transaction, createID(item.id.client, item.id.clock + diffDel)) + } + if (length < item.length) { + getItemCleanEnd(store, transaction, createID(item.id.client, item.id.clock + length)) + } + length -= item.length + item.delete(transaction) + count += diffDel + } else { + count += item.length + } } + item = item.right } - item = item._right - } - }) - if (length > 0) { - throw new Error('Delete exceeds the range of the YArray') + }) + } else { + // @ts-ignore _prelimContent is defined because this is not yet integrated + this._prelimContent.splice(index, length) } } - /** - * Inserts content after an element container. - * - * @private - * @param {Item} left The element container to use as a reference. - * @param {Array} content The Array of content to insert (see {@see insert}) - */ - insertAfter (left, content) { - this._transact(y => { - let right - if (left === null) { - right = this._start - } else { - right = left._right - } - let prevJsonIns = null - for (let i = 0; i < content.length; i++) { - let c = content[i] - if (typeof c === 'function') { - c = new c() // eslint-disable-line new-cap - } - if (c instanceof Type) { - if (prevJsonIns !== null) { - if (y !== null) { - prevJsonIns._integrate(y) - } - left = prevJsonIns - prevJsonIns = null - } - c._origin = left - c._left = left - c._right = right - c._right_origin = right - c._parent = this - if (y !== null) { - c._integrate(y) - } else if (left === null) { - this._start = c - } else { - left._right = c - } - left = c - } else if (c.constructor === ArrayBuffer) { - if (prevJsonIns !== null) { - if (y !== null) { - prevJsonIns._integrate(y) - } - left = prevJsonIns - prevJsonIns = null - } - const itemBinary = new ItemBinary() - itemBinary._origin = left - itemBinary._left = left - itemBinary._right = right - itemBinary._right_origin = right - itemBinary._parent = this - itemBinary._content = c - if (y !== null) { - itemBinary._integrate(y) - } else if (left === null) { - this._start = itemBinary - } else { - left._right = itemBinary - } - left = itemBinary - } else { - if (prevJsonIns === null) { - prevJsonIns = new ItemJSON() - prevJsonIns._origin = left - prevJsonIns._left = left - prevJsonIns._right = right - prevJsonIns._right_origin = right - prevJsonIns._parent = this - prevJsonIns._content = [] - } - prevJsonIns._content.push(c) - } - } - if (prevJsonIns !== null) { - if (y !== null) { - prevJsonIns._integrate(y) - } else if (prevJsonIns._left === null) { - this._start = prevJsonIns - } else { - left._right = prevJsonIns - } - } - }) - return content - } - /** * Inserts new content at an index. * @@ -347,62 +225,30 @@ export class YArray extends AbstractType { * yarray.insert(2, [1, 2]) * * @param {number} index The index to insert content at. - * @param {Array} content The array of content + * @param {Array} content The array of content */ insert (index, content) { - this._transact(() => { - let left = null - let right = this._start - let count = 0 - const y = this._y - while (right !== null) { - const rightLen = right._deleted ? 0 : (right._length - 1) - if (count <= index && index <= count + rightLen) { - const splitDiff = index - count - right = right._splitAt(y, splitDiff) - left = right._left - count += splitDiff - break - } - if (!right._deleted) { - count += right._length - } - left = right - right = right._right - } - if (index > count) { - throw new Error('Index exceeds array range!') - } - this.insertAfter(left, content) - }) + if (this._y !== null) { + this._y.transact(transaction => { + typeArrayInsertGenerics(transaction, this, index, content) + }) + } else { + // @ts-ignore _prelimContent is defined because this is not yet integrated + this._prelimContent.splice(index, 0, ...content) + } } /** * Appends content to this YArray. * - * @param {Array} content Array of content to append. + * @param {Array} content Array of content to append. */ push (content) { - let n = this._start - let lastUndeleted = null - while (n !== null) { - if (!n._deleted) { - lastUndeleted = n - } - n = n._right - } - this.insertAfter(lastUndeleted, content) - } - - /** - * Transform this YXml Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - _logString () { - return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`) + this.insert(this.length, content) } } -export const readYArray = decoder => new YArray() \ No newline at end of file +/** + * @param {decoding.Decoder} decoder + */ +export const readYArray = decoder => new YArray() diff --git a/src/types/YMap.js b/src/types/YMap.js index c411a084..4f4b7e72 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -2,11 +2,37 @@ * @module types */ -import { AbstractType } from './AbstractType.js' +import { AbstractType, typeMapDelete } from './AbstractType.js' import { ItemJSON } from '../structs/ItemJSON.js' +import { ItemType } from '../structs/ItemType.js' // eslint-disable-line import { YEvent } from '../utils/YEvent.js' import { ItemBinary } from '../structs/ItemBinary.js' -import { HistorySnapshot, isVisible } from '../utils/snapshot.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 + } + } +} /** * Event that describes the changes on a YMap. @@ -15,12 +41,10 @@ export class YMapEvent extends YEvent { /** * @param {YMap} ymap The YArray that changed. * @param {Set} subs The keys that changed. - * @param {boolean} remote Whether the change was created by a remote peer. */ - constructor (ymap, subs, remote) { + constructor (ymap, subs) { super(ymap) this.keysChanged = subs - this.remote = remote } } @@ -28,37 +52,56 @@ export class YMapEvent extends YEvent { * A shared Map implementation. */ export class YMap extends AbstractType { + constructor () { + super() + /** + * @type {Map?} + */ + this._prelimContent = new Map() + } /** - * Creates YMap Event and calls observers. + * Integrate this type into the Yjs instance. * + * * Save this struct in the os + * * This type is sent to other client + * * Observer functions are fired + * + * @param {Transaction} transaction The Yjs instance + * @param {ItemType} item * @private */ - _callObserver (transaction, parentSubs, remote) { - this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote)) + _integrate (transaction, item) { + super._integrate(transaction, item) + // @ts-ignore + for (let [key, value] of this._prelimContent) { + this.set(key, value) + } + this._prelimContent = null + } + /** + * Creates YMapEvent and calls observers. + * @private + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver (transaction, parentSubs) { + this._callEventHandler(transaction, new YMapEvent(this, parentSubs)) } /** * Transforms this Shared Type to a JSON object. * - * @return {Object} + * @return {Object} */ toJSON () { + /** + * @type {Object} + */ const map = {} for (let [key, item] of this._map) { - if (!item._deleted) { - let res - if (item instanceof Type) { - if (item.toJSON !== undefined) { - res = item.toJSON() - } else { - res = item.toString() - } - } else if (item.constructor === ItemBinary) { - res = item._content - } else { - res = item._content[0] - } - map[key] = res + if (!item.deleted) { + map[key] = item.getContent()[0] } } return map @@ -67,26 +110,30 @@ export class YMap extends AbstractType { /** * Returns the keys for each element in the YMap Type. * - * @param {HistorySnapshot} [snapshot] - * @return {Array} + * @return {YMapIterator} */ - keys (snapshot) { - // TODO: Should return either Iterator or Set! - let keys = [] - if (snapshot === undefined) { - for (let [key, value] of this._map) { - if (value._deleted) { - keys.push(key) - } + keys () { + const keys = [] + for (let [key, value] of this._map) { + if (value.deleted) { + keys.push(key) } - } else { - this._map.forEach((_, key) => { - if (YMap.prototype.has.call(this, key, snapshot)) { - keys.push(key) - } - }) } - return keys + return new YMapIterator(keys) + } + + entries () { + const entries = [] + for (let [key, value] of this._map) { + if (value.deleted) { + entries.push([key, value.getContent()[0]]) + } + } + return new YMapIterator(entries) + } + + [Symbol.iterator] () { + return this.entries() } /** @@ -95,19 +142,21 @@ export class YMap extends AbstractType { * @param {string} key The key of the element to remove. */ delete (key) { - this._transact((y) => { - let c = this._map.get(key) - if (y !== null && c !== undefined) { - c._delete(y) - } - }) + if (this._y !== null) { + this._y.transact(transaction => { + typeMapDelete(transaction, this, key) + }) + } else { + // @ts-ignore + this._prelimContent.delete(key) + } } /** * 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 | Type | ArrayBuffer } value The value of the element to add + * @param {Object | string | number | AbstractType | ArrayBuffer } value The value of the element to add */ set (key, value) { this._transact(y => { @@ -198,16 +247,6 @@ export class YMap extends AbstractType { } return isVisible(v, snapshot) } - - /** - * Transform this YXml Type to a readable format. - * Useful for logging as all Items and Delete implement this method. - * - * @private - */ - _logString () { - return logItemHelper('YMap', this, `mapSize:${this._map.size}`) - } } export const readYMap = decoder => new YMap() \ No newline at end of file diff --git a/src/types/YText.js b/src/types/YText.js index 5bbf5250..5813c43d 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -7,25 +7,7 @@ import { ItemEmbed } from '../structs/ItemEmbed.js' import { ItemString } from '../structs/ItemString.js' import { ItemFormat } from '../structs/ItemFormat.js' import { YArrayEvent, YArray } from './YArray.js' -import { isVisible } from '../utils/snapshot.js' - -/** - * @private - */ -const integrateItem = (item, parent, y, left, right) => { - item._origin = left - item._left = left - item._right = right - item._right_origin = right - item._parent = parent - if (y !== null) { - item._integrate(y) - } else if (left === null) { - parent._start = item - } else { - left._right = item - } -} +import { isVisible } from '../utils/Snapshot.js' /** * @private @@ -478,12 +460,14 @@ export class YText extends YArray { } /** - * Creates YMap Event and calls observers. - * + * Creates YTextEvent and calls observers. * @private + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ - _callObserver (transaction, parentSubs, remote) { - this._callEventHandler(transaction, new YTextEvent(this, remote, transaction)) + _callObserver (transaction, parentSubs) { + this._callEventHandler(transaction, new YTextEvent(this, transaction)) } toDom () { diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js index a5a50799..953dd3c0 100644 --- a/src/types/YXmlElement.js +++ b/src/types/YXmlElement.js @@ -2,7 +2,8 @@ * @module types */ -import { logItemHelper } from '../structs/AbstractItem.js/index.js' +import { Transaction } from '../utils/Transaction.js' // eslint-disable-line +import { logItemHelper } from '../structs/AbstractItem.js' import { YMap } from './YMap.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' @@ -22,6 +23,15 @@ import { YXmlEvent } from './YXmlEvent.js' * @typedef {string} CSS_Selector */ +/** + * Dom filter function. + * + * @callback domFilter + * @param {string} nodeName The nodeName of the element + * @param {Map} attributes The map of attributes. + * @return {boolean} Whether to include the Dom node in the YXmlElement. + */ + /** * Represents a subset of the nodes of a YXmlElement / YXmlFragment and a * position within them. @@ -31,9 +41,16 @@ import { YXmlEvent } from './YXmlEvent.js' * @public */ export class YXmlTreeWalker { + /** + * @param {YXmlFragment | YXmlElement} root + * @param {function} f + */ constructor (root, f) { this._filter = f || (() => true) this._root = root + /** + * @type {YXmlFragment | YXmlElement} + */ this._currentNode = root this._firstCall = true } @@ -85,29 +102,6 @@ export class YXmlTreeWalker { } } -/** - * Dom filter function. - * - * @callback domFilter - * @param {string} nodeName The nodeName of the element - * @param {Map} attributes The map of attributes. - * @return {boolean} Whether to include the Dom node in the YXmlElement. - */ - -/** - * Define the elements to which a set of CSS queries apply. - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors} - * - * @example - * query = '.classSelector' - * query = 'nodeSelector' - * query = '#idSelector' - * - * @typedef {string} CSS_Selector - *//** - * @module types - */ - /** * Represents a list of {@link YXmlElement}.and {@link YXmlText} types. * A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a @@ -181,12 +175,14 @@ export class YXmlFragment extends YArray { } /** - * Creates YArray Event and calls observers. - * + * Creates YXmlEvent and calls observers. * @private + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ - _callObserver (transaction, parentSubs, remote) { - this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote, transaction)) + _callObserver (transaction, parentSubs) { + this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, transaction)) } toString () { @@ -302,13 +298,13 @@ export class YXmlElement extends YXmlFragment { * * Sets domFilter * * @private - * @param {Y} y The Yjs instance + * @param {Transaction} transaction The Yjs instance */ - _integrate (y) { + _integrate (transaction) { if (this.nodeName === null) { throw new Error('nodeName must be defined!') } - super._integrate(y) + super._integrate(transaction) } toString () { diff --git a/src/types/YXmlEvent.js b/src/types/YXmlEvent.js index 54f9589d..db871ac9 100644 --- a/src/types/YXmlEvent.js +++ b/src/types/YXmlEvent.js @@ -15,7 +15,7 @@ import { Transaction } from '../utils/Transaction.js' // eslint-disable-line export class YXmlEvent extends YEvent { /** * @param {Type} target The target on which the event is created. - * @param {Set} subs The set of changed attributes. `null` is included if the + * @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 @@ -35,7 +35,7 @@ export class YXmlEvent extends YEvent { this.childListChanged = false /** * Set of all changed attributes. - * @type {Set} + * @type {Set} */ this.attributesChanged = new Set() /** diff --git a/src/types/YXmlHook.js b/src/types/YXmlHook.js index fcc3de3f..75b38e15 100644 --- a/src/types/YXmlHook.js +++ b/src/types/YXmlHook.js @@ -5,7 +5,6 @@ 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 /** * You can manage binding to a custom type with YXmlHook. @@ -14,14 +13,11 @@ import { Y } from '../utils/Y.js' // eslint-disable-line */ export class YXmlHook extends YMap { /** - * @param {String} [hookName] nodeName of the Dom Node. + * @param {string} hookName nodeName of the Dom Node. */ constructor (hookName) { super() - this.hookName = null - if (hookName !== undefined) { - this.hookName = hookName - } + this.hookName = hookName } /** @@ -30,9 +26,7 @@ export class YXmlHook extends YMap { * @private */ _copy () { - const struct = super._copy() - struct.hookName = this.hookName - return struct + return new YXmlHook(this.hookName) } /** @@ -43,7 +37,7 @@ export class YXmlHook extends YMap { * 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} @@ -65,22 +59,6 @@ export class YXmlHook extends YMap { return dom } - /** - * Read the next Item in a Decoder and fill this Item with the read data. - * - * This is called when data is received from a remote peer. - * - * @param {Y} y The Yjs instance that this Item belongs to. - * @param {decoding.Decoder} decoder The decoder object to read data from. - * - * @private - */ - _fromBinary (y, decoder) { - const missing = super._fromBinary(y, decoder) - this.hookName = decoding.readVarString(decoder) - return missing - } - /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. @@ -91,28 +69,15 @@ export class YXmlHook extends YMap { * * @private */ - _toBinary (encoder) { - super._toBinary(encoder) + _write (encoder) { + super._write(encoder) encoding.writeVarString(encoder, this.hookName) } - - /** - * Integrate this type into the Yjs instance. - * - * * Save this struct in the os - * * This type is sent to other client - * * Observer functions are fired - * - * @param {Y} y The Yjs instance - * - * @private - */ - _integrate (y) { - if (this.hookName === null) { - throw new Error('hookName must be defined!') - } - super._integrate(y) - } } -export const readYXmlHook = decoder => new YXmlHook() \ No newline at end of file +/** + * @param {decoding.Decoder} decoder + * @return {YXmlHook} + */ +export const readYXmlHook = decoder => + new YXmlHook(decoding.readVarString(decoder)) diff --git a/src/utils/DeleteSet.js b/src/utils/DeleteSet.js index e1b6aa21..bf33e84f 100644 --- a/src/utils/DeleteSet.js +++ b/src/utils/DeleteSet.js @@ -3,6 +3,8 @@ import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' import { StructStore, getItemRange } from './StructStore.js' // eslint-disable-line import { Transaction } from './Transaction.js' // eslint-disable-line +import * as error from 'lib0/error.js' +import { ID } from './ID.js' class DeleteItem { /** @@ -38,6 +40,15 @@ export class DeleteSet { } } +/** + * @param {DeleteSet} ds + * @param {ID} id + * @return {boolean} + */ +export const isDeleted = (ds, id) => { + +} + /** * @param {DeleteSet} ds */ @@ -70,7 +81,7 @@ export const sortAndMergeDeleteSet = ds => { export const createDeleteSetFromTransaction = transaction => { const ds = new DeleteSet() transaction.deleted.forEach(item => { - map.setTfUndefined(ds.clients, item.id.client, () => []).push(new DeleteItem(item.id.clock, item.length)) + map.setIfUndefined(ds.clients, item.id.client, () => []).push(new DeleteItem(item.id.clock, item.length)) }) sortAndMergeDeleteSet(ds) return ds diff --git a/src/utils/Snapshot.js b/src/utils/Snapshot.js new file mode 100644 index 00000000..8419cf07 --- /dev/null +++ b/src/utils/Snapshot.js @@ -0,0 +1,23 @@ +import { DeleteSet, isDeleted } from './DeleteSet' +import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line + +export class Snapshot { + /** + * @param {DeleteSet} ds delete store + * @param {Map} sm state map + * @param {Map} userMap + */ + constructor (ds, sm, userMap) { + this.ds = new DeleteSet() + this.sm = sm + this.userMap = userMap + } +} + +/** + * @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) +) diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js index cefc4c7b..45ef22e3 100644 --- a/src/utils/StructStore.js +++ b/src/utils/StructStore.js @@ -31,6 +31,19 @@ export const getStates = store => } }) +/** + * @param {StructStore} store + * @param {number} client + */ +export const getState = (store, client) => { + const structs = store.clients.get(client) + if (structs === undefined) { + return 0 + } + const lastStruct = structs[structs.length - 1] + return lastStruct.id.clock + lastStruct.length +} + /** * @param {StructStore} store */ @@ -51,7 +64,7 @@ export const integretyCheck = store => { * @param {AbstractStruct} struct */ export const addStruct = (store, struct) => { - map.setTfUndefined(store.clients, struct.id.client, () => []).push(struct) + map.setIfUndefined(store.clients, struct.id.client, () => []).push(struct) } /** @@ -107,21 +120,6 @@ const find = (store, id) => { // @ts-ignore export const getItemType = (store, id) => find(store, id) -/** - * @param {Transaction} transaction - * @param {AbstractItem} struct - * @param {number} diff - */ -const splitStruct = (transaction, struct, diff) => { - const right = struct.splitAt(diff) - if (transaction.added.has(struct)) { - transaction.added.add(right) - } else if (transaction.deleted.has(struct)) { - transaction.deleted.add(right) - } - return right -} - /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * @param {StructStore} store @@ -139,10 +137,11 @@ export const getItemCleanStart = (store, transaction, id) => { const structs = store.clients.get(id.client) const index = findIndex(structs, id.clock) /** - * @type {any} + * @type {AbstractItem} */ let struct = structs[index] if (struct.id.clock < id.clock) { + struct.splitAt() struct = splitStruct(transaction, struct, id.clock - struct.id.clock) structs.splice(index, 0, struct) } @@ -213,11 +212,11 @@ export const getItemRange = (store, transaction, client, clock, len) => { * @param {AbstractStruct} struct * @param {AbstractStruct} newStruct */ -export const replace = (store, struct, newStruct) => { +export const replaceStruct = (store, struct, newStruct) => { /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(struct.id.client) - structs[findIndex(structs, struct.id)] = newStruct + structs[findIndex(structs, struct.id.clock)] = newStruct } diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 49f3b8f8..264e60ad 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -8,8 +8,10 @@ import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line import { Y } from './Y.js' // eslint-disable-line import { YEvent } from './YEvent.js' // eslint-disable-line import { ItemType } from '../structs/ItemType.js' // eslint-disable-line -import { getState } from './StructStore.js' +import { writeStructsFromTransaction } from './structEncoding.js' import { createID } from './ID.js' // eslint-disable-line +import { createDeleteSetFromTransaction, writeDeleteSet } from './DeleteSet.js' +import { getState } from './StructStore.js' /** * A transaction is created for every change on the Yjs model. It is possible @@ -54,26 +56,41 @@ export class Transaction { */ this.deleted = new Set() /** - * Saves the old state set of the Yjs instance. If a state was modified, - * the original value is saved here. + * If a state was modified, the original value is saved here. + * Use `stateUpdates` to compute the original state before the transaction, + * or to compute the set of inserted operations. * @type {Map} */ - this.beforeState = new Map() + this.stateUpdates = new Map() /** * All types that were directly modified (property added or child * inserted/deleted). New types are not included in this Set. * Maps from type to parentSubs (`item._parentSub = null` for YArray) - * @type {Map>} + * @type {Map>} */ this.changed = new Map() /** * Stores the events for the types that observe also child elements. * It is mainly used by `observeDeep`. - * @type {Map>} + * @type {Map>} */ this.changedParentTypes = new Map() - this.encodedStructsLen = 0 - this.encodedStructs = encoding.createEncoder() + /** + * @type {encoding.Encoder|null} + */ + this._updateMessage = null + } + /** + * @type {encoding.Encoder} + */ + get updateMessage () { + if (this._updateMessage === null) { + const encoder = encoding.createEncoder() + writeStructsFromTransaction(encoder, this) + writeDeleteSet(encoder, createDeleteSetFromTransaction(this)) + this._updateMessage = encoder + } + return this._updateMessage } } diff --git a/src/utils/Y.js b/src/utils/Y.js index 80e5797a..e5ce347b 100644 --- a/src/utils/Y.js +++ b/src/utils/Y.js @@ -1,6 +1,3 @@ -import { DeleteStore } from './DeleteSet.js/index.js' // TODO: remove -import { OperationStore } from './OperationStore.js' -import { StateStore } from './StateStore.js' import { StructStore } from './StructStore.js' import * as random from 'lib0/random.js' import * as map from 'lib0/map.js' @@ -8,22 +5,10 @@ import { Observable } from 'lib0/observable.js' import { Transaction } from './Transaction.js' import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' // eslint-disable-line import { AbstractType } from '../types/AbstractType.js' -import { YArray } from '../types/YArray.js' - -/** - * Anything that can be encoded with `JSON.stringify` and can be decoded with - * `JSON.parse`. - * - * The following property should hold: - * `JSON.parse(JSON.stringify(key))===key` - * - * At the moment the only safe values are number and string. - * - * @typedef {(number|string|Object)} encodable - */ /** * A Yjs instance handles the state of shared data. + * @extends Observable */ export class Y extends Observable { /** @@ -33,6 +18,9 @@ export class Y extends Observable { super() this.gcEnabled = conf.gc || false this.clientID = random.uint32() + /** + * @type {Map} + */ this.share = new Map() this.store = new StructStore() /** @@ -65,7 +53,7 @@ export class Y extends Observable { * that happened inside of the transaction are sent as one message to the * other peers. * - * @param {Function} f The function that should be executed as a transaction + * @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. @@ -78,7 +66,7 @@ export class Y extends Observable { this.emit('beforeTransaction', [this, this._transaction, remote]) } try { - f(this) + f(this._transaction) } catch (e) { console.error(e) } @@ -88,7 +76,7 @@ export class Y extends Observable { this._transaction = null // emit change events on changed types transaction.changed.forEach((subs, itemtype) => { - if (!itemtype._deleted) { + if (!itemtype._item.deleted) { itemtype.type._callObserver(transaction, subs, remote) } }) diff --git a/src/utils/snapshot.js b/src/utils/snapshot.js deleted file mode 100644 index e7c7e6ac..00000000 --- a/src/utils/snapshot.js +++ /dev/null @@ -1,22 +0,0 @@ -import { DeleteStore } from './DeleteSet' - -export class HistorySnapshot { - /** - * @param {DeleteStore} ds delete store - * @param {Map} sm state map - * @param {Map} userMap - */ - constructor (ds, sm, userMap) { - this.ds = new DeleteStore() - this.sm = sm - this.userMap = userMap - } -} - -/** - * @param {Item} item - * @param {HistorySnapshot} [snapshot] - */ -export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : ( - snapshot.sm.has(item._id.user) && (snapshot.sm.get(item._id.user) || 0) > item._id.clock && !snapshot.ds.isDeleted(item._id) -) diff --git a/src/utils/structEncoding.js b/src/utils/structEncoding.js index 4ff92f5e..09586864 100644 --- a/src/utils/structEncoding.js +++ b/src/utils/structEncoding.js @@ -1,13 +1,10 @@ import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' -import { getStructReference } from './structReferences.js' +import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' import { ID, createID, writeID, writeNullID } from './ID.js' import * as binary from 'lib0/binary.js' - -export const writeStructToTransaction = (transaction, struct) => { - transaction.encodedStructsLen++ - struct._toBinary(transaction.encodedStructs) -} +import { Transaction } from './Transaction.js' +import { findIndex } from './StructStore.js' const structRefs = [ ItemBinaryRef @@ -18,7 +15,6 @@ const structRefs = [ * * This is called when data is received from a remote peer. * - * @param {Y} y The Yjs instance that this Item belongs to. * @param {decoding.Decoder} decoder The decoder object to read data from. * @return {AbstractRef} * @@ -28,3 +24,23 @@ 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) + } + }) +}