rework and document api

This commit is contained in:
Kevin Jahns
2019-05-07 13:44:23 +02:00
parent 77687d94e6
commit 8c36f67f0b
33 changed files with 622 additions and 427 deletions

View File

@@ -48,7 +48,7 @@ export class DeleteSet {
/**
* Iterate over all structs that were deleted.
*
* This function expects that the deletes structs are not deleted. Hence, you can
* This function expects that the deletes structs are not merged. Hence, you can
* probably only use it in type observes and `afterTransaction` events. But not
* in `afterTransactionCleanup`.
*
@@ -266,6 +266,6 @@ export const readDeleteSet = (decoder, transaction, store) => {
if (unappliedDS.clients.size > 0) {
const unappliedDSEncoder = encoding.createEncoder()
writeDeleteSet(unappliedDSEncoder, unappliedDS)
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toBuffer(unappliedDSEncoder)))
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toUint8Array(unappliedDSEncoder)))
}
}

View File

@@ -10,26 +10,23 @@ import {
YMap,
YXmlFragment,
transact,
Transaction, YEvent // eslint-disable-line
AbstractItem, Transaction, YEvent // eslint-disable-line
} from '../internals.js'
import { Observable } from 'lib0/observable.js'
import * as random from 'lib0/random.js'
import * as map from 'lib0/map.js'
// @todo rename to shared document
/**
* A Yjs instance handles the state of shared data.
* @extends Observable<string>
*/
export class Y extends Observable {
export class Doc extends Observable {
/**
* @param {Object|undefined} conf configuration
*/
constructor (conf = {}) {
super()
// todo: change to clientId
this.clientID = random.uint32()
/**
* @type {Map<string, AbstractType<YEvent>>}
@@ -82,7 +79,7 @@ export class Y extends Observable {
* }
*
* @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
*
* @public
@@ -97,9 +94,18 @@ export class Y extends Observable {
const Constr = type.constructor
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
if (Constr === AbstractType) {
const t = new Constr()
// @ts-ignore
const t = new TypeConstructor()
t._map = type._map
type._map.forEach(/** @param {AbstractItem?} n */ n => {
for (; n !== null; n = n.left) {
n.parent = t
}
})
t._start = type._start
for (let n = t._start; n !== null; n = n.right) {
n.parent = t
}
t._length = type._length
this.share.set(name, t)
t._integrate(this, null)

View File

@@ -22,16 +22,6 @@ export class ID {
*/
this.clock = clock
}
/**
* @deprecated
* @todo remove and adapt relative position implementation
*/
toJSON () {
return {
client: this.client,
clock: this.clock
}
}
}
/**
@@ -91,7 +81,7 @@ export const readID = decoder =>
*/
export const findRootTypeKey = type => {
// @ts-ignore _y must be defined, otherwise unexpected case
for (let [key, value] of type._y.share) {
for (let [key, value] of type.doc.share) {
if (value === type) {
return key
}

View File

@@ -1,6 +1,3 @@
/**
* @module Cursors
*/
import {
getItem,
@@ -13,7 +10,7 @@ import {
findRootTypeKey,
AbstractItem,
ItemType,
ID, StructStore, Y, AbstractType // eslint-disable-line
ID, StructStore, Doc, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
@@ -21,29 +18,30 @@ import * as decoding from 'lib0/decoding.js'
import * as error from 'lib0/error.js'
/**
* A Cursor is a relative position that is based on the Yjs model. In contrast to an
* absolute position (position by index), the Cursor can be
* recomputed when remote changes are received. For example:
* A relative position is based on the Yjs model and is not affected by document changes.
* E.g. If you place a relative position before a certain character, it will always point to this character.
* If you place a relative position at the end of a type, it will always point to the end of the type.
*
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the cursor position.
* A numeric position is often unsuited for user selections, because it does not change when content is inserted
* before or after.
*
* A relative cursor position can be obtained with the function
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position.
*
* One of the properties must be defined.
*
* @example
* // Current cursor position is at position 10
* const relativePosition = createCursorFromOffset(yText, 10)
* const relativePosition = createRelativePositionFromIndex(yText, 10)
* // modify yText
* yText.insert(0, 'abc')
* yText.delete(3, 10)
* // Compute the cursor position
* const absolutePosition = toAbsolutePosition(y, relativePosition)
* const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition)
* absolutePosition.type === yText // => true
* console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3
* console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3
*
*/
export class Cursor {
export class RelativePosition {
/**
* @param {ID|null} type
* @param {string|null} tname
@@ -63,35 +61,22 @@ export class Cursor {
*/
this.item = item
}
toJSON () {
const json = {}
if (this.type !== null) {
json.type = this.type.toJSON()
}
if (this.tname !== null) {
json.tname = this.tname
}
if (this.item !== null) {
json.item = this.item.toJSON()
}
return json
}
}
/**
* @param {Object} json
* @return {Cursor}
* @return {RelativePosition}
*
* @function
*/
export const createCursorFromJSON = json => new Cursor(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock))
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock))
export class AbsolutePosition {
/**
* @param {AbstractType<any>} type
* @param {number} offset
* @param {number} index
*/
constructor (type, offset) {
constructor (type, index) {
/**
* @type {AbstractType<any>}
*/
@@ -99,17 +84,17 @@ export class AbsolutePosition {
/**
* @type {number}
*/
this.offset = offset
this.index = index
}
}
/**
* @param {AbstractType<any>} type
* @param {number} offset
* @param {number} index
*
* @function
*/
export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset)
export const createAbsolutePosition = (type, index) => new AbsolutePosition(type, index)
/**
* @param {AbstractType<any>} type
@@ -117,7 +102,7 @@ export const createAbsolutePosition = (type, offset) => new AbsolutePosition(typ
*
* @function
*/
export const createCursor = (type, item) => {
export const createRelativePosition = (type, item) => {
let typeid = null
let tname = null
if (type._item === null) {
@@ -125,40 +110,40 @@ export const createCursor = (type, item) => {
} else {
typeid = type._item.id
}
return new Cursor(typeid, tname, item)
return new RelativePosition(typeid, tname, item)
}
/**
* Create a relativePosition based on a absolute position.
*
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
* @param {number} offset The absolute position.
* @return {Cursor}
* @param {number} index The absolute position.
* @return {RelativePosition}
*
* @function
*/
export const createCursorFromTypeOffset = (type, offset) => {
export const createRelativePositionFromTypeIndex = (type, index) => {
let t = type._start
while (t !== null) {
if (!t.deleted && t.countable) {
if (t.length > offset) {
if (t.length > index) {
// case 1: found position somewhere in the linked list
return createCursor(type, createID(t.id.client, t.id.clock + offset))
return createRelativePosition(type, createID(t.id.client, t.id.clock + index))
}
offset -= t.length
index -= t.length
}
t = t.right
}
return createCursor(type, null)
return createRelativePosition(type, null)
}
/**
* @param {encoding.Encoder} encoder
* @param {Cursor} rpos
* @param {RelativePosition} rpos
*
* @function
*/
export const writeCursor = (encoder, rpos) => {
export const writeRelativePosition = (encoder, rpos) => {
const { type, tname, item } = rpos
if (item !== null) {
encoding.writeVarUint(encoder, 0)
@@ -177,15 +162,23 @@ export const writeCursor = (encoder, rpos) => {
return encoder
}
/**
* @param {RelativePosition} rpos
* @return {Uint8Array}
*/
export const encodeRelativePosition = rpos => {
const encoder = encoding.createEncoder()
writeRelativePosition(encoder, rpos)
return encoding.toUint8Array(encoder)
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
* @param {StructStore} store
* @return {Cursor|null}
* @return {RelativePosition|null}
*
* @function
*/
export const readCursor = (decoder, y, store) => {
export const readRelativePosition = decoder => {
let type = null
let tname = null
let itemID = null
@@ -203,23 +196,29 @@ export const readCursor = (decoder, y, store) => {
type = readID(decoder)
}
}
return new Cursor(type, tname, itemID)
return new RelativePosition(type, tname, itemID)
}
/**
* @param {Cursor} cursor
* @param {Y} y
* @param {Uint8Array} uint8Array
* @return {RelativePosition|null}
*/
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
/**
* @param {RelativePosition} rpos
* @param {Doc} doc
* @return {AbsolutePosition|null}
*
* @function
*/
export const createAbsolutePositionFromCursor = (cursor, y) => {
const store = y.store
const rightID = cursor.item
const typeID = cursor.type
const tname = cursor.tname
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
const store = doc.store
const rightID = rpos.item
const typeID = rpos.type
const tname = rpos.tname
let type = null
let offset = 0
let index = 0
if (rightID !== null) {
if (getState(store, rightID.client) <= rightID.clock) {
return null
@@ -228,18 +227,18 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
if (!(right instanceof AbstractItem)) {
return null
}
offset = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
index = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
let n = right.left
while (n !== null) {
if (!n.deleted && n.countable) {
offset += n.length
index += n.length
}
n = n.left
}
type = right.parent
} else {
if (tname !== null) {
type = y.get(tname)
type = doc.get(tname)
} else if (typeID !== null) {
if (getState(store, typeID.client) <= typeID.clock) {
// type does not exist yet
@@ -255,20 +254,20 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
} else {
throw error.unexpectedCase()
}
offset = type._length
index = type._length
}
if (type._item !== null && type._item.deleted) {
return null
}
return createAbsolutePosition(type, offset)
return createAbsolutePosition(type, index)
}
/**
* @param {Cursor|null} a
* @param {Cursor|null} b
* @param {RelativePosition|null} a
* @param {RelativePosition|null} b
*
* @function
*/
export const compareCursors = (a, b) => a === b || (
export const compareRelativePositions = (a, b) => a === b || (
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type)
)

View File

@@ -6,12 +6,10 @@ import {
export class Snapshot {
/**
* @param {DeleteSet} ds delete store
* @param {DeleteSet} ds
* @param {Map<number,number>} sm state map
* @param {Map<number,string>} userMap
* @private
*/
constructor (ds, sm, userMap) {
constructor (ds, sm) {
/**
* @type {DeleteSet}
* @private
@@ -23,14 +21,15 @@ export class Snapshot {
* @private
*/
this.sm = sm
/**
* @type {Map<number,string>}
* @private
*/
this.userMap = userMap
}
}
/**
* @param {DeleteSet} ds
* @param {Map<number,number>} sm
*/
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
/**
* @param {AbstractItem} item
* @param {Snapshot|undefined} snapshot

View File

@@ -6,8 +6,7 @@ import {
import * as math from 'lib0/math.js'
import * as error from 'lib0/error.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
export class StructStore {
constructor () {
@@ -51,7 +50,7 @@ export class StructStore {
* @public
* @function
*/
export const getStates = store => {
export const getStateVector = store => {
const sm = new Map()
store.clients.forEach((structs, client) => {
const struct = structs[structs.length - 1]
@@ -262,42 +261,3 @@ export const replaceStruct = (store, struct, newStruct) => {
const structs = store.clients.get(struct.id.client)
structs[findIndexSS(structs, struct.id.clock)] = newStruct
}
/**
* Read StateMap from Decoder and return as Map
*
* @param {decoding.Decoder} decoder
* @return {Map<number,number>}
*
* @private
* @function
*/
export const readStatesAsMap = decoder => {
const ss = new Map()
const ssLength = decoding.readVarUint(decoder)
for (let i = 0; i < ssLength; i++) {
const client = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
ss.set(client, clock)
}
return ss
}
/**
* Write StateMap to Encoder
*
* @param {encoding.Encoder} encoder
* @param {StructStore} store
*
* @private
* @function
*/
export const writeStates = (encoder, store) => {
encoding.writeVarUint(encoder, store.clients.size)
store.clients.forEach((structs, client) => {
const id = structs[structs.length - 1].id
encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock)
})
return encoder
}

View File

@@ -6,11 +6,11 @@ import {
writeDeleteSet,
DeleteSet,
sortAndMergeDeleteSet,
getStates,
getStateVector,
findIndexSS,
callEventHandlerListeners,
AbstractItem,
ID, AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line
ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
@@ -43,15 +43,15 @@ import * as math from 'lib0/math.js'
*/
export class Transaction {
/**
* @param {Y} y
* @param {Doc} doc
* @param {any} origin
*/
constructor (y, origin) {
constructor (doc, origin) {
/**
* The Yjs instance.
* @type {Y}
* @type {Doc}
*/
this.y = y
this.doc = doc
/**
* Describes the set of deleted items by ids
* @type {DeleteSet}
@@ -61,7 +61,7 @@ export class Transaction {
* Holds the state before the transaction started.
* @type {Map<Number,Number>}
*/
this.beforeState = getStates(y.store)
this.beforeState = getStateVector(doc.store)
/**
* Holds the state after the transaction.
* @type {Map<Number,Number>}
@@ -80,11 +80,6 @@ export class Transaction {
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
*/
this.changedParentTypes = new Map()
/**
* @type {encoding.Encoder|null}
* @private
*/
this._updateMessage = null
/**
* @type {Set<ID>}
* @private
@@ -95,21 +90,20 @@ export class Transaction {
*/
this.origin = origin
}
/**
* @type {encoding.Encoder|null}
* @public
*/
get updateMessage () {
// only create if content was added in transaction (state or ds changed)
if (this._updateMessage === null && (this.deleteSet.clients.size > 0 || map.any(this.afterState, (clock, client) => this.beforeState.get(client) !== clock))) {
const encoder = encoding.createEncoder()
sortAndMergeDeleteSet(this.deleteSet)
writeStructsFromTransaction(encoder, this)
writeDeleteSet(encoder, this.deleteSet)
this._updateMessage = encoder
}
return this._updateMessage
}
/**
* @param {Transaction} transaction
*/
export const computeUpdateMessageFromTransaction = transaction => {
if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
return null
}
const encoder = encoding.createEncoder()
sortAndMergeDeleteSet(transaction.deleteSet)
writeStructsFromTransaction(encoder, transaction)
writeDeleteSet(encoder, transaction.deleteSet)
return encoder
}
/**
@@ -119,45 +113,44 @@ export class Transaction {
* @function
*/
export const nextID = transaction => {
const y = transaction.y
const y = transaction.doc
return createID(y.clientID, getState(y.store, y.clientID))
}
/**
* Implements the functionality of `y.transact(()=>{..})`
*
* @param {Y} y
* @param {Doc} doc
* @param {function(Transaction):void} f
* @param {any} [origin]
*
* @private
* @function
*/
export const transact = (y, f, origin = null) => {
const transactionCleanups = y._transactionCleanups
export const transact = (doc, f, origin = null) => {
const transactionCleanups = doc._transactionCleanups
let initialCall = false
if (y._transaction === null) {
if (doc._transaction === null) {
initialCall = true
y._transaction = new Transaction(y, origin)
transactionCleanups.push(y._transaction)
y.emit('beforeTransaction', [y._transaction, y])
doc._transaction = new Transaction(doc, origin)
transactionCleanups.push(doc._transaction)
doc.emit('beforeTransaction', [doc._transaction, doc])
}
try {
f(y._transaction)
f(doc._transaction)
} finally {
// @todo set after state here
if (initialCall && transactionCleanups[0] === y._transaction) {
if (initialCall && transactionCleanups[0] === doc._transaction) {
// The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after another
for (let i = 0; i < transactionCleanups.length; i++) {
const transaction = transactionCleanups[i]
const store = transaction.y.store
const store = transaction.doc.store
const ds = transaction.deleteSet
sortAndMergeDeleteSet(ds)
transaction.afterState = getStates(transaction.y.store)
y._transaction = null
y.emit('beforeObserverCalls', [transaction, y])
transaction.afterState = getStateVector(transaction.doc.store)
doc._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
// emit change events on changed types
transaction.changed.forEach((subs, itemtype) => {
itemtype._callObserver(transaction, subs)
@@ -175,7 +168,7 @@ export const transact = (y, f, origin = null) => {
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
})
y.emit('afterTransaction', [transaction, y])
doc.emit('afterTransaction', [transaction, doc])
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
@@ -213,7 +206,7 @@ export const transact = (y, f, origin = null) => {
break
}
if (struct.deleted && struct instanceof AbstractItem) {
struct.gc(transaction, store, false)
struct.gc(store, false)
}
}
}
@@ -276,10 +269,15 @@ export const transact = (y, f, origin = null) => {
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
// @todo implement a dedicatet event that we can use to send updates to other peer
y.emit('afterTransactionCleanup', [transaction, y])
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {
const updateMessage = computeUpdateMessageFromTransaction(transaction)
if (updateMessage !== null) {
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
}
}
}
y._transactionCleanups = []
doc._transactionCleanups = []
}
}
}

View File

@@ -116,10 +116,10 @@ export class UndoManager {
this._undoing = false
this._redoing = false
this._lastTransactionWasUndo = false
const y = scope._y
this.y = y
const doc = scope.doc
this.y = doc
let bindingInfos
y.on('beforeTransaction', (y, transaction, remote) => {
doc.on('beforeTransaction', (y, transaction, remote) => {
if (!remote) {
// Store binding information before transaction is executed
// By restoring the binding information, we can make sure that the state
@@ -130,7 +130,7 @@ export class UndoManager {
})
}
})
y.on('afterTransaction', (y, transaction, remote) => {
doc.on('afterTransaction', (y, transaction, remote) => {
if (!remote && transaction.changedParentTypes.has(scope)) {
let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
if (!this._undoing) {
@@ -200,3 +200,4 @@ export class UndoManager {
return performedRedo
}
}
}

View File

@@ -17,11 +17,11 @@ import {
createID,
readID,
getState,
getStates,
getStateVector,
readDeleteSet,
writeDeleteSet,
createDeleteSetFromStructStore,
Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line
Doc, Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
@@ -104,7 +104,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
sm.set(client, clock)
}
})
getStates(store).forEach((clock, client) => {
getStateVector(store).forEach((clock, client) => {
if (!_sm.has(client)) {
sm.set(client, 0)
}
@@ -250,7 +250,7 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
* @private
* @function
*/
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.y.store, transaction.beforeState)
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
/**
* @param {StructStore} store
@@ -297,25 +297,127 @@ export const readStructs = (decoder, transaction, store) => {
}
/**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts an decoder.
*
* @param {decoding.Decoder} decoder
* @param {Transaction} transaction
* @param {StructStore} store
* @param {Doc} ydoc
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
*
* @function
*/
export const readModel = (decoder, transaction, store) => {
readStructs(decoder, transaction, store)
readDeleteSet(decoder, transaction, store)
export const readUpdate = (decoder, ydoc, transactionOrigin) =>
ydoc.transact(transaction => {
readStructs(decoder, transaction, ydoc.store)
readDeleteSet(decoder, transaction, ydoc.store)
}, transactionOrigin)
/**
* 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) =>
readUpdate(decoding.createDecoder(update), ydoc, transactionOrigin)
/**
* 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 {encoding.Encoder} 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)
writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store))
}
/**
* @param {encoding.Encoder} encoder
* @param {StructStore} store
* @param {Map<number,number>} [targetState] The state of the target that receives the update. Leave empty to write all known structs
* 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}
*
* @function
*/
export const writeModel = (encoder, store, targetState = new Map()) => {
writeClientsStructs(encoder, store, targetState)
writeDeleteSet(encoder, createDeleteSetFromStructStore(store))
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => {
const encoder = encoding.createEncoder()
const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
writeStateAsUpdate(encoder, doc, targetStateVector)
return encoding.toUint8Array(encoder)
}
/**
* Read state vector from Decoder and return as Map
*
* @param {decoding.Decoder} 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)
for (let i = 0; i < ssLength; i++) {
const client = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
ss.set(client, clock)
}
return ss
}
/**
* 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(decoding.createDecoder(decodedState))
/**
* Write State Vector to `lib0/encoding.js#Encoder`.
*
* @param {encoding.Encoder} encoder
* @param {Doc} doc
*
* @function
*/
export const writeDocumentStateVector = (encoder, doc) => {
encoding.writeVarUint(encoder, doc.store.clients.size)
doc.store.clients.forEach((structs, client) => {
const id = structs[structs.length - 1].id
encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock)
})
return encoder
}
/**
* Encode State as Uint8Array.
*
* @param {Doc} doc
* @return {Uint8Array}
*
* @function
*/
export const encodeDocumentStateVector = doc => {
const encoder = encoding.createEncoder()
writeDocumentStateVector(encoder, doc)
return encoding.toUint8Array(encoder)
}