diff --git a/src/MessageHandler/integrateRemoteStructs.js b/src/MessageHandler/integrateRemoteStructs.js index 3abe9d8f..d2067f3c 100644 --- a/src/MessageHandler/integrateRemoteStructs.js +++ b/src/MessageHandler/integrateRemoteStructs.js @@ -1,6 +1,7 @@ import { getStruct } from '../Util/structReferences.js' import BinaryDecoder from '../Util/Binary/Decoder.js' import { logID } from './messageToString.js' +import GC from '../Struct/GC.js'; class MissingEntry { constructor (decoder, missing, struct) { @@ -24,7 +25,14 @@ function _integrateRemoteStructHelper (y, struct) { if (y.ss.getState(id.user) > id.clock) { return } - struct._integrate(y) + if (struct.constructor === GC || (struct._parent.constructor !== GC && struct._parent._deleted === false)) { + // Is either a GC or Item with an undeleted parent + // save to integrate + struct._integrate(y) + } else { + // Is an Item. parent was deleted. + struct._gc(y) + } let msu = y._missingStructs.get(id.user) if (msu != null) { let clock = id.clock diff --git a/src/Store/DeleteStore.js b/src/Store/DeleteStore.js index 77639fd9..eb5b1a9e 100644 --- a/src/Store/DeleteStore.js +++ b/src/Store/DeleteStore.js @@ -52,7 +52,7 @@ export default class DeleteStore extends Tree { if (rightD !== null && rightD._id.user === id.user) { if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node const d = id.clock + length - rightD._id.clock - rightD._id.clock += d + rightD._id = new ID(rightD._id.user, rightD._id.clock + d) rightD.len -= d } } diff --git a/src/Store/OperationStore.js b/src/Store/OperationStore.js index fdc74b03..6ee790a6 100644 --- a/src/Store/OperationStore.js +++ b/src/Store/OperationStore.js @@ -2,6 +2,7 @@ import Tree from '../Util/Tree.js' import RootID from '../Util/ID/RootID.js' import { getStruct } from '../Util/structReferences.js' import { logID } from '../MessageHandler/messageToString.js' +import GC from '../Struct/GC.js' export default class OperationStore extends Tree { constructor (y) { @@ -11,17 +12,25 @@ export default class OperationStore extends Tree { logTable () { const items = [] this.iterate(null, null, function (item) { - items.push({ - id: logID(item), - origin: logID(item._origin === null ? null : item._origin._lastId), - left: logID(item._left === null ? null : item._left._lastId), - right: logID(item._right), - right_origin: logID(item._right_origin), - parent: logID(item._parent), - parentSub: item._parentSub, - deleted: item._deleted, - content: JSON.stringify(item._content) - }) + if (item.constructor === GC) { + items.push({ + id: logID(item), + content: item._length, + deleted: 'GC' + }) + } else { + items.push({ + id: logID(item), + origin: logID(item._origin === null ? null : item._origin._lastId), + left: logID(item._left === null ? null : item._left._lastId), + right: logID(item._right), + right_origin: logID(item._right_origin), + parent: logID(item._parent), + parentSub: item._parentSub, + deleted: item._deleted, + content: JSON.stringify(item._content) + }) + } }) console.table(items) } diff --git a/src/Struct/GC.js b/src/Struct/GC.js index d2cc7f02..1a90c888 100644 --- a/src/Struct/GC.js +++ b/src/Struct/GC.js @@ -1,4 +1,8 @@ +import { getStructReference } from '../Util/structReferences.js' +import { RootFakeUserID } from '../Util/ID/RootID.js' +import ID from '../Util/ID/ID.js' +// TODO should have the same base class as Item export default class GC { constructor () { this._id = null @@ -9,7 +13,38 @@ export default class GC { return true } - integrate () { + _integrate (y) { + const id = this._id + const userState = y.ss.getState(id.user) + if (id.clock === userState) { + y.ss.setState(id.user, id.clock + this._length) + } + y.ds.mark(this._id, this._length, true) + let n = y.os.put(this) + const prev = n.prev().val + if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) { + // TODO: do merging for all items! + prev._length += n.val._length + y.os.delete(n.val._id) + n = prev + } + if (n.val) { + n = n.val + } + const next = y.os.findNext(n._id) + if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) { + n._length += next._length + y.os.delete(next._id) + } + + if (id.user !== RootFakeUserID) { + if (y.connector !== null && (y.connector._forwardAppliedStructs || id.user === y.userID)) { + y.connector.broadcastStruct(this) + } + if (y.persistence !== null) { + y.persistence.saveStruct(y, this) + } + } } /** @@ -37,8 +72,17 @@ export default class GC { * @private */ _fromBinary (y, decoder) { - this._id = decoder.readID() + const id = decoder.readID() + this._id = id this._length = decoder.readVarUint() - return [] - } + const missing = [] + if (y.ss.getState(id.user) < id.clock) { + missing.push(new ID(id.user, id.clock - 1)) + } + return missing + } + + _splitAt () { + return this + } } \ No newline at end of file diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 657fc97f..9b76e14b 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -240,27 +240,11 @@ export default class Item { _gcChildren (y) {} _gc (y) { - y.ds.mark(this._id, this._length, true) const gc = new GC() gc._id = this._id gc._length = this._length y.os.delete(this._id) - let n = y.os.put(gc) - const prev = n.prev().val - if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) { - // TODO: do merging for all items! - prev._length += n.val._length - y.os.delete(n.val._id) - n = prev - } - if (n.val) { - n = n.val - } - const next = y.os.findNext(n._id) - if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) { - n._length += next._length - y.os.delete(n._id) - } + gc._integrate(y) } /** @@ -511,9 +495,19 @@ export default class Item { } } else if (this._parent === null) { if (this._origin !== null) { - this._parent = this._origin._parent + if (this._origin.constructor === GC) { + // if origin is a gc, set parent also gc'd + this._parent = this._origin + } else { + this._parent = this._origin._parent + } } else if (this._right_origin !== null) { - this._parent = this._right_origin._parent + // if origin is a gc, set parent also gc'd + if (this._right_origin.constructor === GC) { + this._parent = this._right_origin + } else { + this._parent = this._right_origin._parent + } } } if (info & 0b1000) { diff --git a/tests-lib/helper.js b/tests-lib/helper.js index 26a50f08..a8425a9d 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -8,6 +8,7 @@ import ItemJSON from '../src/Struct/ItemJSON.js' import ItemString from '../src/Struct/ItemString.js' import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' import Quill from 'quill' +import GC from '../src/Struct/GC.js'; export const Y = _Y @@ -110,13 +111,22 @@ export async function compareUsers (t, users) { var data = {} let ops = [] u.os.iterate(null, null, function (op) { - const json = { - id: op._id, - left: op._left === null ? null : op._left._lastId, - right: op._right === null ? null : op._right._id, - length: op._length, - deleted: op._deleted, - parent: op._parent._id + let json + if (op.constructor === GC) { + json = { + type: 'GC', + id: op._id, + length: op._length + } + } else { + json = { + id: op._id, + left: op._left === null ? null : op._left._lastId, + right: op._right === null ? null : op._right._id, + length: op._length, + deleted: op._deleted, + parent: op._parent._id + } } if (op instanceof ItemJSON || op instanceof ItemString) { json.content = op._content @@ -186,7 +196,7 @@ export async function initArrays (t, opts) { y.dom = dom y.on('afterTransaction', function () { for (let missing of y._missingStructs.values()) { - if (Array.from(missing.values()).length > 0) { + if (missing.size > 0) { console.error(new Error('Test check in "afterTransaction": missing should be empty!')) } }