diff --git a/src/structs/AbstractStruct.js b/src/structs/AbstractStruct.js index 06f83faa..fddd92c4 100644 --- a/src/structs/AbstractStruct.js +++ b/src/structs/AbstractStruct.js @@ -14,7 +14,13 @@ export class AbstractStruct { constructor (id, length) { this.id = id this.length = length - this.deleted = false + } + + /** + * @type {boolean} + */ + get deleted () { + throw error.methodUnimplemented() } /** diff --git a/src/structs/ContentDeleted.js b/src/structs/ContentDeleted.js index 1d430707..63e2f4f4 100644 --- a/src/structs/ContentDeleted.js +++ b/src/structs/ContentDeleted.js @@ -68,7 +68,7 @@ export class ContentDeleted { */ integrate (transaction, item) { addToDeleteSet(transaction.deleteSet, item.id, this.len) - item.deleted = true + item.markDeleted() } /** diff --git a/src/structs/GC.js b/src/structs/GC.js index f2c5fcd6..45541ad2 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -13,13 +13,8 @@ export const structGCRefNumber = 0 * @private */ export class GC extends AbstractStruct { - /** - * @param {ID} id - * @param {number} length - */ - constructor (id, length) { - super(id, length) - this.deleted = true + get deleted () { + return true } delete () {} diff --git a/src/structs/Item.js b/src/structs/Item.js index 96bae7bb..27e0f0a3 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -100,7 +100,7 @@ export const splitItem = (transaction, leftItem, diff) => { leftItem.content.splice(diff) ) if (leftItem.deleted) { - rightItem.deleted = true + rightItem.markDeleted() } if (leftItem.keep) { rightItem.keep = true @@ -279,11 +279,6 @@ export class Item extends AbstractStruct { * @type {String | null} */ this.parentSub = parentSub - /** - * Whether this item was deleted or not. - * @type {Boolean} - */ - this.deleted = false /** * If this type's effect is reundone this type refers to the type that undid * this operation. @@ -294,14 +289,44 @@ export class Item extends AbstractStruct { * @type {AbstractContent} */ this.content = content - /** - * If true, do not garbage collect this Item. - */ - this.keep = false + this.info = this.content.isCountable() ? binary.BIT2 : 0 + + // this.keep = false + } + + /** + * If true, do not garbage collect this Item. + */ + get keep () { + return (this.info & binary.BIT1) > 0 + } + + set keep (doKeep) { + if (this.keep !== doKeep) { + this.info ^= binary.BIT1 + } } get countable () { - return this.content.isCountable() + return (this.info & binary.BIT2) > 0 + } + + /** + * Whether this item was deleted or not. + * @type {Boolean} + */ + get deleted () { + return (this.info & binary.BIT3) > 0 + } + + set deleted (doDelete) { + if (this.deleted !== doDelete) { + this.info ^= binary.BIT3 + } + } + + markDeleted () { + this.info |= binary.BIT3 } /** @@ -367,110 +392,108 @@ export class Item extends AbstractStruct { * @param {number} offset */ integrate (transaction, offset) { - const store = transaction.doc.store if (offset > 0) { this.id.clock += offset - this.left = getItemCleanEnd(transaction, store, createID(this.id.client, this.id.clock - 1)) + this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1)) this.origin = this.left.lastId this.content = this.content.splice(offset) this.length -= offset } - const parentSub = this.parentSub - const length = this.length - const parent = /** @type {AbstractType|null} */ (this.parent) - if (parent) { - /** - * @type {Item|null} - */ - let left = this.left + if (this.parent) { + if ((!this.left && (!this.right || this.right.left !== null)) || (this.left && this.left.right !== this.right)) { + /** + * @type {Item|null} + */ + let left = this.left - /** - * @type {Item|null} - */ - let o - // set o to the first conflicting item - if (left !== null) { - o = left.right - } else if (parentSub !== null) { - o = parent._map.get(parentSub) || null - while (o !== null && o.left !== null) { - o = o.left - } - } else { - o = parent._start - } - // TODO: use something like DeleteSet here (a tree implementation would be best) - // @todo use global set definitions - /** - * @type {Set} - */ - const conflictingItems = new Set() - /** - * @type {Set} - */ - const itemsBeforeOrigin = new Set() - // Let c in conflictingItems, b in itemsBeforeOrigin - // ***{origin}bbbb{this}{c,b}{c,b}{o}*** - // Note that conflictingItems is a subset of itemsBeforeOrigin - while (o !== null && o !== this.right) { - itemsBeforeOrigin.add(o) - conflictingItems.add(o) - if (compareIDs(this.origin, o.origin)) { - // case 1 - if (o.id.client < this.id.client) { - left = o - conflictingItems.clear() - } - } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) { - // case 2 - if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) { - left = o - conflictingItems.clear() + /** + * @type {Item|null} + */ + let o + // set o to the first conflicting item + if (left !== null) { + o = left.right + } else if (this.parentSub !== null) { + o = /** @type {AbstractType} */ (this.parent)._map.get(this.parentSub) || null + while (o !== null && o.left !== null) { + o = o.left } } else { - break + o = /** @type {AbstractType} */ (this.parent)._start } - o = o.right + // TODO: use something like DeleteSet here (a tree implementation would be best) + // @todo use global set definitions + /** + * @type {Set} + */ + const conflictingItems = new Set() + /** + * @type {Set} + */ + const itemsBeforeOrigin = new Set() + // Let c in conflictingItems, b in itemsBeforeOrigin + // ***{origin}bbbb{this}{c,b}{c,b}{o}*** + // Note that conflictingItems is a subset of itemsBeforeOrigin + while (o !== null && o !== this.right) { + itemsBeforeOrigin.add(o) + conflictingItems.add(o) + if (compareIDs(this.origin, o.origin)) { + // case 1 + if (o.id.client < this.id.client) { + left = o + conflictingItems.clear() + } + } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { + // case 2 + if (o.origin === null || !conflictingItems.has(getItem(transaction.doc.store, o.origin))) { + left = o + conflictingItems.clear() + } + } else { + break + } + o = o.right + } + this.left = left } - this.left = left // reconnect left/right + update parent map/start if necessary - if (left !== null) { - const right = left.right + if (this.left !== null) { + const right = this.left.right this.right = right - left.right = this + this.left.right = this } else { let r - if (parentSub !== null) { - r = parent._map.get(parentSub) || null + if (this.parentSub !== null) { + r = /** @type {AbstractType} */ (this.parent)._map.get(this.parentSub) || null while (r !== null && r.left !== null) { r = r.left } } else { - r = parent._start - parent._start = this + r = /** @type {AbstractType} */ (this.parent)._start + ;/** @type {AbstractType} */ (this.parent)._start = this } this.right = r } if (this.right !== null) { this.right.left = this - } else if (parentSub !== null) { + } else if (this.parentSub !== null) { // set as current parent value if right === null and this is parentSub - parent._map.set(parentSub, this) - if (left !== null) { + /** @type {AbstractType} */ (this.parent)._map.set(this.parentSub, this) + if (this.left !== null) { // this is the current attribute value of parent. delete right - left.delete(transaction) + this.left.delete(transaction) } } // adjust length of parent - if (parentSub === null && this.countable && !this.deleted) { - parent._length += length + if (this.parentSub === null && this.countable && !this.deleted) { + /** @type {AbstractType} */ (this.parent)._length += this.length } - addStruct(store, this) + addStruct(transaction.doc.store, this) this.content.integrate(transaction, this) // add parent to transaction.changed - addChangedTypeToTransaction(transaction, parent, parentSub) - if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) { + addChangedTypeToTransaction(transaction, /** @type {AbstractType} */ (this.parent), this.parentSub) + if ((/** @type {AbstractType} */ (this.parent)._item !== null && /** @type {AbstractType} */ (this.parent)._item.deleted) || (this.right !== null && this.parentSub !== null)) { // delete if parent is deleted or if this is not the current attribute value of parent this.delete(transaction) } @@ -554,7 +577,7 @@ export class Item extends AbstractStruct { if (this.countable && this.parentSub === null) { parent._length -= this.length } - this.deleted = true + this.markDeleted() addToDeleteSet(transaction.deleteSet, this.id, this.length) maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub) this.content.delete(transaction) diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index b49a6fab..2230131b 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -205,29 +205,38 @@ export const testFormattingRemovedInMidText = tc => { t.assert(Y.getTypeChildren(text0).length === 3) } +const tryGc = () => { + if (typeof global !== 'undefined' && global.gc) { + global.gc() + } +} + /** * @param {t.TestCase} tc */ export const testLargeFragmentedDocument = tc => { - const itemsToInsert = 1000000 + const itemsToInsert = 2000000 let update = /** @type {any} */ (null) ;(() => { const doc1 = new Y.Doc() const text0 = doc1.getText('txt') - t.measureTime(`time to insert ${itemsToInsert}`, () => { + tryGc() + t.measureTime(`time to insert ${itemsToInsert} items`, () => { doc1.transact(() => { for (let i = 0; i < itemsToInsert; i++) { text0.insert(0, '0') } }) }) - t.measureTime('time to encode', () => { + tryGc() + t.measureTime('time to encode document', () => { update = Y.encodeStateAsUpdate(doc1) }) })() ;(() => { const doc2 = new Y.Doc() - t.measureTime('time to apply', () => { + tryGc() + t.measureTime(`time to apply ${itemsToInsert} updates`, () => { Y.applyUpdate(doc2, update) }) })()