From e56899a02c7c7e826a7d45a604214a9b0a41a69b Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Fri, 5 Apr 2019 00:37:09 +0200 Subject: [PATCH] after refactor - some tests are working again --- rollup.config.js | 5 ++- src/structs/AbstractItem.js | 41 ++++---------------- src/structs/AbstractStruct.js | 13 ++++--- src/structs/GC.js | 18 +++++---- src/structs/ItemDeleted.js | 15 ++++++-- src/structs/ItemEmbed.js | 2 +- src/structs/ItemFormat.js | 2 +- src/structs/ItemJSON.js | 5 ++- src/structs/ItemString.js | 5 ++- src/structs/ItemType.js | 14 ++++++- src/types/AbstractType.js | 42 +++++++++++--------- src/types/YArray.js | 17 +++++++-- src/types/YMap.js | 20 +++++++--- src/types/YText.js | 17 +++++++-- src/types/YXmlElement.js | 36 +++++++++--------- src/types/YXmlHook.js | 6 ++- src/types/YXmlText.js | 9 ++++- src/utils/StructStore.js | 3 ++ src/utils/Transaction.js | 9 +++-- src/utils/Y.js | 72 ++++++++++++++++++++--------------- src/utils/YEvent.js | 2 +- src/utils/structEncoding.js | 31 +++++++++++---- tests/testHelper.js | 2 +- 23 files changed, 234 insertions(+), 152 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index d9af5846..5077c554 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -85,7 +85,8 @@ export default [{ }), commonjs() ] -}, { +} +/* { input: ['./examples/codemirror.js', './examples/textarea.js', './examples/quill.js', './examples/dom.js', './examples/prosemirror.js'], // fs.readdirSync('./examples').filter(file => /(? './examples/' + file), output: { dir: 'examples/build', @@ -102,4 +103,4 @@ export default [{ commonjs(), ...minificationPlugins ] -}] +} */] diff --git a/src/structs/AbstractItem.js b/src/structs/AbstractItem.js index ac920482..27c00388 100644 --- a/src/structs/AbstractItem.js +++ b/src/structs/AbstractItem.js @@ -11,6 +11,7 @@ import { addStruct, addToDeleteSet, ItemDeleted, + findRootTypeKey, ID, AbstractType, Y, Transaction // eslint-disable-line } from '../internals.js' @@ -229,7 +230,7 @@ export class AbstractItem extends AbstractStruct { maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub) } // @ts-ignore - if (parent._item.deleted || (left !== null && parentSub !== null)) { + if ((parent._item !== null && parent._item.deleted) || (left !== null && parentSub !== null)) { // delete if parent is deleted or if this is not the current attribute value of parent this.delete(transaction) } else if (parentSub !== null && left === null && right !== null) { @@ -443,16 +444,12 @@ export class AbstractItem extends AbstractStruct { const info = (encodingRef & binary.BITS5) | ((this.origin === null) ? 0 : binary.BIT8) | // origin is defined ((this.rightOrigin === null) ? 0 : binary.BIT7) | // right origin is defined - ((this.parentSub !== null) ? 0 : binary.BIT6) // parentSub is non-null + ((this.parentSub === null) ? 0 : binary.BIT6) // parentSub is non-null encoding.writeUint8(encoder, info) - if (offset === 0) { - writeID(encoder, this.id) - if (this.origin !== null) { + if (this.origin !== null) { + if (offset === 0) { writeID(encoder, this.origin.lastId) - } - } else { - writeID(encoder, createID(this.id.client, this.id.clock + offset)) - if (this.origin !== null) { + } else { writeID(encoder, createID(this.id.client, this.id.clock + offset - 1)) } } @@ -465,17 +462,7 @@ export class AbstractItem extends AbstractStruct { // 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() - } + const ykey = findRootTypeKey(this.parent) encoding.writeVarUint(encoder, 1) // write parentYKey encoding.writeVarString(encoder, ykey) } else { @@ -509,7 +496,7 @@ export class AbstractItemRef extends AbstractRef { */ this.right = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0 - const hasParentYKey = decoding.readVarUint(decoder) === 1 + const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false /** * 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. @@ -541,16 +528,4 @@ export class AbstractItemRef extends AbstractRef { missing.push(this.parent) } } - /** - * @param {Transaction} transaction - * @return {Array} - */ - getMissing (transaction) { - return [ - createID(this.id.client, this.id.clock - 1), - this.left, - this.right, - this.parent - ] - } } diff --git a/src/structs/AbstractStruct.js b/src/structs/AbstractStruct.js index 860b92f7..6ae75d93 100644 --- a/src/structs/AbstractStruct.js +++ b/src/structs/AbstractStruct.js @@ -18,6 +18,7 @@ export class AbstractStruct { * @readonly */ this.id = id + this.deleted = false } /** * Merge this struct with the item to the right. @@ -35,12 +36,6 @@ export class AbstractStruct { get length () { throw error.methodUnimplemented() } - /** - * @type {boolean} - */ - get deleted () { - throw error.methodUnimplemented() - } /** * @param {encoding.Encoder} encoder The encoder to write data to. * @param {number} offset @@ -87,4 +82,10 @@ export class AbstractRef { toStruct (transaction) { throw error.methodUnimplemented() } + /** + * @type {number} + */ + get length () { + return 1 + } } diff --git a/src/structs/GC.js b/src/structs/GC.js index 06d42be5..71cb597f 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -25,11 +25,12 @@ export class GC extends AbstractStruct { /** * @type {number} */ - this.length = length + this._len = length + this.deleted = true } - get deleted () { - return true + get length () { + return this._len } /** @@ -37,7 +38,7 @@ export class GC extends AbstractStruct { * @return {boolean} */ mergeWith (right) { - this.length += right.length + this._len += right.length return true } @@ -52,7 +53,7 @@ export class GC extends AbstractStruct { } else { writeID(encoder, createID(this.id.client, this.id.clock + offset)) } - encoding.writeVarUint(encoder, this.length) + encoding.writeVarUint(encoder, this._len) } } @@ -71,7 +72,10 @@ export class GCRef extends AbstractRef { /** * @type {number} */ - this.length = decoding.readVarUint(decoder) + this._len = decoding.readVarUint(decoder) + } + get length () { + return this._len } missing () { return [ @@ -84,7 +88,7 @@ export class GCRef extends AbstractRef { toStruct () { return new GC( this.id, - this.length + this._len ) } } diff --git a/src/structs/ItemDeleted.js b/src/structs/ItemDeleted.js index 0c4a93a2..069a249f 100644 --- a/src/structs/ItemDeleted.js +++ b/src/structs/ItemDeleted.js @@ -29,7 +29,11 @@ export class ItemDeleted extends AbstractItem { */ constructor (id, left, right, parent, parentSub, length) { super(id, left, right, parent, parentSub) - this.length = length + this._len = length + this.deleted = true + } + get length () { + return this._len } /** * @param {ID} id @@ -47,7 +51,7 @@ export class ItemDeleted extends AbstractItem { */ mergeWith (right) { if (right.origin === this && this.right === right) { - this.length += right.length + this._len += right.length return true } return false @@ -73,7 +77,10 @@ export class ItemDeletedRef extends AbstractItemRef { /** * @type {number} */ - this.length = decoding.readVarUint(decoder) + this.len = decoding.readVarUint(decoder) + } + get length () { + return this.len } /** * @param {Transaction} transaction @@ -89,7 +96,7 @@ export class ItemDeletedRef extends AbstractItemRef { // @ts-ignore this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type, this.parentSub, - this.length + this.len ) } } diff --git a/src/structs/ItemEmbed.js b/src/structs/ItemEmbed.js index 27c47dd2..ceb25932 100644 --- a/src/structs/ItemEmbed.js +++ b/src/structs/ItemEmbed.js @@ -74,7 +74,7 @@ export class ItemEmbedRef extends AbstractItemRef { 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), + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type, this.parentSub, this.embed ) diff --git a/src/structs/ItemFormat.js b/src/structs/ItemFormat.js index 8747146c..a250e573 100644 --- a/src/structs/ItemFormat.js +++ b/src/structs/ItemFormat.js @@ -81,7 +81,7 @@ export class ItemFormatRef extends AbstractItemRef { 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), + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type, this.parentSub, this.key, this.value diff --git a/src/structs/ItemJSON.js b/src/structs/ItemJSON.js index 21e041f3..96807aa1 100644 --- a/src/structs/ItemJSON.js +++ b/src/structs/ItemJSON.js @@ -108,6 +108,9 @@ export class ItemJSONRef extends AbstractItemRef { } this.content = cs } + get length () { + return this.content.length + } /** * @param {Transaction} transaction * @return {ItemJSON} @@ -120,7 +123,7 @@ export class ItemJSONRef extends AbstractItemRef { 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), + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type, this.parentSub, this.content ) diff --git a/src/structs/ItemString.js b/src/structs/ItemString.js index 66785ac8..5217f0a5 100644 --- a/src/structs/ItemString.js +++ b/src/structs/ItemString.js @@ -97,6 +97,9 @@ export class ItemStringRef extends AbstractItemRef { */ this.string = decoding.readVarString(decoder) } + get length () { + return this.string.length + } /** * @param {Transaction} transaction * @return {ItemString} @@ -109,7 +112,7 @@ export class ItemStringRef extends AbstractItemRef { 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), + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type, this.parentSub, this.string ) diff --git a/src/structs/ItemType.js b/src/structs/ItemType.js index 906dd3db..915fe089 100644 --- a/src/structs/ItemType.js +++ b/src/structs/ItemType.js @@ -49,6 +49,14 @@ export const typeRefs = [ readYXmlText ] +export const YArrayRefID = 0 +export const YMapRefID = 1 +export const YTextRefID = 2 +export const YXmlElementRefID = 3 +export const YXmlFragmentRefID = 4 +export const YXmlHookRefID = 5 +export const YXmlTextRefID = 6 + export class ItemType extends AbstractItem { /** * @param {ID} id @@ -62,6 +70,7 @@ export class ItemType extends AbstractItem { super(id, left, right, parent, parentSub) this.type = type } + getContent () { return [this.type] } @@ -80,7 +89,8 @@ export class ItemType extends AbstractItem { * @param {Transaction} transaction */ integrate (transaction) { - this.type._integrate(transaction, this) + this.type._integrate(transaction.y, this) + super.integrate(transaction) } /** * @param {encoding.Encoder} encoder @@ -168,7 +178,7 @@ export class ItemTypeRef extends AbstractItemRef { 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), + this.parent === null ? y.get(this.parentYKey) : getItemType(store, this.parent).type, this.parentSub, this.type ) diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index e57231ef..a3342fbd 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -68,12 +68,12 @@ export class AbstractType { * * This type is sent to other client * * Observer functions are fired * - * @param {Transaction} transaction The Yjs instance - * @param {ItemType} item + * @param {Y} y The Yjs instance + * @param {ItemType|null} item * @private */ - _integrate (transaction, item) { - this._y = transaction.y + _integrate (y, item) { + this._y = y this._item = item } @@ -87,9 +87,7 @@ export class AbstractType { /** * @param {encoding.Encoder} encoder */ - _write (encoder) { - throw new Error('unimplemented') - } + _write (encoder) { } /** * The first non-deleted item @@ -329,12 +327,19 @@ export const typeArrayGet = (type, index) => { * @param {Array|Array|number|string|ArrayBuffer>} content */ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem, content) => { - let left = referenceItem + const left = referenceItem const right = referenceItem === null ? parent._start : referenceItem.right /** * @type {Array} */ let jsonContent = [] + const packJsonContent = () => { + if (jsonContent.length > 0) { + const item = new ItemJSON(nextID(transaction), left, right, parent, null, jsonContent) + item.integrate(transaction) + jsonContent = [] + } + } content.forEach(c => { switch (c.constructor) { case Number: @@ -344,11 +349,7 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem, jsonContent.push(c) break default: - if (jsonContent.length > 0) { - const item = new ItemJSON(nextID(transaction), left, right, parent, null, jsonContent) - item.integrate(transaction) - jsonContent = [] - } + packJsonContent() switch (c.constructor) { case ArrayBuffer: // @ts-ignore c is definitely an ArrayBuffer @@ -363,6 +364,7 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem, } } }) + packJsonContent() } /** @@ -373,20 +375,22 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem, */ export const typeArrayInsertGenerics = (transaction, parent, index, content) => { if (index === 0) { - typeArrayInsertGenericsAfter(transaction, parent, null, content) + return typeArrayInsertGenericsAfter(transaction, parent, null, content) } - for (let n = parent._start; n !== null; n = n.right) { + let n = parent._start + for (; n !== null; n = n.right) { if (!n.deleted && n.countable) { if (index <= n.length) { if (index < n.length) { + // insert in-between getItemCleanStart(transaction.y.store, transaction, createID(n.id.client, n.id.clock + index)) } - return typeArrayInsertGenericsAfter(transaction, parent, n, content) + break } index -= n.length } } - throw new Error('Index exceeds array range') + return typeArrayInsertGenericsAfter(transaction, parent, n, content) } /** @@ -440,6 +444,10 @@ export const typeMapDelete = (transaction, parent, key) => { */ export const typeMapSet = (transaction, parent, key, value) => { const right = parent._map.get(key) || null + if (value == null) { + new ItemJSON(nextID(transaction), null, right, parent, key, [value]).integrate(transaction) + return + } switch (value.constructor) { case Number: case Object: diff --git a/src/types/YArray.js b/src/types/YArray.js index f5a13941..e10c7f0f 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -12,10 +12,12 @@ import { typeArrayInsertGenerics, typeArrayDelete, typeArrayMap, - Transaction, ItemType, // eslint-disable-line + YArrayRefID, + Y, Transaction, ItemType, // eslint-disable-line } from '../internals.js' import * as decoding from 'lib0/decoding.js' // eslint-disable-line +import * as encoding from 'lib0/encoding.js' /** * Event that describes the changes on a YArray @@ -52,12 +54,12 @@ export class YArray extends AbstractType { * * This type is sent to other client * * Observer functions are fired * - * @param {Transaction} transaction The Yjs instance + * @param {Y} y The Yjs instance * @param {ItemType} item * @private */ - _integrate (transaction, item) { - super._integrate(transaction, item) + _integrate (y, item) { + super._integrate(y, item) // @ts-ignore this.insert(0, this._prelimContent) this._prelimContent = null @@ -183,6 +185,13 @@ export class YArray extends AbstractType { push (content) { this.insert(this.length, content) } + + /** + * @param {encoding.Encoder} encoder + */ + _write (encoder) { + encoding.writeVarUint(encoder, YArrayRefID) + } } /** diff --git a/src/types/YMap.js b/src/types/YMap.js index 25f4d58e..eda6f192 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -10,9 +10,11 @@ import { typeMapGet, typeMapHas, createMapIterator, - Transaction, ItemType, // eslint-disable-line + YMapRefID, + Y, Transaction, ItemType, // eslint-disable-line } from '../internals.js' +import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' // eslint-disable-line import * as iterator from 'lib0/iterator.js' @@ -53,12 +55,12 @@ export class YMap extends AbstractType { * * This type is sent to other client * * Observer functions are fired * - * @param {Transaction} transaction The Yjs instance + * @param {Y} y The Yjs instance * @param {ItemType} item * @private */ - _integrate (transaction, item) { - super._integrate(transaction, item) + _integrate (y, item) { + super._integrate(y, item) // @ts-ignore for (let [key, value] of this._prelimContent) { this.set(key, value) @@ -88,7 +90,8 @@ export class YMap extends AbstractType { const map = {} for (let [key, item] of this._map) { if (!item.deleted) { - map[key] = item.getContent()[0] + const v = item.getContent()[0] + map[key] = v instanceof AbstractType ? v.toJSON() : v } } return map @@ -169,6 +172,13 @@ export class YMap extends AbstractType { has (key) { return typeMapHas(this, key) } + + /** + * @param {encoding.Encoder} encoder + */ + _write (encoder) { + encoding.writeVarUint(encoder, YMapRefID) + } } /** diff --git a/src/types/YText.js b/src/types/YText.js index f949dae2..cc018b98 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -12,10 +12,12 @@ import { createID, getItemCleanStart, isVisible, - ItemType, AbstractItem, Snapshot, StructStore, Transaction // eslint-disable-line + YTextRefID, + Y, ItemType, AbstractItem, Snapshot, StructStore, Transaction // eslint-disable-line } from '../internals.js' import * as decoding from 'lib0/decoding.js' // eslint-disable-line +import * as encoding from 'lib0/encoding.js' /** * @private @@ -566,11 +568,11 @@ export class YText extends AbstractType { } /** - * @param {Transaction} transaction + * @param {Y} y * @param {ItemType} item */ - _integrate (transaction, item) { - super._integrate(transaction, item) + _integrate (y, item) { + super._integrate(y, item) // @ts-ignore this._prelimContent is still defined this.insert(0, this._prelimContent.join('')) this._prelimContent = null @@ -839,6 +841,13 @@ export class YText extends AbstractType { }) } } + + /** + * @param {encoding.Encoder} encoder + */ + _write (encoder) { + encoding.writeVarUint(encoder, YTextRefID) + } } /** diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js index 69f1479e..b0bfd51e 100644 --- a/src/types/YXmlElement.js +++ b/src/types/YXmlElement.js @@ -13,7 +13,8 @@ import { typeArrayDelete, typeMapSet, typeMapDelete, - Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line + YXmlElementRefID, + Y, Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line } from '../internals.js' import * as encoding from 'lib0/encoding.js' @@ -259,12 +260,12 @@ export class YXmlElement extends YXmlFragment { * * This type is sent to other client * * Observer functions are fired * - * @param {Transaction} transaction The Yjs instance + * @param {Y} y The Yjs instance * @param {ItemType} item * @private */ - _integrate (transaction, item) { - super._integrate(transaction, item) + _integrate (y, item) { + super._integrate(y, item) // @ts-ignore this.insert(0, this._prelimContent) this._prelimContent = null @@ -285,19 +286,6 @@ export class YXmlElement extends YXmlFragment { return new YXmlElement(this.nodeName) } - /** - * Transform the properties of this type to binary and write it to an - * BinaryEncoder. - * - * This is called when this Item is sent to a remote peer. - * - * @private - * @param {encoding.Encoder} encoder The encoder to write data to. - */ - _write (encoder) { - encoding.writeVarString(encoder, this.nodeName) - } - toString () { return this.toDomString() } @@ -460,6 +448,20 @@ export class YXmlElement extends YXmlFragment { } return dom } + + /** + * Transform the properties of this type to binary and write it to an + * BinaryEncoder. + * + * This is called when this Item is sent to a remote peer. + * + * @private + * @param {encoding.Encoder} encoder The encoder to write data to. + */ + _write (encoder) { + encoding.writeVarUint(encoder, YXmlElementRefID) + encoding.writeVarString(encoder, this.nodeName) + } } /** diff --git a/src/types/YXmlHook.js b/src/types/YXmlHook.js index a052ffad..a16262be 100644 --- a/src/types/YXmlHook.js +++ b/src/types/YXmlHook.js @@ -2,7 +2,10 @@ * @module types */ -import { YMap } from '../internals.js' +import { + YMap, + YXmlHookRefID +} from '../internals.js' import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' @@ -71,6 +74,7 @@ export class YXmlHook extends YMap { */ _write (encoder) { super._write(encoder) + encoding.writeVarUint(encoder, YXmlHookRefID) encoding.writeVarString(encoder, this.hookName) } } diff --git a/src/types/YXmlText.js b/src/types/YXmlText.js index c410cb9c..f53d0023 100644 --- a/src/types/YXmlText.js +++ b/src/types/YXmlText.js @@ -2,8 +2,9 @@ * @module types */ -import { YText } from '../internals.js' +import { YText, YXmlTextRefID } from '../internals.js' +import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' // eslint-disable-line /** @@ -33,6 +34,12 @@ export class YXmlText extends YText { } return dom } + /** + * @param {encoding.Encoder} encoder + */ + _write (encoder) { + encoding.writeVarUint(encoder, YXmlTextRefID) + } } /** diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js index 27790373..6e58a6dd 100644 --- a/src/utils/StructStore.js +++ b/src/utils/StructStore.js @@ -88,6 +88,9 @@ export const findIndexSS = (structs, clock) => { if (clock < midclock + mid.length) { return midindex } + if (left === midindex) { + throw error.unexpectedCase() + } left = midindex } else { right = midindex diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 1e33d6c8..7a34356e 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -9,6 +9,7 @@ import { writeDeleteSet, DeleteSet, sortAndMergeDeleteSet, + getStates, AbstractType, AbstractItem, YEvent, ItemType, Y // eslint-disable-line } from '../internals.js' @@ -52,12 +53,12 @@ export class Transaction { */ this.deleteSet = new DeleteSet() /** - * If a state was modified, the original value is saved here. - * Use `stateUpdates` to compute the original state before the transaction, - * or to compute the set of inserted operations. + * Holds the state before the transaction started. * @type {Map} */ - this.stateUpdates = new Map() + this.beforeState = new Map() + getStates(y.store).forEach(({client, clock}) => { this.beforeState.set(client, clock) }) + this.afterState = new Map() /** * All types that were directly modified (property added or child * inserted/deleted). New types are not included in this Set. diff --git a/src/utils/Y.js b/src/utils/Y.js index d385bac7..655776fa 100644 --- a/src/utils/Y.js +++ b/src/utils/Y.js @@ -1,4 +1,4 @@ -import { } from './StructStore.js' +import { getStates } from './StructStore.js' import { callEventHandlerListeners, @@ -75,26 +75,29 @@ export class Y extends Observable { if (initialCall) { const transaction = this._transaction this._transaction = null - // only call event listeners / observers if anything changed + this.emit('beforeObserverCalls', [this, this._transaction]) + // emit change events on changed types + transaction.changed.forEach((subs, itemtype) => { + itemtype._callObserver(transaction, subs) + }) + transaction.changedParentTypes.forEach((events, type) => { + events = events + .filter(event => + event.target._item === null || !event.target._item.deleted + ) + events + .forEach(event => { + event.currentTarget = type + }) + // we don't need to check for events.length + // because we know it has at least one element + callEventHandlerListeners(type._dEH, [events, transaction]) + }) + // only call afterTransaction listeners if anything changed const transactionChangedContent = transaction.changedParentTypes.size !== 0 if (transactionChangedContent) { - this.emit('beforeObserverCalls', [this, this._transaction]) - // emit change events on changed types - transaction.changed.forEach((subs, itemtype) => { - itemtype._callObserver(transaction, subs) - }) - transaction.changedParentTypes.forEach((events, type) => { - events = events - .filter(event => - event.target._item === null || !event.target._item.deleted - ) - events - .forEach(event => { - event.currentTarget = type - }) - // we don't need to check for events.length - // because we know it has at least one element - callEventHandlerListeners(type._dEH, [events, transaction]) + getStates(transaction.y.store).forEach(({client, clock}) => { + transaction.afterState.set(client, clock) }) // when all changes & events are processed, emit afterTransaction event this.emit('afterTransaction', [this, transaction]) @@ -141,15 +144,17 @@ export class Y extends Observable { } } // 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) + for (const [client, clock] of transaction.beforeState) { + if (transaction.afterState.get(client) !== clock) { + /** + * @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 @@ -200,16 +205,21 @@ export class Y extends Observable { * @return {AbstractType} The created type. Constructed with TypeConstructor */ get (name, TypeConstructor = AbstractType) { - // @ts-ignore - const type = map.setIfUndefined(this.share, name, () => new TypeConstructor()) + const type = map.setIfUndefined(this.share, name, () => { + // @ts-ignore + const t = new TypeConstructor() + t._integrate(this, null) + return t + }) const Constr = type.constructor - if (Constr !== TypeConstructor) { + if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) { if (Constr === AbstractType) { const t = new Constr() t._map = type._map t._start = type._start t._length = type._length this.share.set(name, t) + t._integrate(this, null) return t } else { throw new Error(`Type with the name ${name} has already been defined with a different constructor`) diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index c908390c..e6700956 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -67,7 +67,7 @@ export class YEvent { * @return {boolean} */ adds (struct) { - return struct.id.clock > (this.transaction.stateUpdates.get(struct.id.client) || 0) + return struct.id.clock > (this.transaction.beforeState.get(struct.id.client) || 0) } } diff --git a/src/utils/structEncoding.js b/src/utils/structEncoding.js index 266ef4c2..93991a15 100644 --- a/src/utils/structEncoding.js +++ b/src/utils/structEncoding.js @@ -13,6 +13,8 @@ import { writeID, createID, readID, + getState, + getStates, Transaction, AbstractStruct, AbstractRef, StructStore, ID // eslint-disable-line } from '../internals.js' @@ -51,7 +53,8 @@ const createStructReaderIterator = (decoder, structsLen, nextID) => iterator.cre } else { const info = decoding.readUint8(decoder) value = new structRefs[binary.BITS5 & info](decoder, nextID, info) - nextID = createID(nextID.client, nextID.clock) + nextID = createID(nextID.client, nextID.clock + value.length) + structsLen-- } return { done, value } }) @@ -60,18 +63,30 @@ const createStructReaderIterator = (decoder, structsLen, nextID) => iterator.cre * @param {encoding.Encoder} encoder * @param {Transaction} transaction */ -export const writeStructsFromTransaction = (encoder, transaction) => writeStructs(encoder, transaction.y.store, transaction.stateUpdates) +export const writeStructsFromTransaction = (encoder, transaction) => writeStructs(encoder, transaction.y.store, transaction.beforeState) /** * @param {encoding.Encoder} encoder * @param {StructStore} store - * @param {StateMap} sm + * @param {StateMap} _sm */ -export const writeStructs = (encoder, store, sm) => { +export const writeStructs = (encoder, store, _sm) => { + // we filter all valid _sm entries into sm + const sm = new Map() + _sm.forEach((clock, client) => { + if (getState(store, client) > clock) { + sm.set(client, clock) + } + }) + getStates(store).forEach(({client}) => { + if (!_sm.has(client)) { + sm.set(client, 0) + } + }) const encoderUserPosMap = map.create() // write # states that were updated encoding.writeVarUint(encoder, sm.size) - sm.forEach((client, clock) => { + sm.forEach((clock, client) => { // write first id writeID(encoder, createID(client, clock)) encoderUserPosMap.set(client, encoding.length(encoder)) @@ -79,7 +94,7 @@ export const writeStructs = (encoder, store, sm) => { // We will fill out this value later *) encoding.writeUint32(encoder, 0) }) - sm.forEach((client, clock) => { + sm.forEach((clock, client) => { const decPos = encoderUserPosMap.get(client) encoding.setUint32(encoder, decPos, encoding.length(encoder) - decPos) /** @@ -115,8 +130,8 @@ export const readStructs = (decoder, transaction, store) => { * @type {Map>} */ const structReaders = new Map() - const clientStateUpdates = decoding.readVarUint(decoder) - for (let i = 0; i < clientStateUpdates; i++) { + const clientbeforeState = decoding.readVarUint(decoder) + for (let i = 0; i < clientbeforeState; i++) { const nextID = readID(decoder) const decoderPos = decoder.pos + decoding.readUint32(decoder) const structReaderDecoder = decoding.clone(decoder, decoderPos) diff --git a/tests/testHelper.js b/tests/testHelper.js index da6240e8..45aeef66 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -268,7 +268,7 @@ export const compare = users => { while (users[0].tc.flushAllMessages()) {} 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 userXmlValues = users.map(u => u.get('xml', Y.XmlElement).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}`)