845 lines
21 KiB
JavaScript
845 lines
21 KiB
JavaScript
|
|
import {
|
|
removeEventHandlerListener,
|
|
callEventHandlerListeners,
|
|
addEventHandlerListener,
|
|
createEventHandler,
|
|
getState,
|
|
isVisible,
|
|
ContentType,
|
|
createID,
|
|
ContentAny,
|
|
ContentBinary,
|
|
getItemCleanStart,
|
|
AbstractUpdateEncoder, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
|
} from '../internals.js'
|
|
|
|
import * as map from 'lib0/map.js'
|
|
import * as iterator from 'lib0/iterator.js'
|
|
import * as error from 'lib0/error.js'
|
|
import * as math from 'lib0/math.js'
|
|
|
|
const maxSearchMarker = 60
|
|
|
|
/**
|
|
* A unique timestamp that identifies each marker.
|
|
*
|
|
* Time is relative,.. this is more like an ever-increasing clock.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
let globalSearchMarkerTimestamp = 0
|
|
|
|
export class ArraySearchMarker {
|
|
/**
|
|
* @param {Item} p
|
|
* @param {number} index
|
|
*/
|
|
constructor (p, index) {
|
|
p.marker = true
|
|
this.p = p
|
|
this.index = index
|
|
this.timestamp = globalSearchMarkerTimestamp++
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {ArraySearchMarker} marker
|
|
*/
|
|
const refreshMarkerTimestamp = marker => { marker.timestamp = globalSearchMarkerTimestamp++ }
|
|
|
|
/**
|
|
* This is rather complex so this function is the only thing that should overwrite a marker
|
|
*
|
|
* @param {ArraySearchMarker} marker
|
|
* @param {Item} p
|
|
* @param {number} index
|
|
*/
|
|
const overwriteMarker = (marker, p, index) => {
|
|
marker.p.marker = false
|
|
marker.p = p
|
|
p.marker = true
|
|
marker.index = index
|
|
marker.timestamp = globalSearchMarkerTimestamp++
|
|
}
|
|
|
|
/**
|
|
* @param {Array<ArraySearchMarker>} searchMarker
|
|
* @param {Item} p
|
|
* @param {number} index
|
|
*/
|
|
const markPosition = (searchMarker, p, index) => {
|
|
if (searchMarker.length >= maxSearchMarker) {
|
|
// override oldest marker (we don't want to create more objects)
|
|
const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b)
|
|
overwriteMarker(marker, p, index)
|
|
return marker
|
|
} else {
|
|
// create new marker
|
|
const pm = new ArraySearchMarker(p, index)
|
|
searchMarker.push(pm)
|
|
return pm
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search marker help us to find positions in the associative array faster.
|
|
*
|
|
* They speed up the process of finding a position without much bookkeeping.
|
|
*
|
|
* A maximum of `maxSearchMarker` objects are created.
|
|
*
|
|
* This function always returns a refreshed marker (updated timestamp)
|
|
*
|
|
* @param {AbstractType<any>} yarray
|
|
* @param {number} index
|
|
*/
|
|
export const findMarker = (yarray, index) => {
|
|
if (yarray._start === null || index === 0 || yarray._searchMarker === null) {
|
|
return null
|
|
}
|
|
const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b)
|
|
let p = yarray._start
|
|
let pindex = 0
|
|
if (marker !== null) {
|
|
p = marker.p
|
|
pindex = marker.index
|
|
refreshMarkerTimestamp(marker) // we used it, we might need to use it again
|
|
}
|
|
// iterate to right if possible
|
|
while (p.right !== null && pindex < index) {
|
|
if (!p.deleted && p.countable) {
|
|
if (index < pindex + p.length) {
|
|
break
|
|
}
|
|
pindex += p.length
|
|
}
|
|
p = p.right
|
|
}
|
|
// iterate to left if necessary (might be that pindex > index)
|
|
while (p.left !== null && pindex > index) {
|
|
p = p.left
|
|
if (!p.deleted && p.countable) {
|
|
pindex -= p.length
|
|
}
|
|
}
|
|
// we want to make sure that p can't be merged with left, because that would screw up everything
|
|
// in that cas just return what we have (it is most likely the best marker anyway)
|
|
// iterate to left until p can't be merged with left
|
|
while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) {
|
|
p = p.left
|
|
if (!p.deleted && p.countable) {
|
|
pindex -= p.length
|
|
}
|
|
}
|
|
|
|
// @todo remove!
|
|
// assure position
|
|
// {
|
|
// let start = yarray._start
|
|
// let pos = 0
|
|
// while (start !== p) {
|
|
// if (!start.deleted && start.countable) {
|
|
// pos += start.length
|
|
// }
|
|
// start = /** @type {Item} */ (start.right)
|
|
// }
|
|
// if (pos !== pindex) {
|
|
// debugger
|
|
// throw new Error('Gotcha position fail!')
|
|
// }
|
|
// }
|
|
// if (marker) {
|
|
// if (window.lengthes == null) {
|
|
// window.lengthes = []
|
|
// }
|
|
// window.lengthes.push(marker.index - pindex)
|
|
// console.log('distance', marker.index - pindex, 'len', p && p.parent.length)
|
|
// }
|
|
if (marker !== null && math.abs(marker.index - pindex) < 30) {
|
|
// adjust existing marker
|
|
overwriteMarker(marker, p, pindex)
|
|
return marker
|
|
} else {
|
|
// create new marker
|
|
return markPosition(yarray._searchMarker, p, pindex)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update markers when a change happened.
|
|
*
|
|
* This should be called before doing a deletion!
|
|
*
|
|
* @param {Array<ArraySearchMarker>} searchMarker
|
|
* @param {number} index
|
|
* @param {number} len If insertion, len is positive. If deletion, len is negative.
|
|
*/
|
|
export const updateMarkerChanges = (searchMarker, index, len) => {
|
|
for (let i = searchMarker.length - 1; i >= 0; i--) {
|
|
const m = searchMarker[i]
|
|
if (len > 0) {
|
|
/**
|
|
* @type {Item|null}
|
|
*/
|
|
let p = m.p
|
|
p.marker = false
|
|
// Ideally we just want to do a simple position comparison, but this will only work if
|
|
// search markers don't point to deleted items for formats.
|
|
// Iterate marker to prev undeleted countable position so we know what to do when updating a position
|
|
while (p && (p.deleted || !p.countable)) {
|
|
p = p.left
|
|
if (p && !p.deleted && p.countable) {
|
|
// adjust position. the loop should break now
|
|
m.index -= p.length
|
|
}
|
|
}
|
|
if (p === null || p.marker === true) {
|
|
// remove search marker if updated position is null or if position is already marked
|
|
searchMarker.splice(i, 1)
|
|
continue
|
|
}
|
|
m.p = p
|
|
p.marker = true
|
|
}
|
|
if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice
|
|
m.index = math.max(index, m.index + len)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accumulate all (list) children of a type and return them as an Array.
|
|
*
|
|
* @param {AbstractType<any>} t
|
|
* @return {Array<Item>}
|
|
*/
|
|
export const getTypeChildren = t => {
|
|
let s = t._start
|
|
const arr = []
|
|
while (s) {
|
|
arr.push(s)
|
|
s = s.right
|
|
}
|
|
return arr
|
|
}
|
|
|
|
/**
|
|
* Call event listeners with an event. This will also add an event to all
|
|
* parents (for `.observeDeep` handlers).
|
|
*
|
|
* @template EventType
|
|
* @param {AbstractType<EventType>} type
|
|
* @param {Transaction} transaction
|
|
* @param {EventType} event
|
|
*/
|
|
export const callTypeObservers = (type, transaction, event) => {
|
|
const changedType = type
|
|
const changedParentTypes = transaction.changedParentTypes
|
|
while (true) {
|
|
// @ts-ignore
|
|
map.setIfUndefined(changedParentTypes, type, () => []).push(event)
|
|
if (type._item === null) {
|
|
break
|
|
}
|
|
type = /** @type {AbstractType<any>} */ (type._item.parent)
|
|
}
|
|
callEventHandlerListeners(changedType._eH, event, transaction)
|
|
}
|
|
|
|
/**
|
|
* @template EventType
|
|
* Abstract Yjs Type class
|
|
*/
|
|
export class AbstractType {
|
|
constructor () {
|
|
/**
|
|
* @type {Item|null}
|
|
*/
|
|
this._item = null
|
|
/**
|
|
* @type {Map<string,Item>}
|
|
*/
|
|
this._map = new Map()
|
|
/**
|
|
* @type {Item|null}
|
|
*/
|
|
this._start = null
|
|
/**
|
|
* @type {Doc|null}
|
|
*/
|
|
this.doc = null
|
|
this._length = 0
|
|
/**
|
|
* Event handlers
|
|
* @type {EventHandler<EventType,Transaction>}
|
|
*/
|
|
this._eH = createEventHandler()
|
|
/**
|
|
* Deep event handlers
|
|
* @type {EventHandler<Array<YEvent>,Transaction>}
|
|
*/
|
|
this._dEH = createEventHandler()
|
|
/**
|
|
* @type {null | Array<ArraySearchMarker>}
|
|
*/
|
|
this._searchMarker = null
|
|
}
|
|
|
|
/**
|
|
* Integrate this type into the Yjs instance.
|
|
*
|
|
* * Save this struct in the os
|
|
* * This type is sent to other client
|
|
* * Observer functions are fired
|
|
*
|
|
* @param {Doc} y The Yjs instance
|
|
* @param {Item|null} item
|
|
*/
|
|
_integrate (y, item) {
|
|
this.doc = y
|
|
this._item = item
|
|
}
|
|
|
|
/**
|
|
* @return {AbstractType<EventType>}
|
|
*/
|
|
_copy () {
|
|
throw error.methodUnimplemented()
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractUpdateEncoder} encoder
|
|
*/
|
|
_write (encoder) { }
|
|
|
|
/**
|
|
* The first non-deleted item
|
|
*/
|
|
get _first () {
|
|
let n = this._start
|
|
while (n !== null && n.deleted) {
|
|
n = n.right
|
|
}
|
|
return n
|
|
}
|
|
|
|
/**
|
|
* Creates YEvent and calls all type observers.
|
|
* Must be implemented by each type.
|
|
*
|
|
* @param {Transaction} transaction
|
|
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
|
*/
|
|
_callObserver (transaction, parentSubs) {
|
|
if (!transaction.local && this._searchMarker) {
|
|
this._searchMarker.length = 0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Observe all events that are created on this type.
|
|
*
|
|
* @param {function(EventType, Transaction):void} f Observer function
|
|
*/
|
|
observe (f) {
|
|
addEventHandlerListener(this._eH, f)
|
|
}
|
|
|
|
/**
|
|
* Observe all events that are created by this type and its children.
|
|
*
|
|
* @param {function(Array<YEvent>,Transaction):void} f Observer function
|
|
*/
|
|
observeDeep (f) {
|
|
addEventHandlerListener(this._dEH, f)
|
|
}
|
|
|
|
/**
|
|
* Unregister an observer function.
|
|
*
|
|
* @param {function(EventType,Transaction):void} f Observer function
|
|
*/
|
|
unobserve (f) {
|
|
removeEventHandlerListener(this._eH, f)
|
|
}
|
|
|
|
/**
|
|
* Unregister an observer function.
|
|
*
|
|
* @param {function(Array<YEvent>,Transaction):void} f Observer function
|
|
*/
|
|
unobserveDeep (f) {
|
|
removeEventHandlerListener(this._dEH, f)
|
|
}
|
|
|
|
/**
|
|
* @abstract
|
|
* @return {any}
|
|
*/
|
|
toJSON () {}
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractType<any>} type
|
|
* @return {Array<any>}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListToArray = type => {
|
|
const cs = []
|
|
let n = type._start
|
|
while (n !== null) {
|
|
if (n.countable && !n.deleted) {
|
|
const c = n.content.getContent()
|
|
for (let i = 0; i < c.length; i++) {
|
|
cs.push(c[i])
|
|
}
|
|
}
|
|
n = n.right
|
|
}
|
|
return cs
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractType<any>} type
|
|
* @param {Snapshot} snapshot
|
|
* @return {Array<any>}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListToArraySnapshot = (type, snapshot) => {
|
|
const cs = []
|
|
let n = type._start
|
|
while (n !== null) {
|
|
if (n.countable && isVisible(n, snapshot)) {
|
|
const c = n.content.getContent()
|
|
for (let i = 0; i < c.length; i++) {
|
|
cs.push(c[i])
|
|
}
|
|
}
|
|
n = n.right
|
|
}
|
|
return cs
|
|
}
|
|
|
|
/**
|
|
* Executes a provided function on once on overy element of this YArray.
|
|
*
|
|
* @param {AbstractType<any>} type
|
|
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListForEach = (type, f) => {
|
|
let index = 0
|
|
let n = type._start
|
|
while (n !== null) {
|
|
if (n.countable && !n.deleted) {
|
|
const c = n.content.getContent()
|
|
for (let i = 0; i < c.length; i++) {
|
|
f(c[i], index++, type)
|
|
}
|
|
}
|
|
n = n.right
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @template C,R
|
|
* @param {AbstractType<any>} type
|
|
* @param {function(C,number,AbstractType<any>):R} f
|
|
* @return {Array<R>}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListMap = (type, f) => {
|
|
/**
|
|
* @type {Array<any>}
|
|
*/
|
|
const result = []
|
|
typeListForEach(type, (c, i) => {
|
|
result.push(f(c, i, type))
|
|
})
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractType<any>} type
|
|
* @return {IterableIterator<any>}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListCreateIterator = type => {
|
|
let n = type._start
|
|
/**
|
|
* @type {Array<any>|null}
|
|
*/
|
|
let currentContent = null
|
|
let currentContentIndex = 0
|
|
return {
|
|
[Symbol.iterator] () {
|
|
return this
|
|
},
|
|
next: () => {
|
|
// find some content
|
|
if (currentContent === null) {
|
|
while (n !== null && n.deleted) {
|
|
n = n.right
|
|
}
|
|
// check if we reached the end, no need to check currentContent, because it does not exist
|
|
if (n === null) {
|
|
return {
|
|
done: true,
|
|
value: undefined
|
|
}
|
|
}
|
|
// we found n, so we can set currentContent
|
|
currentContent = n.content.getContent()
|
|
currentContentIndex = 0
|
|
n = n.right // we used the content of n, now iterate to next
|
|
}
|
|
const value = currentContent[currentContentIndex++]
|
|
// check if we need to empty currentContent
|
|
if (currentContent.length <= currentContentIndex) {
|
|
currentContent = null
|
|
}
|
|
return {
|
|
done: false,
|
|
value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes a provided function on once on overy element of this YArray.
|
|
* Operates on a snapshotted state of the document.
|
|
*
|
|
* @param {AbstractType<any>} type
|
|
* @param {function(any,number,AbstractType<any>):void} f A function to execute on every element of this YArray.
|
|
* @param {Snapshot} snapshot
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListForEachSnapshot = (type, f, snapshot) => {
|
|
let index = 0
|
|
let n = type._start
|
|
while (n !== null) {
|
|
if (n.countable && isVisible(n, snapshot)) {
|
|
const c = n.content.getContent()
|
|
for (let i = 0; i < c.length; i++) {
|
|
f(c[i], index++, type)
|
|
}
|
|
}
|
|
n = n.right
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractType<any>} type
|
|
* @param {number} index
|
|
* @return {any}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListGet = (type, index) => {
|
|
const marker = findMarker(type, index)
|
|
let n = type._start
|
|
if (marker !== null) {
|
|
n = marker.p
|
|
index -= marker.index
|
|
}
|
|
for (; n !== null; n = n.right) {
|
|
if (!n.deleted && n.countable) {
|
|
if (index < n.length) {
|
|
return n.content.getContent()[index]
|
|
}
|
|
index -= n.length
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Transaction} transaction
|
|
* @param {AbstractType<any>} parent
|
|
* @param {Item?} referenceItem
|
|
* @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
|
|
let left = referenceItem
|
|
const doc = transaction.doc
|
|
const ownClientId = doc.clientID
|
|
const store = doc.store
|
|
const right = referenceItem === null ? parent._start : referenceItem.right
|
|
/**
|
|
* @type {Array<Object|Array<any>|number>}
|
|
*/
|
|
let jsonContent = []
|
|
const packJsonContent = () => {
|
|
if (jsonContent.length > 0) {
|
|
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
|
|
left.integrate(transaction, 0)
|
|
jsonContent = []
|
|
}
|
|
}
|
|
content.forEach(c => {
|
|
switch (c.constructor) {
|
|
case Number:
|
|
case Object:
|
|
case Boolean:
|
|
case Array:
|
|
case String:
|
|
jsonContent.push(c)
|
|
break
|
|
default:
|
|
packJsonContent()
|
|
switch (c.constructor) {
|
|
case Uint8Array:
|
|
case ArrayBuffer:
|
|
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
|
|
left.integrate(transaction, 0)
|
|
break
|
|
default:
|
|
if (c instanceof AbstractType) {
|
|
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
|
|
left.integrate(transaction, 0)
|
|
} else {
|
|
throw new Error('Unexpected content type in insert operation')
|
|
}
|
|
}
|
|
}
|
|
})
|
|
packJsonContent()
|
|
}
|
|
|
|
/**
|
|
* @param {Transaction} transaction
|
|
* @param {AbstractType<any>} parent
|
|
* @param {number} index
|
|
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
|
if (index === 0) {
|
|
if (parent._searchMarker) {
|
|
updateMarkerChanges(parent._searchMarker, index, content.length)
|
|
}
|
|
return typeListInsertGenericsAfter(transaction, parent, null, content)
|
|
}
|
|
const startIndex = index
|
|
const marker = findMarker(parent, index)
|
|
let n = parent._start
|
|
if (marker !== null) {
|
|
n = marker.p
|
|
index -= marker.index
|
|
// we need to iterate one to the left so that the algorithm works
|
|
if (index === 0) {
|
|
// @todo refactor this as it actually doesn't consider formats
|
|
n = n.prev // important! get the left undeleted item so that we can actually decrease index
|
|
index += (n && n.countable && !n.deleted) ? n.length : 0
|
|
}
|
|
}
|
|
for (; n !== null; n = n.right) {
|
|
if (!n.deleted && n.countable) {
|
|
if (index <= n.length) {
|
|
if (index < n.length) {
|
|
// insert in-between
|
|
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
|
|
}
|
|
break
|
|
}
|
|
index -= n.length
|
|
}
|
|
}
|
|
if (parent._searchMarker) {
|
|
updateMarkerChanges(parent._searchMarker, startIndex, content.length)
|
|
}
|
|
return typeListInsertGenericsAfter(transaction, parent, n, content)
|
|
}
|
|
|
|
/**
|
|
* @param {Transaction} transaction
|
|
* @param {AbstractType<any>} parent
|
|
* @param {number} index
|
|
* @param {number} length
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeListDelete = (transaction, parent, index, length) => {
|
|
if (length === 0) { return }
|
|
const startIndex = index
|
|
const startLength = length
|
|
const marker = findMarker(parent, index)
|
|
let n = parent._start
|
|
if (marker !== null) {
|
|
n = marker.p
|
|
index -= marker.index
|
|
}
|
|
// compute the first item to be deleted
|
|
for (; n !== null && index > 0; n = n.right) {
|
|
if (!n.deleted && n.countable) {
|
|
if (index < n.length) {
|
|
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
|
|
}
|
|
index -= n.length
|
|
}
|
|
}
|
|
// delete all items until done
|
|
while (length > 0 && n !== null) {
|
|
if (!n.deleted) {
|
|
if (length < n.length) {
|
|
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
|
|
}
|
|
n.delete(transaction)
|
|
length -= n.length
|
|
}
|
|
n = n.right
|
|
}
|
|
if (length > 0) {
|
|
throw error.create('array length exceeded')
|
|
}
|
|
if (parent._searchMarker) {
|
|
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Transaction} transaction
|
|
* @param {AbstractType<any>} parent
|
|
* @param {string} key
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeMapDelete = (transaction, parent, key) => {
|
|
const c = parent._map.get(key)
|
|
if (c !== undefined) {
|
|
c.delete(transaction)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Transaction} transaction
|
|
* @param {AbstractType<any>} parent
|
|
* @param {string} key
|
|
* @param {Object|number|Array<any>|string|Uint8Array|AbstractType<any>} value
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeMapSet = (transaction, parent, key, value) => {
|
|
const left = parent._map.get(key) || null
|
|
const doc = transaction.doc
|
|
const ownClientId = doc.clientID
|
|
let content
|
|
if (value == null) {
|
|
content = new ContentAny([value])
|
|
} else {
|
|
switch (value.constructor) {
|
|
case Number:
|
|
case Object:
|
|
case Boolean:
|
|
case Array:
|
|
case String:
|
|
content = new ContentAny([value])
|
|
break
|
|
case Uint8Array:
|
|
content = new ContentBinary(/** @type {Uint8Array} */ (value))
|
|
break
|
|
default:
|
|
if (value instanceof AbstractType) {
|
|
content = new ContentType(value)
|
|
} else {
|
|
throw new Error('Unexpected content type')
|
|
}
|
|
}
|
|
}
|
|
new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0)
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractType<any>} parent
|
|
* @param {string} key
|
|
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeMapGet = (parent, key) => {
|
|
const val = parent._map.get(key)
|
|
return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractType<any>} parent
|
|
* @return {Object<string,Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeMapGetAll = (parent) => {
|
|
/**
|
|
* @type {Object<string,any>}
|
|
*/
|
|
const res = {}
|
|
parent._map.forEach((value, key) => {
|
|
if (!value.deleted) {
|
|
res[key] = value.content.getContent()[value.length - 1]
|
|
}
|
|
})
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractType<any>} parent
|
|
* @param {string} key
|
|
* @return {boolean}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeMapHas = (parent, key) => {
|
|
const val = parent._map.get(key)
|
|
return val !== undefined && !val.deleted
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractType<any>} parent
|
|
* @param {string} key
|
|
* @param {Snapshot} snapshot
|
|
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const typeMapGetSnapshot = (parent, key, snapshot) => {
|
|
let v = parent._map.get(key) || null
|
|
while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
|
|
v = v.left
|
|
}
|
|
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
|
|
}
|
|
|
|
/**
|
|
* @param {Map<string,Item>} map
|
|
* @return {IterableIterator<Array<any>>}
|
|
*
|
|
* @private
|
|
* @function
|
|
*/
|
|
export const createMapIterator = map => iterator.iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)
|