diff --git a/src/internals.js b/src/internals.js index 5d32f0ad..bc386f0a 100644 --- a/src/internals.js +++ b/src/internals.js @@ -40,3 +40,4 @@ export * from './structs/ContentAny.js' export * from './structs/ContentString.js' export * from './structs/ContentType.js' export * from './structs/Item.js' +export * from './structs/Skip.js' diff --git a/src/structs/GC.js b/src/structs/GC.js index 0b9e4244..110f68b6 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -22,6 +22,9 @@ export class GC extends AbstractStruct { * @return {boolean} */ mergeWith (right) { + if (this.constructor !== right.constructor) { + return false + } this.length += right.length return true } diff --git a/src/structs/Item.js b/src/structs/Item.js index b4c10074..c63dff84 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -554,6 +554,7 @@ export class Item extends AbstractStruct { */ mergeWith (right) { if ( + this.constructor === right.constructor && compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin) && @@ -675,7 +676,7 @@ export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS * @type {Array} */ export const contentRefs = [ - () => { throw error.unexpectedCase() }, // GC is not ItemContent + () => { error.unexpectedCase() }, // GC is not ItemContent readContentDeleted, // 1 readContentJSON, // 2 readContentBinary, // 3 @@ -684,7 +685,8 @@ export const contentRefs = [ readContentFormat, // 6 readContentType, // 7 readContentAny, // 8 - readContentDoc // 9 + readContentDoc, // 9 + () => { error.unexpectedCase() } // 10 - Skip is not ItemContent ] /** diff --git a/src/utils/updates.js b/src/utils/updates.js index c2285cce..e0b6733c 100644 --- a/src/utils/updates.js +++ b/src/utils/updates.js @@ -7,6 +7,7 @@ import { readItemContent, readDeleteSet, writeDeleteSet, + Skip, mergeDeleteSets, Item, GC, AbstractUpdateDecoder, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line } from '../internals.js' @@ -22,7 +23,12 @@ function * lazyStructReaderGenerator (decoder) { let clock = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numberOfStructs; i++) { const info = decoder.readInfo() - if ((binary.BITS5 & info) !== 0) { + // @todo use switch instead of ifs + if (info === 10) { + const len = decoder.readLen() + yield new Skip(createID(client, clock), len) + clock += len + } else if ((binary.BITS5 & 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,7 +74,11 @@ export class LazyStructReader { * @return {Item | GC | null} */ next () { - return (this.curr = this.gen.next().value || null) + // ignore "Skip" structs + do { + this.curr = this.gen.next().value || null + } while (this.curr !== null && this.curr.constructor === Skip) + return this.curr } } @@ -77,13 +87,6 @@ export class LazyStructWriter { * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ constructor (encoder) { - /** - * We keep the last written struct around in case we want to - * merge it with the next written struct. - * When a new struct is received: if mergeable ⇒ merge; otherwise ⇒ write curr and keep new struct around. - * @type {null | Item | GC} - */ - this.curr = null this.currClient = 0 this.startClock = 0 this.written = 0 @@ -112,7 +115,7 @@ export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1, * This method is intended to slice any kind of struct and retrieve the right part. * It does not handle side-effects, so it should only be used by the lazy-encoder. * - * @param {Item | GC} left + * @param {Item | GC | Skip} left * @param {number} diff * @return {Item | GC} */ @@ -120,6 +123,9 @@ const sliceStruct = (left, diff) => { if (left.constructor === GC) { const { client, clock } = left.id return new GC(createID(client, clock + diff), left.length - diff) + } else if (left.constructor === Skip) { + const { client, clock } = left.id + return new Skip(createID(client, clock + diff), left.length - diff) } else { const leftItem = /** @type {Item} */ (left) const { client, clock } = leftItem.id @@ -151,7 +157,7 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U /** * @todo we don't need offset because we always slice before - * @type {null | { struct: Item | GC, offset: number }} + * @type {null | { struct: Item | GC | Skip, offset: number }} */ let currWrite = null @@ -167,10 +173,20 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U // Write higher clients first ⇒ sort by clientID & clock and remove decoders without content lazyStructDecoders = lazyStructDecoders.filter(dec => dec.curr !== null) lazyStructDecoders.sort( - /** @type {function(any,any):number} */ (dec1, dec2) => - dec1.curr.id.client === dec2.curr.id.client - ? dec1.curr.id.clock - dec2.curr.id.clock - : dec1.curr.id.client - dec2.curr.id.client + /** @type {function(any,any):number} */ (dec1, dec2) => { + if (dec1.curr.id.client === dec2.curr.id.client) { + const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock + if (clockDiff === 0) { + return dec1.curr.constructor === dec2.curr.constructor ? 0 : ( + dec1.curr.constructor === Skip ? 1 : -1 + ) + } else { + return clockDiff + } + } else { + return dec2.curr.id.client - dec1.curr.id.client + } + } ) if (lazyStructDecoders.length === 0) { break @@ -187,11 +203,27 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U currDecoder.next() } else if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) { // @todo write currStruct & set currStruct = Skip(clock = currStruct.id.clock + currStruct.length, length = curr.id.clock - self.clock) - throw new Error('unhandled case') // @Todo ! + if (currWrite.struct.constructor === Skip) { + // extend existing skip + currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock + } else { + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) + const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length + /** + * @type {Skip} + */ + const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff) + currWrite = { struct, offset: 0 } + } } else if (currWrite.struct.id.clock + currWrite.struct.length >= curr.id.clock) { const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock if (diff > 0) { - curr = sliceStruct(curr, diff) + if (currWrite.struct.constructor === Skip) { + // prefer to slice Skip because the other struct might contain more information + currWrite.struct.length -= diff + } else { + curr = sliceStruct(curr, diff) + } } if (!currWrite.struct.mergeWith(/** @type {any} */ (curr))) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) @@ -205,7 +237,7 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U } for ( let next = currDecoder.curr; - next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length; // @Todo && next.constructor !== skippable + next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip; next = currDecoder.next() ) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) diff --git a/tests/updates.tests.js b/tests/updates.tests.js index 70084273..108c5535 100644 --- a/tests/updates.tests.js +++ b/tests/updates.tests.js @@ -56,9 +56,15 @@ export const testMergeUpdatesWrongOrder = tc => { Y.mergeUpdates([updates[1], updates[3]]) ]) - ;[wrongOrder, overlapping, separated].forEach(updates => { + const targetState = Y.encodeStateAsUpdate(ydoc) + ;[wrongOrder, overlapping, separated].forEach((updates, i) => { const merged = new Y.Doc() Y.applyUpdate(merged, updates) t.compareArrays(merged.getArray().toArray(), array.toArray()) + t.compare(updates, targetState) }) } + +/** + * @todo be able to apply Skip structs to Yjs docs + */