diff --git a/src/Struct/Delete.js b/src/Struct/Delete.js index 16b0bd0d..8e1e7c21 100644 --- a/src/Struct/Delete.js +++ b/src/Struct/Delete.js @@ -13,18 +13,18 @@ export function deleteItemRange (y, user, clock, range) { if (item !== null) { if (!item._deleted) { item._splitAt(y, range) - item._delete(y, createDelete) + item._delete(y, createDelete, true) } let itemLen = item._length range -= itemLen clock += itemLen if (range > 0) { let node = y.os.findNode(new ID(user, clock)) - while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) { + while (node !== null && node.val !== null && range > 0 && node.val._id.equals(new ID(user, clock))) { const nodeVal = node.val if (!nodeVal._deleted) { nodeVal._splitAt(y, range) - nodeVal._delete(y, createDelete) + nodeVal._delete(y, createDelete, true) } const nodeLen = nodeVal._length range -= nodeLen diff --git a/src/Struct/GC.js b/src/Struct/GC.js new file mode 100644 index 00000000..d2cc7f02 --- /dev/null +++ b/src/Struct/GC.js @@ -0,0 +1,44 @@ + +export default class GC { + constructor () { + this._id = null + this._length = 0 + } + + get _deleted () { + return true + } + + integrate () { + } + + /** + * 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. + * + * @param {BinaryEncoder} encoder The encoder to write data to. + * @private + */ + _toBinary (encoder) { + encoder.writeUint8(getStructReference(this.constructor)) + encoder.writeID(this._id) + encoder.writeVarUint(this._length) + } + + /** + * 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 {BinaryDecoder} decoder The decoder object to read data from. + * @private + */ + _fromBinary (y, decoder) { + this._id = decoder.readID() + this._length = decoder.readVarUint() + return [] + } +} \ No newline at end of file diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 79f97f3c..657fc97f 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -3,6 +3,7 @@ import ID from '../Util/ID/ID.js' import { RootFakeUserID } from '../Util/ID/RootID.js' import Delete from './Delete.js' import { transactionTypeChanged } from '../Transaction.js' +import GC from './GC.js' /** * @private @@ -220,7 +221,7 @@ export default class Item { _delete (y, createDelete = true) { if (!this._deleted) { this._deleted = true - y.ds.markDeleted(this._id, this._length) + y.ds.mark(this._id, this._length, false) let del = new Delete() del._targetID = this._id del._length = this._length @@ -236,6 +237,32 @@ 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) + } + } + /** * This is called right before this Item receives any children. * It can be overwritten to apply pending changes before applying remote changes diff --git a/src/Struct/Type.js b/src/Struct/Type.js index cb98f374..2d96dcc2 100644 --- a/src/Struct/Type.js +++ b/src/Struct/Type.js @@ -30,6 +30,14 @@ export function getListItemIDByPosition (type, i) { } } +function gcChildren (y, item) { + while (item !== null) { + item._delete(y, false, true) + item._gc(y) + item = item._right + } +} + /** * Abstract Yjs Type class */ @@ -184,6 +192,20 @@ export default class Type extends Item { } } + _gcChildren (y) { + gcChildren(y, this._start) + this._start = null + this._map.forEach(item => { + gcChildren(y, item) + }) + this._map = new Map() + } + + _gc (y) { + this._gcChildren(y) + super._gc(y) + } + /** * @private * Mark this Item as deleted. @@ -192,22 +214,25 @@ export default class Type extends Item { * @param {boolean} createDelete Whether to propagate a message that this * Type was deleted. */ - _delete (y, createDelete) { - super._delete(y, createDelete) + _delete (y, createDelete, gcChildren = true) { + super._delete(y, createDelete, gcChildren) y._transaction.changedTypes.delete(this) // delete map types for (let value of this._map.values()) { if (value instanceof Item && !value._deleted) { - value._delete(y, false) + value._delete(y, false, gcChildren) } } // delete array types let t = this._start while (t !== null) { if (!t._deleted) { - t._delete(y, false) + t._delete(y, false, gcChildren) } t = t._right } + if (gcChildren) { + this._gcChildren(y) + } } } diff --git a/src/Types/YXml/YXmlFragment.js b/src/Types/YXml/YXmlFragment.js index babb1638..8bbe932c 100644 --- a/src/Types/YXml/YXmlFragment.js +++ b/src/Types/YXml/YXmlFragment.js @@ -126,8 +126,8 @@ export default class YXmlFragment extends YArray { * * @private */ - _delete (y, createDelete) { - super._delete(y, createDelete) + _delete (y, createDelete, gcChildren) { + super._delete(y, createDelete, gcChildren) } /** diff --git a/src/Types/YXml/YXmlText.js b/src/Types/YXml/YXmlText.js index 848fe985..81c9ee69 100644 --- a/src/Types/YXml/YXmlText.js +++ b/src/Types/YXml/YXmlText.js @@ -36,7 +36,7 @@ export default class YXmlText extends YText { * * @private */ - _delete (y, createDelete) { - super._delete(y, createDelete) + _delete (y, createDelete, gcChildren) { + super._delete(y, createDelete, gcChildren) } } diff --git a/src/Util/structReferences.js b/src/Util/structReferences.js index 4101e455..a79d8c71 100644 --- a/src/Util/structReferences.js +++ b/src/Util/structReferences.js @@ -8,6 +8,7 @@ import ItemJSON from '../Struct/ItemJSON.js' import ItemString from '../Struct/ItemString.js' import ItemFormat from '../Struct/ItemFormat.js' import ItemEmbed from '../Struct/ItemEmbed.js' +import GC from '../Struct/GC.js' const structs = new Map() const references = new Map() @@ -54,3 +55,5 @@ registerStruct(6, YXmlFragment) registerStruct(7, YXmlElement) registerStruct(8, YXmlText) registerStruct(9, YXmlHook) + +registerStruct(12, GC) \ No newline at end of file