@y/y
Version:
Shared Editing Library
698 lines (665 loc) • 26 kB
JavaScript
/**
* @module encoding
*/
/*
* We use the first five bits in the info flag for determining the type of the struct.
*
* 0: GC
* 1: Item with Deleted content
* 2: Item with JSON content
* 3: Item with Binary content
* 4: Item with String content
* 5: Item with Embed content (for richtext content)
* 6: Item with Format content (a formatting marker for richtext content)
* 7: Item with Type
*/
import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding'
import * as map from 'lib0/map'
import * as math from 'lib0/math'
import * as array from 'lib0/array'
import { getStateVector, StructStore } from './StructStore.js'
import { getItemCleanStart, getItemCleanEnd } from './transaction-helpers.js'
import { createIdSet, IdRange, readAndApplyDeleteSet, readIdSet, mergeIdSets, writeIdSet } from './ids.js'
import { createID, ID } from './ID.js'
import { UpdateDecoderV1, UpdateDecoderV2, IdSetDecoderV1 } from './UpdateDecoder.js'
import { UpdateEncoderV1, UpdateEncoderV2, IdSetEncoderV1, IdSetEncoderV2 } from './UpdateEncoder.js'
import { convertUpdateFormatV2ToV1, LazyStructReader, LazyStructWriter, writeStructToLazyStructWriter, finishLazyStructWriting } from './updates.js'
import { readBlockSet, writeBlockSet } from './BlockSet.js'
import { Skip } from '../structs/Skip.js'
import { Item } from '../structs/Item.js'
import { GC } from '../structs/GC.js'
import { Doc } from './Doc.js'
import { writeStructs } from './encoding-helpers.js'
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {StructStore} store
* @param {Map<number,number>} _sm
*
* @private
* @function
*/
export const writeClientsStructs = (encoder, store, _sm) => {
// we filter all valid _sm entries into sm
const sm = new Map()
_sm.forEach((clock, client) => {
// only write if new structs are available
if (store.getClock(client) > clock) {
sm.set(client, clock)
}
})
getStateVector(store).forEach((_clock, client) => {
if (!_sm.has(client)) {
sm.set(client, 0)
}
})
// write # states that were updated
encoding.writeVarUint(encoder.restEncoder, sm.size)
// Write items with higher client ids first
// This heavily improves the conflict algorithm.
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
const lastStruct = structs[structs.length - 1]
writeStructs(encoder, structs, client, [new IdRange(clock, lastStruct.id.clock + lastStruct.length - clock)])
})
}
/**
* Resume computing structs generated by struct readers.
*
* While there is something to do, we integrate structs in this order
* 1. top element on stack, if stack is not empty
* 2. next element from current struct reader (if empty, use next struct reader)
*
* If struct causally depends on another struct (ref.missing), we put next reader of
* `ref.id.client` on top of stack.
*
* At some point we find a struct that has no causal dependencies,
* then we start emptying the stack.
*
* It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2)
* depends on struct3 (from client1). Therefore the max stack size is equal to `structReaders.length`.
*
* This method is implemented in a way so that we can resume computation if this update
* causally depends on another update.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @param {BlockSet} clientsStructRefs
* @return { null | { update: Uint8Array<ArrayBuffer>, missing: Map<number,number> } }
*
* @private
* @function
*/
const integrateStructs = (transaction, store, clientsStructRefs) => {
/**
* @type {Array<Item | GC | Skip>}
*/
const stack = []
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
let clientsStructRefsIds = array.from(clientsStructRefs.clients.keys()).sort((a, b) => a - b)
if (clientsStructRefsIds.length === 0) {
return null
}
const getNextStructTarget = () => {
if (clientsStructRefsIds.length === 0) {
return null
}
let nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.clients.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
while (nextStructsTarget.refs.length === nextStructsTarget.i) {
clientsStructRefsIds.pop()
if (clientsStructRefsIds.length > 0) {
nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.clients.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
} else {
return null
}
}
return nextStructsTarget
}
let curStructsTarget = getNextStructTarget()
if (curStructsTarget === null) {
return null
}
/**
* @type {StructStore}
*/
const restStructs = new StructStore()
const missingSV = new Map()
/**
* @param {number} client
* @param {number} clock
*/
const updateMissingSv = (client, clock) => {
const mclock = missingSV.get(client)
if (mclock == null || mclock > clock) {
missingSV.set(client, clock)
}
}
/**
* @type {GC|Item|Skip}
*/
let stackHead = /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++]
// caching the state because it is used very often
const state = new Map()
// // caching the state because it is used very often
// const currentInsertSet = createIdSet()
// clientsStructRefsIds.forEach(clientId => {
// currentInsertSet.clients.set(clientid, new IdRanges(_createInsertSliceFromStructs(store.clients.get(clientId) ?? [], false)))
// })
const addStackToRestSS = () => {
for (const item of stack) {
const client = item.id.client
const inapplicableItems = clientsStructRefs.clients.get(client)
if (inapplicableItems) {
// decrement because we weren't able to apply previous operation
inapplicableItems.i--
restStructs.clients.set(client, inapplicableItems.refs.slice(inapplicableItems.i))
clientsStructRefs.clients.delete(client)
inapplicableItems.i = 0
inapplicableItems.refs = []
} else {
// item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue
restStructs.clients.set(client, [item])
}
// remove client from clientsStructRefsIds to prevent users from applying the same update again
clientsStructRefsIds = clientsStructRefsIds.filter(c => c !== client)
}
stack.length = 0
}
// iterate over all struct readers until we are done
while (true) {
if (stackHead.constructor !== Skip) {
const localClock = map.setIfUndefined(state, stackHead.id.client, () => store.getClock(stackHead.id.client))
const offset = localClock - stackHead.id.clock
const missing = getMissing(/** @type {any} */ (stackHead), transaction, store)
if (missing !== null) {
stack.push(stackHead)
// get the struct reader that has the missing struct
/**
* @type {{ refs: Array<GC|Item|Skip>, i: number }}
*/
const structRefs = clientsStructRefs.clients.get(/** @type {number} */ (missing)) || { refs: [], i: 0 }
if (structRefs.refs.length === structRefs.i || missing === stackHead.id.client || stack.some(s => s.id.client === missing)) { // @todo this could be optimized!
// This update message causally depends on another update message that doesn't exist yet
updateMissingSv(/** @type {number} */ (missing), store.getClock(missing))
addStackToRestSS()
} else {
stackHead = structRefs.refs[structRefs.i++]
continue
}
} else {
// all fine, apply the stackhead
// but first add a skip to structs if necessary
if (offset < 0) {
const skip = new Skip(createID(stackHead.id.client, localClock), -offset)
skip.integrate(transaction, 0)
}
stackHead.integrate(transaction, 0)
state.set(stackHead.id.client, math.max(stackHead.id.clock + stackHead.length, localClock))
}
}
// iterate to next stackHead
if (stack.length > 0) {
stackHead = /** @type {GC|Item} */ (stack.pop())
} else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) {
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++])
} else {
curStructsTarget = getNextStructTarget()
if (curStructsTarget === null) {
// we are done!
break
} else {
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++])
}
}
}
if (restStructs.clients.size > 0) {
const encoder = new UpdateEncoderV2()
writeClientsStructs(encoder, restStructs, new Map())
// write empty deleteset
// writeDeleteSet(encoder, new DeleteSet())
encoding.writeVarUint(encoder.restEncoder, 0) // => no need for an extra function call, just write 0 deletes
return { missing: missingSV, update: encoder.toUint8Array() }
}
return null
}
/**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts a decoder.
*
* @param {decoding.Decoder} decoder
* @param {Doc} ydoc
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
* @param {UpdateDecoderV1 | UpdateDecoderV2} [structDecoder]
*
* @function
*/
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
ydoc.transact(transaction => {
// force that transaction.local is set to non-local
transaction.local = false
let retry = false
const doc = transaction.doc
const store = doc.store
// let start = performance.now()
const ss = readBlockSet(structDecoder)
const knownState = createIdSet()
ss.clients.forEach((_, client) => {
const storeStructs = store.clients.get(client)
if (storeStructs) {
const last = storeStructs[storeStructs.length - 1]
knownState.add(client, 0, last.id.clock + last.length)
// remove known items from ss
store.skips.clients.get(client)?.getIds().forEach(idrange => {
knownState.delete(client, idrange.clock, idrange.len)
})
}
})
// remove known items from ss
ss.exclude(knownState)
// console.log('time to read structs: ', performance.now() - start) // @todo remove
// start = performance.now()
// console.log('time to merge: ', performance.now() - start) // @todo remove
// start = performance.now()
const restStructs = integrateStructs(transaction, store, ss)
const pending = store.pendingStructs
if (pending) {
// check if we can apply something
for (const [client, clock] of pending.missing) {
if (ss.clients.has(client) || clock < store.getClock(client)) {
retry = true
break
}
}
if (restStructs) {
// merge restStructs into store.pending
for (const [client, clock] of restStructs.missing) {
const mclock = pending.missing.get(client)
if (mclock == null || mclock > clock) {
pending.missing.set(client, clock)
}
}
pending.update = mergeUpdatesV2([pending.update, restStructs.update])
}
} else {
store.pendingStructs = restStructs
}
// console.log('time to integrate: ', performance.now() - start) // @todo remove
// start = performance.now()
const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store)
if (store.pendingDs) {
// @todo we could make a lower-bound state-vector check as we do above
const pendingDSUpdate = new UpdateDecoderV2(decoding.createDecoder(store.pendingDs))
decoding.readVarUint(pendingDSUpdate.restDecoder) // read 0 structs, because we only encode deletes in pendingdsupdate
const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store)
if (dsRest && dsRest2) {
// case 1: ds1 != null && ds2 != null
store.pendingDs = mergeUpdatesV2([dsRest, dsRest2])
} else {
// case 2: ds1 != null
// case 3: ds2 != null
// case 4: ds1 == null && ds2 == null
store.pendingDs = dsRest || dsRest2
}
} else {
// Either dsRest == null && pendingDs == null OR dsRest != null
store.pendingDs = dsRest
}
// console.log('time to cleanup: ', performance.now() - start) // @todo remove
// start = performance.now()
// console.log('time to resume delete readers: ', performance.now() - start) // @todo remove
// start = performance.now()
if (retry) {
const update = /** @type {{update: Uint8Array}} */ (store.pendingStructs).update
store.pendingStructs = null
applyUpdateV2(transaction.doc, update)
}
}, transactionOrigin, false)
/**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts a decoder.
*
* @param {decoding.Decoder} decoder
* @param {Doc} ydoc
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
*
* @function
*/
export const readUpdate = (decoder, ydoc, transactionOrigin) => readUpdateV2(decoder, ydoc, transactionOrigin, new UpdateDecoderV1(decoder))
/**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
*
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
*
* @param {Doc} ydoc
* @param {Uint8Array} update
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
*
* @function
*/
export const applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = UpdateDecoderV2) => {
const decoder = decoding.createDecoder(update)
readUpdateV2(decoder, ydoc, transactionOrigin, new YDecoder(decoder))
}
/**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
*
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
*
* @param {Doc} ydoc
* @param {Uint8Array} update
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
*
* @function
*/
export const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1)
/**
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
* only write the operations that are missing.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Doc} doc
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
*
* @function
*/
export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => {
writeClientsStructs(encoder, doc.store, targetStateVector)
writeIdSet(encoder, doc.store.ds)
}
/**
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
* only write the operations that are missing.
*
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
*
* @param {Doc} doc
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
* @param {UpdateEncoderV1 | UpdateEncoderV2} [encoder]
* @return {Uint8Array<ArrayBuffer>}
*
* @function
*/
export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => {
const targetStateVector = decodeStateVector(encodedTargetStateVector)
writeStateAsUpdate(encoder, doc, targetStateVector)
const updates = [encoder.toUint8Array()]
// also add the pending updates (if there are any)
if (doc.store.pendingDs) {
updates.push(doc.store.pendingDs)
}
if (doc.store.pendingStructs) {
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
}
if (updates.length > 1) {
if (encoder.constructor === UpdateEncoderV1) {
return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update)))
} else if (encoder.constructor === UpdateEncoderV2) {
return mergeUpdatesV2(updates)
}
}
return updates[0]
}
/**
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
* only write the operations that are missing.
*
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
*
* @param {Doc} doc
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
* @return {Uint8Array<ArrayBuffer>}
*
* @function
*/
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new UpdateEncoderV1())
/**
* Read state vector from Decoder and return as Map
*
* @param {IdSetDecoderV1 | IdSetDecoderV2} decoder
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
export const readStateVector = decoder => {
const ss = new Map()
const ssLength = decoding.readVarUint(decoder.restDecoder)
for (let i = 0; i < ssLength; i++) {
const client = decoding.readVarUint(decoder.restDecoder)
const clock = decoding.readVarUint(decoder.restDecoder)
ss.set(client, clock)
}
return ss
}
/**
*
* This function works similarly to `readUpdateV2`.
*
* @param {Array<Uint8Array<ArrayBuffer>>} updates
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
* @return {Uint8Array<ArrayBuffer>}
*/
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
if (updates.length === 1) {
return updates[0]
} else if (updates.length === 0) {
return encodeStateAsUpdateV2(new Doc(), new Uint8Array([0]), new YEncoder())
}
const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
const blocksets = updateDecoders.map(dec => readBlockSet(dec))
const mergedBlockset = blocksets[0]
for (let i = 1; i < blocksets.length; i++) {
mergedBlockset.insertInto(blocksets[i])
}
const updateEncoder = new YEncoder()
writeBlockSet(updateEncoder, mergedBlockset)
const dss = updateDecoders.map(decoder => readIdSet(decoder))
const ds = mergeIdSets(dss)
writeIdSet(updateEncoder, ds)
return updateEncoder.toUint8Array()
}
/**
* @param {Array<Uint8Array<ArrayBuffer>>} updates
* @return {Uint8Array<ArrayBuffer>}
*/
export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1)
/**
* @deprecated
* @param {Uint8Array} update
* @param {Uint8Array} sv
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
*/
export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
const state = decodeStateVector(sv)
const encoder = new YEncoder()
const lazyStructWriter = new LazyStructWriter(encoder)
const decoder = new YDecoder(decoding.createDecoder(update))
const reader = new LazyStructReader(decoder, false)
while (reader.curr) {
const curr = reader.curr
const currClient = curr.id.client
const svClock = state.get(currClient) || 0
if (reader.curr.constructor === Skip) {
// the first written struct shouldn't be a skip
reader.next()
continue
}
if (curr.id.clock + curr.length > svClock) {
writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0), 0)
reader.next()
while (reader.curr && reader.curr.id.client === currClient) {
writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0, 0)
reader.next()
}
} else {
// read until something new comes up
while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) {
reader.next()
}
}
}
finishLazyStructWriting(lazyStructWriter)
// write ds
const ds = readIdSet(decoder)
writeIdSet(encoder, ds)
return encoder.toUint8Array()
}
/**
* @deprecated
* @todo remove this in favor of intersectupdate
*
* @param {Uint8Array<ArrayBuffer>} update
* @param {Uint8Array<ArrayBuffer>} sv
*/
export const diffUpdate = (update, sv) => diffUpdateV2(update, sv, UpdateDecoderV1, UpdateEncoderV1)
/**
* Read decodedState and return State as Map.
*
* @param {Uint8Array} decodedState
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
// export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState)))
/**
* Read decodedState and return State as Map.
*
* @param {Uint8Array} decodedState
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
export const decodeStateVector = decodedState => readStateVector(new IdSetDecoderV1(decoding.createDecoder(decodedState)))
/**
* @param {IdSetEncoderV1 | IdSetEncoderV2} encoder
* @param {Map<number,number>} sv
* @function
*/
export const writeStateVector = (encoder, sv) => {
encoding.writeVarUint(encoder.restEncoder, sv.size)
array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
encoding.writeVarUint(encoder.restEncoder, clock)
})
return encoder
}
/**
* @param {IdSetEncoderV1 | IdSetEncoderV2} encoder
* @param {Doc} doc
*
* @function
*/
export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encoder, getStateVector(doc.store))
/**
* Encode State as Uint8Array.
*
* @param {Doc|Map<number,number>} doc
* @param {IdSetEncoderV1 | IdSetEncoderV2} [encoder]
* @return {Uint8Array<ArrayBuffer>}
*
* @function
*/
export const encodeStateVectorV2 = (doc, encoder = new IdSetEncoderV2()) => {
if (doc instanceof Map) {
writeStateVector(encoder, doc)
} else {
writeDocumentStateVector(encoder, doc)
}
return encoder.toUint8Array()
}
/**
* Encode State as Uint8Array.
*
* @param {Doc|Map<number,number>} doc
* @return {Uint8Array<ArrayBuffer>}
*
* @function
*/
export const encodeStateVector = doc => encodeStateVectorV2(doc, new IdSetEncoderV1())
/**
* Return the creator clientID of the missing op or define missing items and return null.
*
* @param {Item} struct
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | number}
*/
const getMissing = (struct, transaction, store) => {
if (struct.constructor !== Item) return null
// we may not access these variables anymore after they have been written!
const origin = struct.origin
const rightOrigin = struct.rightOrigin
const parent = struct.parent
if (origin && (origin.clock >= store.getClock(origin.client) || store.skips.hasId(origin))) {
return origin.client
}
if (rightOrigin && (rightOrigin.clock >= store.getClock(rightOrigin.client) || store.skips.hasId(rightOrigin))) {
return rightOrigin.client
}
if (parent && parent.constructor === ID && (parent.clock >= store.getClock(parent.client) || store.skips.hasId(parent))) {
return parent.client
}
// We have all missing ids, now find the items
if (origin) {
struct.left = getItemCleanEnd(transaction, store, origin)
// copy left id to so that the original id can be gc'd
struct.origin = struct.left.lastId
}
if (rightOrigin) {
struct.right = getItemCleanStart(transaction, rightOrigin)
struct.rightOrigin = struct.right.id
}
if ((struct.left && struct.left.constructor === GC) || (struct.right && struct.right.constructor === GC)) {
struct.parent = null
} else if (parent == null) {
// only set parent if this shouldn't be garbage collected
if (struct.left && struct.left.constructor === Item) {
struct.parent = struct.left.parent
struct.parentSub = struct.left.parentSub
} else if (struct.right && struct.right.constructor === Item) {
struct.parent = struct.right.parent
struct.parentSub = struct.right.parentSub
}
} else if (parent.constructor === ID) {
const parentItem = store.getItem(parent)
if (parentItem.constructor === GC) {
struct.parent = null
} else {
struct.parent = /** @type {ContentType} */ (parentItem.content).type
}
} else if (typeof parent === 'string') {
struct.parent = transaction.doc.get(parent)
}
return null
}
/**
* @param {Uint8Array} update
* @param {import('./Doc.js').DocOpts} opts
*/
export const createDocFromUpdate = (update, opts = {}) => {
const ydoc = new Doc(opts)
applyUpdate(ydoc, update)
return ydoc
}
/**
* @param {Uint8Array} update
* @param {import('./Doc.js').DocOpts} opts
*/
export const createDocFromUpdateV2 = (update, opts = {}) => {
const ydoc = new Doc(opts)
applyUpdateV2(ydoc, update)
return ydoc
}
/**
* @param {Doc} ydoc
* @param {import('./Doc.js').DocOpts} [opts]
*/
export const cloneDoc = (ydoc, opts) => {
const clone = new Doc(opts)
applyUpdate(clone, encodeStateAsUpdate(ydoc))
return clone
}