@y/y
Version:
Shared Editing Library
276 lines (267 loc) • 10.3 kB
JavaScript
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()
}
}