first working version that also considers holes in document updates - #263
This commit is contained in:
parent
004a781a56
commit
f8341220c3
@ -40,3 +40,4 @@ export * from './structs/ContentAny.js'
|
|||||||
export * from './structs/ContentString.js'
|
export * from './structs/ContentString.js'
|
||||||
export * from './structs/ContentType.js'
|
export * from './structs/ContentType.js'
|
||||||
export * from './structs/Item.js'
|
export * from './structs/Item.js'
|
||||||
|
export * from './structs/Skip.js'
|
||||||
|
@ -22,6 +22,9 @@ export class GC extends AbstractStruct {
|
|||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
mergeWith (right) {
|
mergeWith (right) {
|
||||||
|
if (this.constructor !== right.constructor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
this.length += right.length
|
this.length += right.length
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -554,6 +554,7 @@ export class Item extends AbstractStruct {
|
|||||||
*/
|
*/
|
||||||
mergeWith (right) {
|
mergeWith (right) {
|
||||||
if (
|
if (
|
||||||
|
this.constructor === right.constructor &&
|
||||||
compareIDs(right.origin, this.lastId) &&
|
compareIDs(right.origin, this.lastId) &&
|
||||||
this.right === right &&
|
this.right === right &&
|
||||||
compareIDs(this.rightOrigin, right.rightOrigin) &&
|
compareIDs(this.rightOrigin, right.rightOrigin) &&
|
||||||
@ -675,7 +676,7 @@ export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS
|
|||||||
* @type {Array<function(AbstractUpdateDecoder):AbstractContent>}
|
* @type {Array<function(AbstractUpdateDecoder):AbstractContent>}
|
||||||
*/
|
*/
|
||||||
export const contentRefs = [
|
export const contentRefs = [
|
||||||
() => { throw error.unexpectedCase() }, // GC is not ItemContent
|
() => { error.unexpectedCase() }, // GC is not ItemContent
|
||||||
readContentDeleted, // 1
|
readContentDeleted, // 1
|
||||||
readContentJSON, // 2
|
readContentJSON, // 2
|
||||||
readContentBinary, // 3
|
readContentBinary, // 3
|
||||||
@ -684,7 +685,8 @@ export const contentRefs = [
|
|||||||
readContentFormat, // 6
|
readContentFormat, // 6
|
||||||
readContentType, // 7
|
readContentType, // 7
|
||||||
readContentAny, // 8
|
readContentAny, // 8
|
||||||
readContentDoc // 9
|
readContentDoc, // 9
|
||||||
|
() => { error.unexpectedCase() } // 10 - Skip is not ItemContent
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
readItemContent,
|
readItemContent,
|
||||||
readDeleteSet,
|
readDeleteSet,
|
||||||
writeDeleteSet,
|
writeDeleteSet,
|
||||||
|
Skip,
|
||||||
mergeDeleteSets,
|
mergeDeleteSets,
|
||||||
Item, GC, AbstractUpdateDecoder, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
Item, GC, AbstractUpdateDecoder, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
@ -22,7 +23,12 @@ function * lazyStructReaderGenerator (decoder) {
|
|||||||
let clock = decoding.readVarUint(decoder.restDecoder)
|
let clock = decoding.readVarUint(decoder.restDecoder)
|
||||||
for (let i = 0; i < numberOfStructs; i++) {
|
for (let i = 0; i < numberOfStructs; i++) {
|
||||||
const info = decoder.readInfo()
|
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
|
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`
|
// 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.
|
// and we read the next string as parentYKey.
|
||||||
@ -68,7 +74,11 @@ export class LazyStructReader {
|
|||||||
* @return {Item | GC | null}
|
* @return {Item | GC | null}
|
||||||
*/
|
*/
|
||||||
next () {
|
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
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
*/
|
*/
|
||||||
constructor (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.currClient = 0
|
||||||
this.startClock = 0
|
this.startClock = 0
|
||||||
this.written = 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.
|
* 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.
|
* 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
|
* @param {number} diff
|
||||||
* @return {Item | GC}
|
* @return {Item | GC}
|
||||||
*/
|
*/
|
||||||
@ -120,6 +123,9 @@ const sliceStruct = (left, diff) => {
|
|||||||
if (left.constructor === GC) {
|
if (left.constructor === GC) {
|
||||||
const { client, clock } = left.id
|
const { client, clock } = left.id
|
||||||
return new GC(createID(client, clock + diff), left.length - diff)
|
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 {
|
} else {
|
||||||
const leftItem = /** @type {Item} */ (left)
|
const leftItem = /** @type {Item} */ (left)
|
||||||
const { client, clock } = leftItem.id
|
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
|
* @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
|
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
|
// Write higher clients first ⇒ sort by clientID & clock and remove decoders without content
|
||||||
lazyStructDecoders = lazyStructDecoders.filter(dec => dec.curr !== null)
|
lazyStructDecoders = lazyStructDecoders.filter(dec => dec.curr !== null)
|
||||||
lazyStructDecoders.sort(
|
lazyStructDecoders.sort(
|
||||||
/** @type {function(any,any):number} */ (dec1, dec2) =>
|
/** @type {function(any,any):number} */ (dec1, dec2) => {
|
||||||
dec1.curr.id.client === dec2.curr.id.client
|
if (dec1.curr.id.client === dec2.curr.id.client) {
|
||||||
? dec1.curr.id.clock - dec2.curr.id.clock
|
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock
|
||||||
: dec1.curr.id.client - dec2.curr.id.client
|
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) {
|
if (lazyStructDecoders.length === 0) {
|
||||||
break
|
break
|
||||||
@ -187,11 +203,27 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
|
|||||||
currDecoder.next()
|
currDecoder.next()
|
||||||
} else if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) {
|
} 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)
|
// @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) {
|
} else if (currWrite.struct.id.clock + currWrite.struct.length >= curr.id.clock) {
|
||||||
const diff = 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) {
|
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))) {
|
if (!currWrite.struct.mergeWith(/** @type {any} */ (curr))) {
|
||||||
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
||||||
@ -205,7 +237,7 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
|
|||||||
}
|
}
|
||||||
for (
|
for (
|
||||||
let next = currDecoder.curr;
|
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()
|
next = currDecoder.next()
|
||||||
) {
|
) {
|
||||||
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
|
||||||
|
@ -56,9 +56,15 @@ export const testMergeUpdatesWrongOrder = tc => {
|
|||||||
Y.mergeUpdates([updates[1], updates[3]])
|
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()
|
const merged = new Y.Doc()
|
||||||
Y.applyUpdate(merged, updates)
|
Y.applyUpdate(merged, updates)
|
||||||
t.compareArrays(merged.getArray().toArray(), array.toArray())
|
t.compareArrays(merged.getArray().toArray(), array.toArray())
|
||||||
|
t.compare(updates, targetState)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo be able to apply Skip structs to Yjs docs
|
||||||
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user