@y/y
Version:
Shared Editing Library
292 lines (283 loc) • 11 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 math from 'lib0/math'
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 '../ytype.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)
}
}
}
}
}
/**
* @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
/**
* @type {Array<GC|Item|Skip>}
*/
const result = []
let nextExpectedClock = leftRanges[0].id.clock
/**
* @param {Item|GC|Skip} block
*/
const addToResult = block => {
result.push(block)
nextExpectedClock = block.id.clock + block.length
}
let li = 0
let ri = 0
/**
* @type {Item|GC|Skip|undefined}
*/
let lblock = leftRanges[li]
/**
* @type {Item|GC|Skip|undefined}
*/
let rblock = rightRanges[ri]
const applyLeft = () => {
if (lblock === undefined) return
// first try to consume left
// left: filter skips and known ops
while (lblock !== undefined && (lblock.constructor === Skip || lblock.id.clock + lblock.length <= nextExpectedClock)) {
lblock = leftRanges[++li]
}
// left: trim first op
if (lblock !== undefined && lblock.id.clock < nextExpectedClock && lblock.id.clock + lblock.length > nextExpectedClock) {
lblock = sliceStruct(lblock, lblock.id.clock + lblock.length - nextExpectedClock)
}
// left: add to result
while (lblock !== undefined && lblock.id.clock === nextExpectedClock && lblock.constructor !== Skip) {
addToResult(lblock)
lblock = leftRanges[++li]
}
}
const applyRight = () => {
// right: filter skips and known ops
while (rblock !== undefined && (rblock.constructor === Skip || rblock.id.clock + rblock.length <= nextExpectedClock)) {
rblock = rightRanges[++ri]
}
// right: trim first op
if (rblock !== undefined && rblock.id.clock < nextExpectedClock && rblock.id.clock + rblock.length > nextExpectedClock) {
rblock = sliceStruct(rblock, rblock.id.clock + rblock.length - nextExpectedClock)
}
// right: add to result
while (rblock !== undefined && rblock.id.clock === nextExpectedClock && rblock.constructor !== Skip) {
addToResult(rblock)
rblock = rightRanges[++ri]
}
}
for (; li < leftRanges.length && ri < rightRanges.length;) {
applyLeft()
applyRight()
// add skip if necessary
const minNextClock = math.min(lblock?.id.clock || 0, rblock?.id.clock || 0)
const gapSize = minNextClock - nextExpectedClock
if (gapSize > 0) {
addToResult(new Skip(new ID(clientid, nextExpectedClock), gapSize))
}
}
while (li < leftRanges.length) {
applyLeft()
if (lblock !== undefined) {
const gapSize = lblock.id.clock - nextExpectedClock
if (gapSize > 0) {
addToResult(new Skip(new ID(clientid, nextExpectedClock), gapSize))
}
}
}
while (ri < rightRanges.length) {
applyRight()
if (rblock !== undefined) {
const gapSize = rblock.id.clock - nextExpectedClock
if (gapSize > 0) {
addToResult(new Skip(new ID(clientid, nextExpectedClock), gapSize))
}
}
}
ranges.refs = result
}
}
})
inserts.clients.clear()
}
}