import { findIndexSS, getState, splitItem, iterateStructs, UpdateEncoderV2, IdMap, AttrRanges, AbstractStruct, DSDecoderV1, IdSetEncoderV1, DSDecoderV2, IdSetEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line } from '../internals.js' import * as array from 'lib0/array' import * as math from 'lib0/math' import * as encoding from 'lib0/encoding' import * as decoding from 'lib0/decoding' export class IdRange { /** * @param {number} clock * @param {number} len */ constructor (clock, len) { /** * @type {number} */ this.clock = clock /** * @type {number} */ this.len = len } /** * @param {number} clock * @param {number} len */ copyWith (clock, len) { return new IdRange(clock, len) } } class IdRanges { /** * @param {Array} ids */ constructor (ids) { this.sorted = false /** * @private */ this._ids = ids } copy () { return new IdRanges(this._ids.slice()) } /** * @param {number} clock * @param {number} length */ add (clock, length) { const last = this._ids[this._ids.length - 1] if (last.clock + last.len === clock) { this._ids[this._ids.length - 1] = new IdRange(last.clock, last.len + length) } else { this.sorted = false this._ids.push(new IdRange(clock, length)) } } /** * Return the list of id ranges, sorted and merged. */ getIds () { const ids = this._ids if (!this.sorted) { this.sorted = true ids.sort((a, b) => a.clock - b.clock) // merge items without filtering or splicing the array // i is the current pointer // j refers to the current insert position for the pointed item // try to merge dels[i] into dels[j-1] or set dels[j]=dels[i] let i, j for (i = 1, j = 1; i < ids.length; i++) { const left = ids[j - 1] const right = ids[i] if (left.clock + left.len >= right.clock) { const r = right.clock + right.len - left.clock if (left.len < r) { ids[j - 1] = new IdRange(left.clock, r) } } else if (left.len === 0) { ids[j - 1] = right } else { if (j < i) { ids[j] = right } j++ } } ids.length = ids[j - 1].len === 0 ? j - 1 : j } return ids } } export class IdSet { constructor () { /** * @type {Map} */ this.clients = new Map() } /** * @param {(idrange:IdRange, client:number) => void} f */ forEach (f) { this.clients.forEach((ranges, client) => { ranges.getIds().forEach((range) => { f(range, client) }) }) } /** * @param {ID} id * @return {boolean} */ hasId (id) { return this.has(id.client, id.clock) } /** * @param {number} client * @param {number} clock */ has (client, clock) { const dr = this.clients.get(client) if (dr) { return findIndexInIdRanges(dr.getIds(), clock) !== null } return false } /** * @param {number} client * @param {number} clock * @param {number} len */ add (client, clock, len) { addToIdSet(this, client, clock, len) } /** * @param {number} client * @param {number} clock * @param {number} len */ delete (client, clock, len) { _deleteRangeFromIdSet(this, client, clock, len) } } /** * @param {IdSet | IdMap} set * @param {number} client * @param {number} clock * @param {number} len */ export const _deleteRangeFromIdSet = (set, client, clock, len) => { const dr = set.clients.get(client) if (dr && len > 0) { const ids = dr.getIds() let index = findRangeStartInIdRanges(ids, clock) if (index != null) { for (let r = ids[index]; index < ids.length && r.clock < clock + len; r = ids[++index]) { if (r.clock < clock) { ids[index] = r.copyWith(r.clock, clock - r.clock) if (clock + len < r.clock + r.len) { ids.splice(index + 1, 0, r.copyWith(clock + len, r.clock + r.len - clock - len)) } } else if (clock + len < r.clock + r.len) { // need to retain end ids[index] = r.copyWith(clock + len, r.clock + r.len - clock - len) } else if (ids.length === 1) { set.clients.delete(client) return } else { ids.splice(index--, 1) } } } } } /** * Iterate over all structs that are mentioned by the IdSet. * * @param {Transaction} transaction * @param {IdSet} ds * @param {function(GC|Item):void} f * * @function */ export const iterateStructsByIdSet = (transaction, ds, f) => ds.clients.forEach((idRanges, clientid) => { const ranges = idRanges.getIds() const structs = /** @type {Array} */ (transaction.doc.store.clients.get(clientid)) if (structs != null) { for (let i = 0; i < ranges.length; i++) { const del = ranges[i] iterateStructs(transaction, structs, del.clock, del.len, f) } } }) /** * @param {Array} dis * @param {number} clock * @return {number|null} * * @private * @function */ export const findIndexInIdRanges = (dis, clock) => { let left = 0 let right = dis.length - 1 while (left <= right) { const midindex = math.floor((left + right) / 2) const mid = dis[midindex] const midclock = mid.clock if (midclock <= clock) { if (clock < midclock + mid.len) { return midindex } left = midindex + 1 } else { right = midindex - 1 } } return null } /** * Find the first range that contains clock or comes after clock. * * @param {Array} dis * @param {number} clock * @return {number|null} * * @private * @function */ export const findRangeStartInIdRanges = (dis, clock) => { let left = 0 let right = dis.length - 1 while (left <= right) { const midindex = math.floor((left + right) / 2) const mid = dis[midindex] const midclock = mid.clock if (midclock <= clock) { if (clock < midclock + mid.len) { return midindex } left = midindex + 1 } else { right = midindex - 1 } } return left < dis.length ? left : null } /** * @param {Array} idSets * @return {IdSet} A fresh IdSet */ export const mergeIdSets = idSets => { const merged = new IdSet() for (let dssI = 0; dssI < idSets.length; dssI++) { idSets[dssI].clients.forEach((rangesLeft, client) => { if (!merged.clients.has(client)) { // Write all missing keys from current ds and all following. // If merged already contains `client` current ds has already been added. const ids = rangesLeft.getIds().slice() for (let i = dssI + 1; i < idSets.length; i++) { const nextIds = idSets[i].clients.get(client) if (nextIds) { array.appendTo(ids, nextIds.getIds()) } } merged.clients.set(client, new IdRanges(ids)) } }) } return merged } /** * @template {IdSet | IdMap} S * @param {S} dest * @param {S} src */ export const _insertIntoIdSet = (dest, src) => { src.clients.forEach((srcRanges, client) => { const targetRanges = dest.clients.get(client) if (targetRanges) { array.appendTo(targetRanges.getIds(), srcRanges.getIds()) targetRanges.sorted = false } else { const res = srcRanges.copy() res.sorted = true dest.clients.set(client, /** @type {any} */ (res)) } }) } /** * @param {IdSet} dest * @param {IdSet} src */ export const insertIntoIdSet = _insertIntoIdSet /** * Remove all ranges from `exclude` from `ds`. The result is a fresh IdSet containing all ranges from `idSet` that are not * in `exclude`. * * @template {IdSet | IdMap} Set * @param {Set} set * @param {IdSet | IdMap} exclude * @return {Set} */ export const _diffSet = (set, exclude) => { /** * @type {Set} */ const res = /** @type {any } */ (set instanceof IdSet ? new IdSet() : new IdMap()) const Ranges = set instanceof IdSet ? IdRanges : AttrRanges set.clients.forEach((_setRanges, client) => { /** * @type {Array} */ let resRanges = [] const _excludedRanges = exclude.clients.get(client) const setRanges = _setRanges.getIds() if (_excludedRanges == null) { resRanges = setRanges.slice() } else { const excludedRanges = _excludedRanges.getIds() let i = 0; let j = 0 let currRange = setRanges[0] while (i < setRanges.length && j < excludedRanges.length) { const e = excludedRanges[j] if (currRange.clock + currRange.len <= e.clock) { // no overlapping, use next range item if (currRange.len > 0) resRanges.push(currRange) currRange = setRanges[++i] } else if (e.clock + e.len <= currRange.clock) { // no overlapping, use next excluded item j++ } else if (e.clock <= currRange.clock) { // exclude laps into range (we already know that the ranges somehow collide) const newClock = e.clock + e.len const newLen = currRange.clock + currRange.len - newClock if (newLen > 0) { currRange = currRange.copyWith(newClock, newLen) j++ } else { // this item is completely overwritten. len=0. We can jump to the next range currRange = setRanges[++i] } } else { // currRange.clock < e.clock -- range laps into exclude => adjust len // beginning can't be empty, add it to the result const nextLen = e.clock - currRange.clock resRanges.push(currRange.copyWith(currRange.clock, nextLen)) // retain the remaining length after exclude in currRange currRange = currRange.copyWith(currRange.clock + e.len + nextLen, math.max(currRange.len - e.len - nextLen, 0)) if (currRange.len === 0) currRange = setRanges[++i] else j++ } } if (currRange != null) { resRanges.push(currRange) } i++ while (i < setRanges.length) { resRanges.push(setRanges[i++]) } } // @ts-ignore if (resRanges.length > 0) res.clients.set(client, /** @type {any} */ (new Ranges(resRanges))) }) return res } /** * Remove all ranges from `exclude` from `idSet`. The result is a fresh IdSet containing all ranges from `idSet` that are not * in `exclude`. * * @type {(idSet: IdSet, exclude: IdSet|IdMap) => IdSet} */ export const diffIdSet = _diffSet /** * @param {IdSet} idSet * @param {number} client * @param {number} clock * @param {number} length * * @private * @function */ export const addToIdSet = (idSet, client, clock, length) => { if (length === 0) return const idRanges = idSet.clients.get(client) if (idRanges) { idRanges.add(clock, length) } else { idSet.clients.set(client, new IdRanges([new IdRange(clock, length)])) } } /** * @param {IdSet} idSet * @param {AbstractStruct} struct * * @private * @function */ export const addStructToIdSet = (idSet, struct) => addToIdSet(idSet, struct.id.client, struct.id.clock, struct.length) export const createIdSet = () => new IdSet() /** * @param {StructStore} ss * @return {IdSet} * * @private * @function */ export const createDeleteSetFromStructStore = ss => { const ds = createIdSet() ss.clients.forEach((structs, client) => { /** * @type {Array} */ const dsitems = [] for (let i = 0; i < structs.length; i++) { const struct = structs[i] if (struct.deleted) { const clock = struct.id.clock let len = struct.length if (i + 1 < structs.length) { for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) { len += next.length } } dsitems.push(new IdRange(clock, len)) } } if (dsitems.length > 0) { ds.clients.set(client, new IdRanges(dsitems)) } }) return ds } /** * @param {import('../internals.js').StructStore} ss * @param {boolean} filterDeleted */ export const createInsertionSetFromStructStore = (ss, filterDeleted) => { const idset = createIdSet() ss.clients.forEach((structs, client) => { /** * @type {Array} */ const iditems = [] for (let i = 0; i < structs.length; i++) { const struct = structs[i] if (!(filterDeleted && struct.deleted)) { const clock = struct.id.clock let len = struct.length if (i + 1 < structs.length) { // eslint-disable-next-line for (let next = structs[i + 1]; i + 1 < structs.length && !(filterDeleted && next.deleted); next = structs[++i + 1]) { len += next.length } } iditems.push(new IdRange(clock, len)) } } if (iditems.length > 0) { idset.clients.set(client, new IdRanges(iditems)) } }) return idset } /** * @param {IdSetEncoderV1 | IdSetEncoderV2} encoder * @param {IdSet} idSet * * @private * @function */ export const writeIdSet = (encoder, idSet) => { encoding.writeVarUint(encoder.restEncoder, idSet.clients.size) // Ensure that the delete set is written in a deterministic order array.from(idSet.clients.entries()) .sort((a, b) => b[0] - a[0]) .forEach(([client, _idRanges]) => { const idRanges = _idRanges.getIds() encoder.resetIdSetCurVal() encoding.writeVarUint(encoder.restEncoder, client) const len = idRanges.length encoding.writeVarUint(encoder.restEncoder, len) for (let i = 0; i < len; i++) { const item = idRanges[i] encoder.writeIdSetClock(item.clock) encoder.writeIdSetLen(item.len) } }) } /** * @param {DSDecoderV1 | DSDecoderV2} decoder * @return {IdSet} * * @private * @function */ export const readIdSet = decoder => { const ds = new IdSet() const numClients = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numClients; i++) { decoder.resetDsCurVal() const client = decoding.readVarUint(decoder.restDecoder) const numberOfDeletes = decoding.readVarUint(decoder.restDecoder) if (numberOfDeletes > 0) { /** * @type {Array} */ const dsRanges = [] for (let i = 0; i < numberOfDeletes; i++) { dsRanges.push(new IdRange(decoder.readDsClock(), decoder.readDsLen())) } ds.clients.set(client, new IdRanges(dsRanges)) } } return ds } /** * @todo YDecoder also contains references to String and other Decoders. Would make sense to exchange YDecoder.toUint8Array for YDecoder.DsToUint8Array().. */ /** * @param {DSDecoderV1 | DSDecoderV2} decoder * @param {Transaction} transaction * @param {StructStore} store * @return {Uint8Array|null} Returns a v2 update containing all deletes that couldn't be applied yet; or null if all deletes were applied successfully. * * @private * @function */ export const readAndApplyDeleteSet = (decoder, transaction, store) => { const unappliedDS = new IdSet() const numClients = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numClients; i++) { decoder.resetDsCurVal() const client = decoding.readVarUint(decoder.restDecoder) const numberOfDeletes = decoding.readVarUint(decoder.restDecoder) const structs = store.clients.get(client) || [] const state = getState(store, client) for (let i = 0; i < numberOfDeletes; i++) { const clock = decoder.readDsClock() const clockEnd = clock + decoder.readDsLen() if (clock < state) { if (state < clockEnd) { addToIdSet(unappliedDS, client, state, clockEnd - state) } let index = findIndexSS(structs, clock) /** * We can ignore the case of GC and Delete structs, because we are going to skip them * @type {Item} */ // @ts-ignore let struct = structs[index] // split the first item if necessary if (!struct.deleted && struct.id.clock < clock) { structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)) index++ // increase we now want to use the next struct } while (index < structs.length) { // @ts-ignore struct = structs[index++] if (struct.id.clock < clockEnd) { if (!struct.deleted) { if (clockEnd < struct.id.clock + struct.length) { structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock)) } struct.delete(transaction) } } else { break } } } else { addToIdSet(unappliedDS, client, clock, clockEnd - clock) } } } if (unappliedDS.clients.size > 0) { const ds = new UpdateEncoderV2() encoding.writeVarUint(ds.restEncoder, 0) // encode 0 structs writeIdSet(ds, unappliedDS) return ds.toUint8Array() } return null } /** * @param {IdSet} ds1 * @param {IdSet} ds2 */ export const equalIdSets = (ds1, ds2) => { if (ds1.clients.size !== ds2.clients.size) return false for (const [client, _deleteItems1] of ds1.clients.entries()) { const deleteItems1 = _deleteItems1.getIds() const deleteItems2 = ds2.clients.get(client)?.getIds() if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false for (let i = 0; i < deleteItems1.length; i++) { const di1 = deleteItems1[i] const di2 = deleteItems2[i] if (di1.clock !== di2.clock || di1.len !== di2.len) { return false } } } return true }