diff --git a/src/structs/AbstractItem.js b/src/structs/AbstractItem.js index f9b5ffcd..df8eba31 100644 --- a/src/structs/AbstractItem.js +++ b/src/structs/AbstractItem.js @@ -168,6 +168,7 @@ export class AbstractItem extends AbstractStruct { } else { o = parent._start } + // TODO: use something like DeleteSet here (a tree implementation would be best) /** * @type {Set} */ @@ -391,6 +392,20 @@ export class AbstractItem extends AbstractStruct { throw new Error('unimplemented') } + /** + * @param {AbstractItem} right + * @return {boolean} + */ + mergeWith (right) { + if (compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin)) { + this.right = right.right + if (right.right !== null) { + right.right = this + } + return true + } + return false + } /** * Mark this Item as deleted. * diff --git a/src/structs/ItemDeleted.js b/src/structs/ItemDeleted.js index f4efa144..8447e3ef 100644 --- a/src/structs/ItemDeleted.js +++ b/src/structs/ItemDeleted.js @@ -13,7 +13,6 @@ import { changeItemRefOffset, GC, splitItem, - compareIDs, addToDeleteSet, StructStore, Transaction, ID, AbstractType // eslint-disable-line } from '../internals.js' @@ -80,7 +79,7 @@ export class ItemDeleted extends AbstractItem { * @return {boolean} */ mergeWith (right) { - if (compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin)) { + if (super.mergeWith(right)) { this._len += right._len return true } diff --git a/src/structs/ItemJSON.js b/src/structs/ItemJSON.js index a0118f4e..2db923c1 100644 --- a/src/structs/ItemJSON.js +++ b/src/structs/ItemJSON.js @@ -10,7 +10,6 @@ import { getItemType, splitItem, changeItemRefOffset, - compareIDs, GC, ItemDeleted, StructStore, Transaction, ID, AbstractType // eslint-disable-line @@ -75,7 +74,7 @@ export class ItemJSON extends AbstractItem { * @return {boolean} */ mergeWith (right) { - if (compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin)) { + if (super.mergeWith(right)) { this.content = this.content.concat(right.content) return true } diff --git a/src/structs/ItemString.js b/src/structs/ItemString.js index db852532..fa854dc2 100644 --- a/src/structs/ItemString.js +++ b/src/structs/ItemString.js @@ -9,7 +9,6 @@ import { getItemType, splitItem, changeItemRefOffset, - compareIDs, ItemDeleted, GC, StructStore, Transaction, ID, AbstractType // eslint-disable-line @@ -76,7 +75,7 @@ export class ItemString extends AbstractItem { * @return {boolean} */ mergeWith (right) { - if (compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin)) { + if (super.mergeWith(right)) { this.string += right.string return true } diff --git a/src/types/YText.js b/src/types/YText.js index 8896da74..e43717a0 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -36,12 +36,10 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co case ItemString: if (!right.deleted) { if (count < right.length) { - right = getItemCleanStart(store, createID(right.id.client, right.id.clock + count)) - left = right.left - count = 0 - } else { - count -= right.length + // split right + getItemCleanStart(store, createID(right.id.client, right.id.clock + count)) } + count -= right.length } break case ItemFormat: diff --git a/src/utils/DeleteSet.js b/src/utils/DeleteSet.js index c82e1d99..6801eb28 100644 --- a/src/utils/DeleteSet.js +++ b/src/utils/DeleteSet.js @@ -1,7 +1,7 @@ import { - getItemRange, - StructStore, Transaction, ID // eslint-disable-line + findIndexSS, + AbstractItem, StructStore, Transaction, ID // eslint-disable-line } from '../internals.js' import * as math from 'lib0/math.js' @@ -50,7 +50,7 @@ export class DeleteSet { */ export const findIndexDS = (dis, clock) => { let left = 0 - let right = dis.length + let right = dis.length - 1 while (left <= right) { const midindex = math.floor((left + right) / 2) const mid = dis[midindex] @@ -59,11 +59,9 @@ export const findIndexDS = (dis, clock) => { if (clock < midclock + mid.len) { return midindex } - left = midindex - } else if (right !== midindex) { - right = midindex + left = midindex + 1 } else { - break + right = midindex - 1 } } return null @@ -165,18 +163,50 @@ export const writeDeleteSet = (encoder, ds) => { /** * @param {decoding.Decoder} decoder - * @param {StructStore} ss + * @param {StructStore} store * @param {Transaction} transaction */ -export const readDeleteSet = (decoder, ss, transaction) => { +export const readDeleteSet = (decoder, store, transaction) => { const numClients = decoding.readVarUint(decoder) for (let i = 0; i < numClients; i++) { const client = decoding.readVarUint(decoder) - const len = decoding.readVarUint(decoder) - for (let i = 0; i < len; i++) { + const numberOfDeletes = decoding.readVarUint(decoder) + const structs = store.clients.get(client) || [] + const lastStruct = structs[structs.length - 1] + const state = lastStruct.id.clock + lastStruct.length + for (let i = 0; i < numberOfDeletes; i++) { const clock = decoding.readVarUint(decoder) const len = decoding.readVarUint(decoder) - getItemRange(ss, client, clock, len).forEach(struct => struct.delete(transaction)) + if (clock < state) { + let index = findIndexSS(structs, clock) + /** + * We can ignore the case of GC and Delete structs, because we are going to skip them + * @type {AbstractItem} + */ + // @ts-ignore + let struct = structs[index++] + if (!struct.deleted) { + if (struct.id.clock < clock) { + struct = struct.splitAt(store, clock - struct.id.clock) + structs.splice(index, 0, struct) + } + struct.delete(transaction) + } + while (index < structs.length) { + // @ts-ignore + struct = structs[index++] + if (struct.id.clock < clock + len) { + if (!struct.deleted) { + if (clock + len < struct.id.clock + struct.length) { + structs.splice(index, 0, struct.splitAt(store, clock + len - struct.id.clock)) + } + struct.delete(transaction) + } + } else { + break + } + } + } } } } diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js index 77628ff1..601b46e7 100644 --- a/src/utils/StructStore.js +++ b/src/utils/StructStore.js @@ -97,12 +97,9 @@ export const findIndexSS = (structs, clock) => { if (clock < midclock + mid.length) { return midindex } - if (left === midindex) { - throw error.unexpectedCase() - } - left = midindex + left = midindex + 1 } else { - right = midindex + right = midindex - 1 } } throw error.unexpectedCase() @@ -193,47 +190,6 @@ export const getItemCleanEnd = (store, id) => { return struct } -/** - * Expects that id is actually in store. This function throws or is an infinite loop otherwise. - * @param {StructStore} store - * @param {number} client - * @param {number} clock - * @param {number} len - * @return {Array} - * - * @private - */ -export const getItemRange = (store, client, clock, len) => { - /** - * @type {Array} - */ - // @ts-ignore - const structs = store.clients.get(client) - let index = findIndexSS(structs, clock) - let struct = structs[index] - let range = [] - if (struct.id.clock <= clock) { - if (struct.id.clock < clock) { - struct = struct.splitAt(store, clock - struct.id.clock) - structs.splice(index + 1, 0, struct) - } - range.push(struct) - } - index++ - while (index < structs.length) { - struct = structs[index++] - if (struct.id.clock < clock + len) { - range.push(struct) - } else { - break - } - } - if (struct.id.clock < clock + len && struct.id.clock + struct.length > clock + len) { - structs.splice(index + 1, 0, struct.splitAt(store, clock + len - struct.id.clock)) - } - return range -} - /** * Replace `item` with `newitem` in store * @param {StructStore} store diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index ee0e19d8..7ee2179f 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -67,7 +67,7 @@ export class YEvent { * @return {boolean} */ adds (struct) { - return struct.id.clock > (this.transaction.beforeState.get(struct.id.client) || 0) + return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0) } } diff --git a/src/utils/structEncoding.js b/src/utils/structEncoding.js index 887f9e49..e324c00a 100644 --- a/src/utils/structEncoding.js +++ b/src/utils/structEncoding.js @@ -77,6 +77,11 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeStruct export const writeStructs = (encoder, store, _sm) => { // we filter all valid _sm entries into sm const sm = new Map() + const encoderUserPosMap = map.create() + const startMessagePos = encoding.length(encoder) + // write diff to pos of end of this message + // we use it in readStructs to jump ahead to the end of the message + encoding.writeUint32(encoder, 0) _sm.forEach((clock, client) => { if (getState(store, client) > clock) { sm.set(client, clock) @@ -87,7 +92,6 @@ export const writeStructs = (encoder, store, _sm) => { sm.set(client, 0) } }) - const encoderUserPosMap = map.create() // write # states that were updated encoding.writeVarUint(encoder, sm.size) sm.forEach((clock, client) => { @@ -95,11 +99,11 @@ export const writeStructs = (encoder, store, _sm) => { writeID(encoder, createID(client, clock)) encoderUserPosMap.set(client, encoding.length(encoder)) // write diff to pos where structs are written - // We will fill out this value later *) encoding.writeUint32(encoder, 0) }) sm.forEach((clock, client) => { const decPos = encoderUserPosMap.get(client) + // fill out diff to pos where structs are written encoding.setUint32(encoder, decPos, encoding.length(encoder) - decPos) /** * @type {Array} @@ -116,6 +120,8 @@ export const writeStructs = (encoder, store, _sm) => { structs[i].write(encoder, 0, 0) } }) + // fill out diff to pos of end of message + encoding.setUint32(encoder, startMessagePos, encoding.length(encoder) - startMessagePos) } /** @@ -134,20 +140,26 @@ export const readStructs = (decoder, transaction, store) => { * @type {Map>} */ const structReaders = new Map() + const endOfMessagePos = decoder.pos + decoding.readUint32(decoder) const clientbeforeState = decoding.readVarUint(decoder) /** + * Stack of pending structs waiting for struct dependencies. + * Maximum length of stack is structReaders.size. * @type {Array} */ const stack = [] const localState = getStates(store) - let lastStructReader = null for (let i = 0; i < clientbeforeState; i++) { const nextID = readID(decoder) const decoderPos = decoder.pos + decoding.readUint32(decoder) - lastStructReader = decoding.clone(decoder, decoderPos) - const numberOfStructs = decoding.readVarUint(lastStructReader) - structReaders.set(nextID.client, createStructReaderIterator(lastStructReader, numberOfStructs, nextID, localState.get(nextID.client) || 0)) + const structReaderDecoder = decoding.clone(decoder, decoderPos) + const numberOfStructs = decoding.readVarUint(structReaderDecoder) + structReaders.set(nextID.client, createStructReaderIterator(structReaderDecoder, numberOfStructs, nextID, localState.get(nextID.client) || 0)) } + // Decoder is still stuck at creating struct readers. + // Jump ahead to end of message so that reading can continue. + // We will use the created struct readers for the remaining part of this workflow. + decoder.pos = endOfMessagePos for (const it of structReaders.values()) { // todo try for in of it for (let res = it.next(); !res.done; res = it.next()) { @@ -173,8 +185,4 @@ export const readStructs = (decoder, transaction, store) => { } } } - // if we read some structs, this points to the end of the transaction - if (lastStructReader !== null) { - decoder.pos = lastStructReader.pos - } } diff --git a/tests/y-array.tests.js b/tests/y-array.tests.js index ae0f9162..7b51e271 100644 --- a/tests/y-array.tests.js +++ b/tests/y-array.tests.js @@ -310,7 +310,7 @@ const arrayTransactions = [ * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests20 = tc => { - applyRandomTests(tc, arrayTransactions, 20) + applyRandomTests(tc, arrayTransactions, 2) } /**