diff --git a/src/structs/Item.js b/src/structs/Item.js index 1b7ba939..dc9f3eec 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -93,6 +93,7 @@ export const splitItem = (transaction, leftItem, diff) => { leftItem.rightOrigin, leftItem.parent, leftItem.parentSub, + leftItem.redone !== null ? createID(leftItem.redone.client, leftItem.redone.clock + diff) : null, leftItem.content.splice(diff) ) if (leftItem.deleted) { @@ -101,9 +102,6 @@ export const splitItem = (transaction, leftItem, diff) => { if (leftItem.keep) { rightItem.keep = true } - if (leftItem.redone !== null) { - rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff) - } // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing) leftItem.right = rightItem // update right @@ -232,6 +230,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo right, right && right.id, parentType, item.parentSub, + null, item.content.copy() ) item.redone = nextId @@ -252,9 +251,10 @@ export class Item extends AbstractStruct { * @param {ID | null} rightOrigin * @param {AbstractType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it. * @param {string | null} parentSub + * @param {ID | null} redone * @param {AbstractContent} content */ - constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) { + constructor (id, left, origin, right, rightOrigin, parent, parentSub, redone, content) { super(id, content.getLength()) /** * The item that was originally to the left of this item. @@ -293,7 +293,7 @@ export class Item extends AbstractStruct { * this operation. * @type {ID | null} */ - this.redone = null + this.redone = redone /** * @type {AbstractContent} */ @@ -653,10 +653,11 @@ export class Item extends AbstractStruct { const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin const rightOrigin = this.rightOrigin const parentSub = this.parentSub - const info = (this.content.getRef() & binary.BITS5) | + const info = (this.content.getRef() & binary.BITS4) | (origin === null ? 0 : binary.BIT8) | // origin is defined (rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined - (parentSub === null ? 0 : binary.BIT6) // parentSub is non-null + (parentSub === null ? 0 : binary.BIT6) | // parentSub is non-null + (this.redone === null ? 0 : binary.BIT5) // redone is defined encoder.writeInfo(info) if (origin !== null) { encoder.writeLeftID(origin) @@ -691,6 +692,9 @@ export class Item extends AbstractStruct { encoder.writeString(parentSub) } } + if (this.redone !== null) { + encoder.writeRedone(this.redone) + } this.content.write(encoder, offset) } } @@ -699,7 +703,7 @@ export class Item extends AbstractStruct { * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @param {number} info */ -export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder) +export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS4](decoder) /** * A lookup map for reading Item content. diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 3dff240c..4b50fea3 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -643,7 +643,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, let jsonContent = [] const packJsonContent = () => { if (jsonContent.length > 0) { - left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent)) + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, null, new ContentAny(jsonContent)) left.integrate(transaction, 0) jsonContent = [] } @@ -665,16 +665,16 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, switch (c.constructor) { case Uint8Array: case ArrayBuffer: - left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c)))) + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c)))) left.integrate(transaction, 0) break case Doc: - left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c))) + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, null, new ContentDoc(/** @type {Doc} */ (c))) left.integrate(transaction, 0) break default: if (c instanceof AbstractType) { - left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c)) + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, null, new ContentType(c)) left.integrate(transaction, 0) } else { throw new Error('Unexpected content type in insert operation') @@ -862,7 +862,7 @@ export const typeMapSet = (transaction, parent, key, value) => { } } } - new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0) + new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, null, content).integrate(transaction, 0) } /** diff --git a/src/types/YText.js b/src/types/YText.js index 8919b009..7e17dfad 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -166,7 +166,7 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes negatedAttributes.forEach((val, key) => { const left = currPos.left const right = currPos.right - const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)) + const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, null, new ContentFormat(key, val)) nextFormat.integrate(transaction, 0) currPos.right = nextFormat currPos.forward() @@ -232,7 +232,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => { // save negated attribute (set null if currentVal undefined) negatedAttributes.set(key, currentVal) const { left, right } = currPos - currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)) + currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, null, new ContentFormat(key, val)) currPos.right.integrate(transaction, 0) currPos.forward() } @@ -266,7 +266,7 @@ const insertText = (transaction, parent, currPos, text, attributes) => { if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength()) } - right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content) + right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, null, content) right.integrate(transaction, 0) currPos.right = right currPos.index = index @@ -342,7 +342,7 @@ const formatText = (transaction, parent, currPos, length, attributes) => { for (; length > 0; length--) { newlines += '\n' } - currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, new ContentString(newlines)) + currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, null, new ContentString(newlines)) currPos.right.integrate(transaction, 0) currPos.forward() } diff --git a/src/utils/UpdateDecoder.js b/src/utils/UpdateDecoder.js index 67a38ceb..e1e476a2 100644 --- a/src/utils/UpdateDecoder.js +++ b/src/utils/UpdateDecoder.js @@ -46,6 +46,13 @@ export class UpdateDecoderV1 extends DSDecoderV1 { return createID(decoding.readVarUint(this.restDecoder), decoding.readVarUint(this.restDecoder)) } + /** + * @return {ID} + */ + readRedone () { + return createID(decoding.readVarUint(this.restDecoder), decoding.readVarUint(this.restDecoder)) + } + /** * Read the next client id. * Use this in favor of readID whenever possible to reduce the number of objects created. @@ -174,6 +181,7 @@ export class UpdateDecoderV2 extends DSDecoderV2 { this.clientDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder)) this.leftClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder)) this.rightClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder)) + this.redoneClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder)) this.infoDecoder = new decoding.RleDecoder(decoding.readVarUint8Array(decoder), decoding.readUint8) this.stringDecoder = new decoding.StringDecoder(decoding.readVarUint8Array(decoder)) this.parentInfoDecoder = new decoding.RleDecoder(decoding.readVarUint8Array(decoder), decoding.readUint8) @@ -195,6 +203,13 @@ export class UpdateDecoderV2 extends DSDecoderV2 { return new ID(this.clientDecoder.read(), this.rightClockDecoder.read()) } + /** + * @return {ID} + */ + readRedone () { + return new ID(this.clientDecoder.read(), this.redoneClockDecoder.read()) + } + /** * Read the next client id. * Use this in favor of readID whenever possible to reduce the number of objects created. diff --git a/src/utils/UpdateEncoder.js b/src/utils/UpdateEncoder.js index 8cf30381..0cfc3d96 100644 --- a/src/utils/UpdateEncoder.js +++ b/src/utils/UpdateEncoder.js @@ -50,6 +50,14 @@ export class UpdateEncoderV1 extends DSEncoderV1 { encoding.writeVarUint(this.restEncoder, id.clock) } + /** + * @param {ID} id + */ + writeRedone (id) { + encoding.writeVarUint(this.restEncoder, id.client) + encoding.writeVarUint(this.restEncoder, id.clock) + } + /** * Use writeClient and writeClock instead of writeID if possible. * @param {number} client @@ -177,6 +185,7 @@ export class UpdateEncoderV2 extends DSEncoderV2 { this.clientEncoder = new encoding.UintOptRleEncoder() this.leftClockEncoder = new encoding.IntDiffOptRleEncoder() this.rightClockEncoder = new encoding.IntDiffOptRleEncoder() + this.redoneClockEncoder = new encoding.IntDiffOptRleEncoder() this.infoEncoder = new encoding.RleEncoder(encoding.writeUint8) this.stringEncoder = new encoding.StringEncoder() this.parentInfoEncoder = new encoding.RleEncoder(encoding.writeUint8) @@ -191,6 +200,7 @@ export class UpdateEncoderV2 extends DSEncoderV2 { encoding.writeVarUint8Array(encoder, this.clientEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array()) + encoding.writeVarUint8Array(encoder, this.redoneClockEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.infoEncoder)) encoding.writeVarUint8Array(encoder, this.stringEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.parentInfoEncoder)) @@ -217,6 +227,14 @@ export class UpdateEncoderV2 extends DSEncoderV2 { this.rightClockEncoder.write(id.clock) } + /** + * @param {ID} id + */ + writeRedone (id) { + this.clientEncoder.write(id.client) + this.redoneClockEncoder.write(id.clock) + } + /** * @param {number} client */ diff --git a/src/utils/encoding.js b/src/utils/encoding.js index 08a9602d..a2639992 100644 --- a/src/utils/encoding.js +++ b/src/utils/encoding.js @@ -127,7 +127,7 @@ export const readClientsStructRefs = (decoder, doc) => { clientRefs.set(client, { i: 0, refs }) for (let i = 0; i < numberOfStructs; i++) { const info = decoder.readInfo() - switch (binary.BITS5 & info) { + switch (binary.BITS4 & info) { case 0: { // GC const len = decoder.readLen() refs[i] = new GC(createID(client, clock), len) @@ -160,6 +160,7 @@ export const readClientsStructRefs = (decoder, doc) => { (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin cantCopyParentInfo ? (decoder.readParentInfo() ? doc.get(decoder.readString()) : decoder.readLeftID()) : null, // parent cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub + (info & binary.BIT5) === binary.BIT5 ? decoder.readRedone() : null, // redone readItemContent(decoder, info) // item content ) /* A non-optimized implementation of the above algorithm: @@ -184,6 +185,7 @@ export const readClientsStructRefs = (decoder, doc) => { rightOrigin, // right origin cantCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub + (info & binary.BIT5) === binary.BIT5 ? decoder.readRedone() : null, // redone readItemContent(decoder, info) // item content ) */ diff --git a/src/utils/updates.js b/src/utils/updates.js index fc40cd57..c4523e5b 100644 --- a/src/utils/updates.js +++ b/src/utils/updates.js @@ -53,7 +53,7 @@ function * lazyStructReaderGenerator (decoder) { const len = decoding.readVarUint(decoder.restDecoder) yield new Skip(createID(client, clock), len) clock += len - } else if ((binary.BITS5 & info) !== 0) { + } else if ((binary.BITS4 & info) !== 0) { const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0 // 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. @@ -68,6 +68,7 @@ function * lazyStructReaderGenerator (decoder) { // @ts-ignore Force writing a string here. cantCopyParentInfo ? (decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID()) : null, // parent cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub + (info & binary.BIT5) === binary.BIT5 ? decoder.readRedone() : null, // redone readItemContent(decoder, info) // item content ) yield struct @@ -316,6 +317,7 @@ const sliceStruct = (left, diff) => { leftItem.rightOrigin, leftItem.parent, leftItem.parentSub, + null, leftItem.content.splice(diff) ) } diff --git a/tests/relativePositions.tests.js b/tests/relativePositions.tests.js index ab86168b..db0bfc0e 100644 --- a/tests/relativePositions.tests.js +++ b/tests/relativePositions.tests.js @@ -120,6 +120,6 @@ export const testRelativePositionWithUndo = tc => { t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false)?.index === 6) const ydocClone = new Y.Doc() Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc)) - t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone)?.index === 6) + t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone)?.index === 1) t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone, false)?.index === 6) } diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index 34ae7e9e..7e6ea9d2 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -2079,7 +2079,7 @@ export const testBestCase = _tc => { /** * @type {Y.Item} */ - const n = new Y.Item(Y.createID(0, 0), null, null, null, null, null, null, c) + const n = new Y.Item(Y.createID(0, 0), null, null, null, null, null, null, null, c) // items.push(n) items[i] = n n.right = prevItem