UNPKG

@y/y

Version:

Shared Editing Library

276 lines (267 loc) • 10.3 kB
import * as decoding from 'lib0/decoding' import * as binary from 'lib0/binary' import * as map from 'lib0/map' import * as array from 'lib0/array' import * as encoding from 'lib0/encoding' import * as number from 'lib0/number' import { createID, ID } from './ID.js' import { Item } from '../structs/Item.js' import { readItemContent } from '../ynode.js' import { findIndexCleanStart } from './transaction-helpers.js' import { Skip } from '../structs/Skip.js' import { createIdSet, IdRange } from './ids.js' import { sliceStruct } from './updates.js' import { GC } from '../structs/GC.js' import { writeStructs } from './encoding-helpers.js' /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from. * @return {BlockSet} * * @private * @function */ export const readBlockSet = (decoder) => { const clientRefs = new BlockSet() const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numOfStateUpdates; i++) { const numberOfBlocks = decoding.readVarUint(decoder.restDecoder) /** * @type {Array<GC|Item|Skip>} */ const refs = new Array(numberOfBlocks) const client = decoder.readClient() let clock = decoding.readVarUint(decoder.restDecoder) clientRefs.clients.set(client, new BlockRange(refs)) for (let i = 0; i < numberOfBlocks; i++) { const info = decoder.readInfo() switch (binary.BITS5 & info) { case 0: { // GC const len = decoder.readLen() refs[i] = new GC(createID(client, clock), len) clock += len break } case 10: { // Skip Block (nothing to apply) // @todo we could reduce the amount of checks by adding Skip block to clientRefs so we know that something is missing. const len = decoding.readVarUint(decoder.restDecoder) refs[i] = new Skip(createID(client, clock), len) clock += len break } default: { // Item with content /** * The optimized implementation doesn't use any variables because inlining variables is faster. * Below a non-optimized version is shown that implements the basic algorithm with * a few comments */ 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. // It indicates how we store/retrieve parent from `y.share` // @type {string|null} const block = new Item( createID(client, clock), null, // left (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin null, // right (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin cantCopyParentInfo ? (decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID()) : null, // parent cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub readItemContent(decoder, info) // item content ) refs[i] = block clock += block.length } } } } return clientRefs } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {BlockSet} blocks */ export const writeBlockSet = (encoder, blocks) => { // write # states that were updated encoding.writeVarUint(encoder.restEncoder, blocks.clients.size) // Write items with higher client ids first // This heavily improves the conflict algorithm. array.from(blocks.clients.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, blockrange]) => { writeStructs(encoder, blockrange.refs, client, [new IdRange(0, number.MAX_SAFE_INTEGER)]) }) } class BlockRange { /** * @param {Array<Item|GC|Skip>} refs */ constructor (refs) { this.i = 0 /** * @type {Array<Item | GC | Skip>} */ this.refs = refs } } export class BlockSet { constructor () { /** * @type {Map<number, BlockRange>} */ this.clients = map.create() } toIdSet () { const inserts = createIdSet() this.clients.forEach((ranges, clientid) => { let lastClock = 0 let lastLen = 0 ranges.refs.forEach(block => { if (block instanceof Skip) return if (lastClock + lastLen === block.id.clock) { // default case: extend prev entry lastLen += block.length } else { lastLen > 0 && inserts.add(clientid, lastClock, lastLen) lastClock = block.id.clock lastLen = block.length } }) inserts.add(clientid, lastClock, lastLen) }) return inserts } /** * Remove id-ranges from update - convert them to skip if applicable. * * @param {IdSet} exclude */ exclude (exclude) { const clientids = this.clients.size < exclude.clients.size ? this.clients.keys() : exclude.clients.keys() for (const client of clientids) { const range = exclude.clients.get(client) const structs = this.clients.get(client)?.refs if (range == null || structs == null) return const firstStruct = structs[0] const lastStruct = structs[structs.length - 1] const idranges = range.getIds() for (let i = 0; i < idranges.length; i++) { const range = idranges[i] let startIndex = 0 if (range.clock >= lastStruct.id.clock + lastStruct.length) continue if (range.clock > firstStruct.id.clock) { startIndex = findIndexCleanStart(null, structs, range.clock) } let endIndex = structs.length // must be set here, after structs is modified if (range.clock + range.len <= firstStruct.id.clock) continue if (range.clock + range.len < lastStruct.id.clock + lastStruct.length) { endIndex = findIndexCleanStart(null, structs, range.clock + range.len) } if (startIndex < endIndex) { structs[startIndex] = new Skip(new ID(client, range.clock), range.len) const d = endIndex - startIndex if (d > 1) { structs.splice(startIndex + 1, d - 1) } } } } } /** * Insert the blocks of a later update into this BlockSet. * * The same id always denotes the same operation, but the encodings may differ (e.g. if one of * them is garbage-collected). We always retain the blocks that are already in this BlockSet. * `inserts` only fills the gaps. This is also how `applyUpdate` behaves. * * @param {BlockSet} inserts */ insertInto (inserts) { inserts.clients.forEach((newranges, clientid) => { const ranges = this.clients.get(clientid) if (ranges == null) { this.clients.set(clientid, newranges) } else { const localIsLeft = ranges.refs[0].id.clock < newranges.refs[0].id.clock const leftRanges = (localIsLeft ? ranges : newranges).refs const rightRanges = (localIsLeft ? newranges : ranges).refs const lastBlockLeft = array.last(leftRanges) const firstBlockRight = rightRanges[0] const gapSize = firstBlockRight.id.clock - lastBlockLeft.id.clock - lastBlockLeft.length if (gapSize >= 0) { // we can do a simple efficient merge if (gapSize > 0) { leftRanges.push(new Skip(new ID(clientid, lastBlockLeft.id.clock + lastBlockLeft.length), gapSize)) } for (let i = 0; i < rightRanges.length; i++) { leftRanges.push(rightRanges[i]) } ranges.refs = leftRanges } else { // requires more computation because we need to filter duplicates const localRanges = ranges.refs const newRanges = newranges.refs /** * @type {Array<GC|Item|Skip>} */ const result = [] // everything before `clock` is already handled let clock = leftRanges[0].id.clock let ni = 0 /** * @type {Item|GC|Skip|undefined} */ let nblock = newRanges[0] const localLen = localRanges.length for (let li = 0; li <= localLen; li++) { const lblock = localRanges[li] if (lblock !== undefined) { if (lblock.constructor === Skip) continue if (lblock.id.clock === clock) { // default case: no gap result.push(lblock) clock += lblock.length continue } } // Fill the gap between `clock` and the next local block (`end`) using the new blocks. // After the last local block, we add all remaining new blocks. const end = lblock === undefined ? number.MAX_SAFE_INTEGER : lblock.id.clock while (clock < end && nblock !== undefined) { const nclock = nblock.id.clock if (nclock >= end) break const nend = nclock + nblock.length if (nend <= clock || nblock.constructor === Skip) { nblock = newRanges[++ni] continue } if (nclock < clock) { nblock = sliceStruct(nblock, clock - nclock) } else if (nclock > clock) { result.push(new Skip(new ID(clientid, clock), nclock - clock)) clock = nclock } result.push(nblock) if (nend > end) { // only the beginning fits into the gap, the rest might fit into another gap const rest = sliceStruct(nblock, end - clock) nblock.length = end - clock nblock = rest clock = end } else { clock = nend nblock = newRanges[++ni] } } if (lblock === undefined) break if (clock < end) { result.push(new Skip(new ID(clientid, clock), end - clock)) } result.push(lblock) clock = end + lblock.length } ranges.refs = result } } }) inserts.clients.clear() } }