prelim refactor commit
This commit is contained in:
parent
293527e62b
commit
d9ab593b07
10
src/index.js
10
src/index.js
@ -2,9 +2,7 @@
|
|||||||
export { Y } from './utils/Y.js'
|
export { Y } from './utils/Y.js'
|
||||||
export { UndoManager } from './utils/UndoManager.js'
|
export { UndoManager } from './utils/UndoManager.js'
|
||||||
export { Transaction } from './utils/Transaction.js'
|
export { Transaction } from './utils/Transaction.js'
|
||||||
|
export { Delete } from './Delete.js'
|
||||||
export { Item, stringifyItemID } from './structs/Item.js'
|
|
||||||
export { Delete } from './structs/Delete.js'
|
|
||||||
export { ItemJSON } from './structs/ItemJSON.js'
|
export { ItemJSON } from './structs/ItemJSON.js'
|
||||||
export { ItemString } from './structs/ItemString.js'
|
export { ItemString } from './structs/ItemString.js'
|
||||||
export { ItemFormat } from './structs/ItemFormat.js'
|
export { ItemFormat } from './structs/ItemFormat.js'
|
||||||
@ -21,10 +19,8 @@ export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/
|
|||||||
|
|
||||||
export { getRelativePosition, fromRelativePosition, equal as equalRelativePosition } from './utils/relativePosition.js'
|
export { getRelativePosition, fromRelativePosition, equal as equalRelativePosition } from './utils/relativePosition.js'
|
||||||
|
|
||||||
export { ID, createID, RootFakeUserID, RootID } from './utils/ID.js'
|
export { ID, createID } from './utils/ID.js'
|
||||||
export { DeleteStore, DSNode } from './utils/DeleteStore.js'
|
export { DeleteStore, DSNode } from './utils/DeleteSet.js/index.js'
|
||||||
export { deleteItemRange } from './utils/structManipulation.js'
|
export { deleteItemRange } from './utils/structManipulation.js'
|
||||||
export { integrateRemoteStructs } from './utils/integrateRemoteStructs.js'
|
export { integrateRemoteStructs } from './utils/integrateRemoteStructs.js'
|
||||||
export { isParentOf } from './utils/isParentOf.js'
|
export { isParentOf } from './utils/isParentOf.js'
|
||||||
|
|
||||||
export * from './utils/structReferences.js'
|
|
||||||
|
570
src/structs/AbstractItem.js
Normal file
570
src/structs/AbstractItem.js
Normal file
@ -0,0 +1,570 @@
|
|||||||
|
/**
|
||||||
|
* @module structs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readID, createID, writeID, writeNullID, ID, createNextID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { Delete } from '../Delete.js'
|
||||||
|
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
||||||
|
import { GC } from './GC.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import { ItemType } from './ItemType.js' // eslint-disable-line
|
||||||
|
import { AbstractType } from '../types/AbstractType.js'
|
||||||
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
import * as maplib from 'lib0/map.js'
|
||||||
|
import * as set from 'lib0/set.js'
|
||||||
|
import * as binary from 'lib0/binary.js'
|
||||||
|
import { AbstractRef, AbstractStruct } from './AbstractStruct.js' // eslint-disable-line
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stringify an item id.
|
||||||
|
*
|
||||||
|
* @param { ID } id
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const stringifyID = id => `(${id.client},${id.clock})`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent).
|
||||||
|
*
|
||||||
|
* @param {AbstractItem | null} item
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const stringifyItemID = item =>
|
||||||
|
item === null ? '()' : (item.id != null ? stringifyID(item.id) : 'y')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper utility to convert an item to a readable format.
|
||||||
|
*
|
||||||
|
* @param {String} name The name of the item class (YText, ItemString, ..).
|
||||||
|
* @param {AbstractItem} item The item instance.
|
||||||
|
* @param {String} [append] Additional information to append to the returned
|
||||||
|
* string.
|
||||||
|
* @return {String} A readable string that represents the item object.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const logItemHelper = (name, item, append) => {
|
||||||
|
const left = item.left !== null ? stringifyID(item.left.lastId) : '()'
|
||||||
|
const origin = item.origin !== null ? stringifyID(item.origin.lastId) : '()'
|
||||||
|
return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item.right)},parent:${stringifyItemID(item.parent)},parentSub:${item.parentSub}${append !== undefined ? ' - ' + append : ''})`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split leftItem into two items
|
||||||
|
* @param {AbstractItem} leftItem
|
||||||
|
* @param {Y} y
|
||||||
|
* @param {number} diff
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
|
export const splitItem = (leftItem, diff) => {
|
||||||
|
const id = leftItem.id
|
||||||
|
// create rightItem
|
||||||
|
const rightItem = leftItem.copy(createID(id.client, id.clock + diff), leftItem, leftItem.rightOrigin, leftItem.parent, leftItem.parentSub)
|
||||||
|
rightItem.right = leftItem.right
|
||||||
|
if (leftItem.deleted) {
|
||||||
|
rightItem.deleted = true
|
||||||
|
}
|
||||||
|
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
|
||||||
|
leftItem.right = rightItem
|
||||||
|
// update right
|
||||||
|
if (rightItem.right !== null) {
|
||||||
|
rightItem.right.left = rightItem
|
||||||
|
}
|
||||||
|
// update all origins to the right
|
||||||
|
// search all relevant items to the right and update origin
|
||||||
|
// if origin is not it foundOrigins, we don't have to search any longer
|
||||||
|
const foundOrigins = new Set()
|
||||||
|
foundOrigins.add(leftItem)
|
||||||
|
let o = rightItem.right
|
||||||
|
while (o !== null && foundOrigins.has(o.origin)) {
|
||||||
|
if (o.origin === leftItem) {
|
||||||
|
o.origin = rightItem
|
||||||
|
}
|
||||||
|
foundOrigins.add(o)
|
||||||
|
o = o.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class that represents any content.
|
||||||
|
*/
|
||||||
|
export class AbstractItem extends AbstractStruct {
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {AbstractType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
*/
|
||||||
|
constructor (id, left, right, parent, parentSub) {
|
||||||
|
if (left !== null) {
|
||||||
|
parent = left.parent
|
||||||
|
parentSub = left.parentSub
|
||||||
|
} else if (right !== null) {
|
||||||
|
parent = right.parent
|
||||||
|
parentSub = right.parentSub
|
||||||
|
} else if (parent === null) {
|
||||||
|
error.throwUnexpectedCase()
|
||||||
|
}
|
||||||
|
super(id)
|
||||||
|
/**
|
||||||
|
* The item that was originally to the left of this item.
|
||||||
|
* @type {AbstractItem | null}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.origin = left
|
||||||
|
/**
|
||||||
|
* The item that is currently to the left of this item.
|
||||||
|
* @type {AbstractItem | null}
|
||||||
|
*/
|
||||||
|
this.left = left
|
||||||
|
/**
|
||||||
|
* The item that is currently to the right of this item.
|
||||||
|
* @type {AbstractItem | null}
|
||||||
|
*/
|
||||||
|
this.right = right
|
||||||
|
/**
|
||||||
|
* The item that was originally to the right of this item.
|
||||||
|
* @readonly
|
||||||
|
* @type {AbstractItem | null}
|
||||||
|
*/
|
||||||
|
this.rightOrigin = right
|
||||||
|
/**
|
||||||
|
* The parent type.
|
||||||
|
* @type {AbstractType}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.parent = parent
|
||||||
|
/**
|
||||||
|
* If the parent refers to this item with some kind of key (e.g. YMap, the
|
||||||
|
* key is specified here. The key is then used to refer to the list in which
|
||||||
|
* to insert this item. If `parentSub = null` type._start is the list in
|
||||||
|
* which to insert to. Otherwise it is `parent._map`.
|
||||||
|
* @type {String | null}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.parentSub = parentSub
|
||||||
|
/**
|
||||||
|
* Whether this item was deleted or not.
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
|
this.deleted = false
|
||||||
|
/**
|
||||||
|
* If this type's effect is reundone this type refers to the type that undid
|
||||||
|
* this operation.
|
||||||
|
* @type {AbstractItem | null}
|
||||||
|
*/
|
||||||
|
this.redone = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
integrate (transaction) {
|
||||||
|
const y = transaction.y
|
||||||
|
const id = this.id
|
||||||
|
const parent = this.parent
|
||||||
|
const parentSub = this.parentSub
|
||||||
|
const length = this.length
|
||||||
|
const left = this.left
|
||||||
|
const right = this.right
|
||||||
|
// integrate
|
||||||
|
const parentType = parent !== null ? parent.type : maplib.setTfUndefined(y.share, parentSub, () => new AbstractType())
|
||||||
|
if (y.ss.getState(id.client) !== id.clock) {
|
||||||
|
throw new Error('Expected other operation')
|
||||||
|
}
|
||||||
|
y.ss.setState(id.client, id.clock + length)
|
||||||
|
transaction.added.add(this)
|
||||||
|
/*
|
||||||
|
# $this has to find a unique position between origin and the next known character
|
||||||
|
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||||
|
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||||
|
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||||
|
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||||
|
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||||
|
# therefore $this would be always to the right of o3
|
||||||
|
# case 2: $origin < $o.origin
|
||||||
|
# if current $this insert_position > $o origin: $this ins
|
||||||
|
# else $insert_position will not change
|
||||||
|
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||||
|
# case 3: $origin > $o.origin
|
||||||
|
# $this insert_position is to the left of $o (forever!)
|
||||||
|
*/
|
||||||
|
// handle conflicts
|
||||||
|
/**
|
||||||
|
* @type {AbstractItem|null}
|
||||||
|
*/
|
||||||
|
let o
|
||||||
|
// set o to the first conflicting item
|
||||||
|
if (left !== null) {
|
||||||
|
o = left.right
|
||||||
|
} else if (this.parentSub !== null) {
|
||||||
|
o = parentType._map.get(parentSub) || null
|
||||||
|
} else {
|
||||||
|
o = parentType._start
|
||||||
|
}
|
||||||
|
const conflictingItems = new Set()
|
||||||
|
const itemsBeforeOrigin = new Set()
|
||||||
|
// Let c in conflictingItems, b in itemsBeforeOrigin
|
||||||
|
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
|
||||||
|
// Note that conflictingItems is a subset of itemsBeforeOrigin
|
||||||
|
while (o !== null && o !== right) {
|
||||||
|
itemsBeforeOrigin.add(o)
|
||||||
|
conflictingItems.add(o)
|
||||||
|
if (this.origin === o.origin) {
|
||||||
|
// case 1
|
||||||
|
if (o.id.client < id.client) {
|
||||||
|
this.left = o
|
||||||
|
conflictingItems.clear()
|
||||||
|
}
|
||||||
|
} else if (itemsBeforeOrigin.has(o.origin)) {
|
||||||
|
// case 2
|
||||||
|
if (!conflictingItems.has(o.origin)) {
|
||||||
|
this.left = o
|
||||||
|
conflictingItems.clear()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// TODO: try to use right_origin instead.
|
||||||
|
// Then you could basically omit conflictingItems!
|
||||||
|
// Note: you probably can't use right_origin in every case.. only when setting _left
|
||||||
|
o = o.right
|
||||||
|
}
|
||||||
|
// reconnect left/right + update parent map/start if necessary
|
||||||
|
if (left !== null) {
|
||||||
|
const right = left.right
|
||||||
|
this.right = right
|
||||||
|
left.right = this
|
||||||
|
if (right !== null) {
|
||||||
|
right.left = this
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let r
|
||||||
|
if (parentSub !== null) {
|
||||||
|
const pmap = parentType._map
|
||||||
|
r = pmap.get(parentSub) || null
|
||||||
|
pmap.set(parentSub, this)
|
||||||
|
} else {
|
||||||
|
r = parentType._start
|
||||||
|
parentType._start = this
|
||||||
|
}
|
||||||
|
this.right = r
|
||||||
|
if (r !== null) {
|
||||||
|
r._left = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// adjust the length of parent
|
||||||
|
if (parentSub === null && this.countable) {
|
||||||
|
parentType._length += length
|
||||||
|
}
|
||||||
|
if (parent !== null && parent.deleted) {
|
||||||
|
this.delete(transaction, false, true)
|
||||||
|
}
|
||||||
|
y.os.put(this)
|
||||||
|
if (parent !== null) {
|
||||||
|
maplib.setTfUndefined(transaction.changed, parent, set.create).add(parentSub)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeStructToTransaction(y._transaction, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next non-deleted item
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
get next () {
|
||||||
|
let n = this.right
|
||||||
|
while (n !== null && n._deleted) {
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the previous non-deleted item
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
get prev () {
|
||||||
|
let n = this.left
|
||||||
|
while (n !== null && n._deleted) {
|
||||||
|
n = n.left
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Item with the same effect as this Item (without position effect)
|
||||||
|
*
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem|null} left
|
||||||
|
* @param {AbstractItem|null} right
|
||||||
|
* @param {ItemType|null} parent
|
||||||
|
* @param {string|null} parentSub
|
||||||
|
* @return {AbstractItem}
|
||||||
|
*/
|
||||||
|
copy (id, left, right, parent, parentSub) {
|
||||||
|
throw new Error('unimplemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redoes the effect of this operation.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance.
|
||||||
|
* @param {Set<AbstractItem>} redoitems
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
redo (y, redoitems) {
|
||||||
|
if (this.redone !== null) {
|
||||||
|
return this.redone
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let parent = this.parent
|
||||||
|
if (parent === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let left, right
|
||||||
|
if (this.parentSub === null) {
|
||||||
|
// Is an array item. Insert at the old position
|
||||||
|
left = this.left
|
||||||
|
right = this
|
||||||
|
} else {
|
||||||
|
// Is a map item. Insert at the start
|
||||||
|
left = null
|
||||||
|
right = parent.type._map.get(this.parentSub)
|
||||||
|
right._delete(y)
|
||||||
|
}
|
||||||
|
// make sure that parent is redone
|
||||||
|
if (parent._deleted === true && parent.redone === null) {
|
||||||
|
// try to undo parent if it will be undone anyway
|
||||||
|
if (!redoitems.has(parent) || !parent.redo(y, redoitems)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent.redone !== null) {
|
||||||
|
while (parent.redone !== null) {
|
||||||
|
parent = parent.redone
|
||||||
|
}
|
||||||
|
// find next cloned_redo items
|
||||||
|
while (left !== null) {
|
||||||
|
if (left.redone !== null && left.redone.parent === parent) {
|
||||||
|
left = left.redone
|
||||||
|
break
|
||||||
|
}
|
||||||
|
left = left.left
|
||||||
|
}
|
||||||
|
while (right !== null) {
|
||||||
|
if (right.redone !== null && right.redone.parent === parent) {
|
||||||
|
right = right.redone
|
||||||
|
}
|
||||||
|
right = right._right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.redone = this.copy(createNextID(y), left, right, parent, this.parentSub)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the last content address of this Item.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
get lastId () {
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
const id = this.id
|
||||||
|
return createID(id.user, id.clock + this.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the length of this Item.
|
||||||
|
*/
|
||||||
|
get length () {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should return false if this Item is some kind of meta information
|
||||||
|
* (e.g. format information).
|
||||||
|
*
|
||||||
|
* * Whether this Item should be addressable via `yarray.get(i)`
|
||||||
|
* * Whether this Item should be counted when computing yarray.length
|
||||||
|
*/
|
||||||
|
get countable () {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits this Item so that another Item can be inserted in-between.
|
||||||
|
* This must be overwritten if _length > 1
|
||||||
|
* Returns right part after split
|
||||||
|
*
|
||||||
|
* (see {@link ItemJSON}/{@link ItemString} for implementation)
|
||||||
|
*
|
||||||
|
* Does not integrate the struct, nor store it in struct store.
|
||||||
|
*
|
||||||
|
* This method should only be cally by StructStore.
|
||||||
|
*
|
||||||
|
* @param {number} diff
|
||||||
|
* @return {AbstractItem}
|
||||||
|
*/
|
||||||
|
splitAt (diff) {
|
||||||
|
throw new Error('unimplemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark this Item as deleted.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {boolean} createDelete Whether to propagate a message that this
|
||||||
|
* Type was deleted.
|
||||||
|
* @param {boolean} [gcChildren]
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
delete (transaction, createDelete = true, gcChildren) {
|
||||||
|
if (!this.deleted) {
|
||||||
|
const y = transaction.y
|
||||||
|
const parent = this.parent
|
||||||
|
const len = this.length
|
||||||
|
// adjust the length of parent
|
||||||
|
if (this.countable && this.parentSub === null) {
|
||||||
|
if (parent !== null) {
|
||||||
|
// parent is y
|
||||||
|
y.get(this.)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
transaction.y.get(this.parentSub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent.length !== undefined && this.countable) {
|
||||||
|
parent.length -= len
|
||||||
|
}
|
||||||
|
this._deleted = true
|
||||||
|
y.ds.mark(this.id, this.length, false)
|
||||||
|
let del = new Delete(this.id, len)
|
||||||
|
if (createDelete) {
|
||||||
|
// broadcast and persists Delete
|
||||||
|
del.integrate(y, true)
|
||||||
|
}
|
||||||
|
if (parent !== null) {
|
||||||
|
maplib.setTfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
|
||||||
|
}
|
||||||
|
transaction.deleted.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Y} y
|
||||||
|
*/
|
||||||
|
gcChildren (y) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Y} y
|
||||||
|
*/
|
||||||
|
gc (y) {
|
||||||
|
if (this.id !== null) {
|
||||||
|
y.os.replace(this, new GC(this.id, this.length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getContent () {
|
||||||
|
throw new Error('Must implement') // TODO: create function in lib0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the properties of this type to binary and write it to an
|
||||||
|
* BinaryEncoder.
|
||||||
|
*
|
||||||
|
* This is called when this Item is sent to a remote peer.
|
||||||
|
*
|
||||||
|
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||||
|
* @param {number} encodingRef
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
write (encoder, encodingRef) {
|
||||||
|
const info = (encodingRef & binary.BITS5) |
|
||||||
|
((this.origin === null) ? 0 : binary.BIT8) | // origin is defined
|
||||||
|
((this.rightOrigin === null) ? 0 : binary.BIT7) | // right origin is defined
|
||||||
|
((this.parentSub !== null) ? 0 : binary.BIT6) // parentSub is non-null
|
||||||
|
encoding.writeUint8(encoder, info)
|
||||||
|
writeID(encoder, this.id)
|
||||||
|
if (this.origin !== null) {
|
||||||
|
writeID(encoder, this.origin.lastId)
|
||||||
|
}
|
||||||
|
if (this.rightOrigin !== null) {
|
||||||
|
writeID(encoder, this.rightOrigin.id)
|
||||||
|
}
|
||||||
|
if (this.origin === null && this.rightOrigin === null) {
|
||||||
|
if (this.parent === null) {
|
||||||
|
writeNullID(encoder)
|
||||||
|
} else {
|
||||||
|
// neither origin nor right is defined
|
||||||
|
writeID(encoder, this.parent.id)
|
||||||
|
}
|
||||||
|
if (this.parentSub !== null) {
|
||||||
|
encoding.writeVarString(encoder, this.parentSub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbstractItemRef extends AbstractRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, info) {
|
||||||
|
super()
|
||||||
|
const id = readID(decoder)
|
||||||
|
if (id === null) {
|
||||||
|
throw new Error('id must not be null')
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The uniqe identifier of this type.
|
||||||
|
* @type {ID}
|
||||||
|
*/
|
||||||
|
this.id = id
|
||||||
|
/**
|
||||||
|
* The item that was originally to the left of this item.
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.left = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
|
||||||
|
/**
|
||||||
|
* The item that was originally to the right of this item.
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.right = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
|
||||||
|
const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
||||||
|
/**
|
||||||
|
* The parent type.
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.parent = canCopyParentInfo ? readID(decoder) : null
|
||||||
|
/**
|
||||||
|
* If the parent refers to this item with some kind of key (e.g. YMap, the
|
||||||
|
* key is specified here. The key is then used to refer to the list in which
|
||||||
|
* to insert this item. If `parentSub = null` type._start is the list in
|
||||||
|
* which to insert to. Otherwise it is `parent._map`.
|
||||||
|
* @type {String | null}
|
||||||
|
*/
|
||||||
|
this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @return {Array<ID|null>}
|
||||||
|
*/
|
||||||
|
getMissing () {
|
||||||
|
return [
|
||||||
|
createID(this.id.client, this.id.clock - 1),
|
||||||
|
this.left,
|
||||||
|
this.right,
|
||||||
|
this.parent
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
38
src/structs/AbstractStruct.js
Normal file
38
src/structs/AbstractStruct.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { ID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export class AbstractStruct {
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
*/
|
||||||
|
constructor (id) {
|
||||||
|
/**
|
||||||
|
* The uniqe identifier of this struct.
|
||||||
|
* @type {ID}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.id = id
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
get length () {
|
||||||
|
throw new Error('unimplemented')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbstractRef {
|
||||||
|
/**
|
||||||
|
* @return {Array<ID|null>}
|
||||||
|
*/
|
||||||
|
getMissing () {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @return {AbstractStruct}
|
||||||
|
*/
|
||||||
|
toStruct (transaction) { throw new Error('Must be defined') }
|
||||||
|
}
|
@ -1,103 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module structs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getStructReference } from '../utils/structReferences.js'
|
|
||||||
import * as ID from '../utils/ID.js'
|
|
||||||
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
|
||||||
import * as decoding from 'lib0/decoding.js'
|
|
||||||
import * as encoding from 'lib0/encoding.js'
|
|
||||||
import * as Item from './Item.js'
|
|
||||||
// import { Y } from '../utils/Y.js' // eslint-disable-line
|
|
||||||
import { deleteItemRange } from '../utils/structManipulation.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* A Delete change is not a real Item, but it provides the same interface as an
|
|
||||||
* Item. The only difference is that it will not be saved in the ItemStore
|
|
||||||
* (OperationStore), but instead it is safed in the DeleteStore.
|
|
||||||
*/
|
|
||||||
export class Delete {
|
|
||||||
constructor () {
|
|
||||||
/**
|
|
||||||
* @type {ID.ID}
|
|
||||||
*/
|
|
||||||
this._targetID = null
|
|
||||||
/**
|
|
||||||
* @type {Item.Item}
|
|
||||||
*/
|
|
||||||
this._target = null
|
|
||||||
this._length = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Read the next Item in a Decoder and fill this Item with the read data.
|
|
||||||
*
|
|
||||||
* This is called when data is received from a remote peer.
|
|
||||||
*
|
|
||||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
|
||||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
|
||||||
*/
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
// TODO: set target, and add it to missing if not found
|
|
||||||
// There is an edge case in p2p networks!
|
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
const targetID = ID.decode(decoder)
|
|
||||||
this._targetID = targetID
|
|
||||||
this._length = decoding.readVarUint(decoder)
|
|
||||||
if (y.os.getItem(targetID) === null) {
|
|
||||||
return [targetID]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Transform the properties of this type to binary and write it to an
|
|
||||||
* BinaryEncoder.
|
|
||||||
*
|
|
||||||
* This is called when this Item is sent to a remote peer.
|
|
||||||
*
|
|
||||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
|
||||||
*/
|
|
||||||
_toBinary (encoder) {
|
|
||||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
|
||||||
this._targetID.encode(encoder)
|
|
||||||
encoding.writeVarUint(encoder, this._length)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Integrates this Item into the shared structure.
|
|
||||||
*
|
|
||||||
* This method actually applies the change to the Yjs instance. In the case of
|
|
||||||
* Delete it marks the delete target as deleted.
|
|
||||||
*
|
|
||||||
* * If created remotely (a remote user deleted something),
|
|
||||||
* this Delete is applied to all structs in id-range.
|
|
||||||
* * If created lokally (e.g. when y-array deletes a range of elements),
|
|
||||||
* this struct is broadcasted only (it is already executed)
|
|
||||||
*/
|
|
||||||
_integrate (y, locallyCreated = false) {
|
|
||||||
if (!locallyCreated) {
|
|
||||||
// from remote
|
|
||||||
const id = this._targetID
|
|
||||||
deleteItemRange(y, id.user, id.clock, this._length, false)
|
|
||||||
}
|
|
||||||
writeStructToTransaction(y._transaction, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform this YXml Type to a readable format.
|
|
||||||
* Useful for logging as all Items and Delete implement this method.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_logString () {
|
|
||||||
return `Delete - target: ${Item.stringifyID(this._targetID)}, len: ${this._length}`
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +1,69 @@
|
|||||||
/**
|
/**
|
||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
import { AbstractRef, AbstractStruct } from './AbstractStruct.js'
|
||||||
import { getStructReference } from '../utils/structReferences.js'
|
import { ID, readID, createID, writeID } from '../utils/ID.js' // eslint-disable-line
|
||||||
import * as ID from '../utils/ID.js'
|
|
||||||
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
// import { Y } from '../utils/Y.js' // eslint-disable-line
|
|
||||||
|
export const structGCRefNumber = 0
|
||||||
|
|
||||||
// TODO should have the same base class as Item
|
// TODO should have the same base class as Item
|
||||||
export class GC {
|
export class GC extends AbstractStruct {
|
||||||
constructor () {
|
|
||||||
/**
|
/**
|
||||||
* @type {ID.ID}
|
* @param {ID} id
|
||||||
|
* @param {number} length
|
||||||
*/
|
*/
|
||||||
this._id = null
|
constructor (id, length) {
|
||||||
this._length = 0
|
super(id)
|
||||||
}
|
/**
|
||||||
|
* @type {number}
|
||||||
get _redone () {
|
*/
|
||||||
return null
|
this.length = length
|
||||||
}
|
|
||||||
|
|
||||||
get _deleted () {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
_integrate (y) {
|
|
||||||
const id = this._id
|
|
||||||
const userState = y.ss.getState(id.user)
|
|
||||||
if (id.clock === userState) {
|
|
||||||
y.ss.setState(id.user, id.clock + this._length)
|
|
||||||
}
|
|
||||||
y.ds.mark(this._id, this._length, true)
|
|
||||||
let n = y.os.put(this)
|
|
||||||
const prev = n.prev().val
|
|
||||||
if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) {
|
|
||||||
// TODO: do merging for all items!
|
|
||||||
prev._length += n.val._length
|
|
||||||
y.os.delete(n.val._id)
|
|
||||||
n = prev
|
|
||||||
}
|
|
||||||
if (n.val) {
|
|
||||||
n = n.val
|
|
||||||
}
|
|
||||||
const next = y.os.findNext(n._id)
|
|
||||||
if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) {
|
|
||||||
n._length += next._length
|
|
||||||
y.os.delete(next._id)
|
|
||||||
}
|
|
||||||
if (id.user !== ID.RootFakeUserID) {
|
|
||||||
writeStructToTransaction(y._transaction, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform the properties of this type to binary and write it to an
|
* @param {encoding.Encoder} encoder
|
||||||
* BinaryEncoder.
|
|
||||||
*
|
|
||||||
* This is called when this Item is sent to a remote peer.
|
|
||||||
*
|
|
||||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
_toBinary (encoder) {
|
write (encoder) {
|
||||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
encoding.writeUint8(encoder, structGCRefNumber)
|
||||||
this._id.encode(encoder)
|
writeID(encoder, this.id)
|
||||||
encoding.writeVarUint(encoder, this._length)
|
encoding.writeVarUint(encoder, this.length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class GCRef extends AbstractRef {
|
||||||
/**
|
/**
|
||||||
* Read the next Item in a Decoder and fill this Item with the read data.
|
* @param {decoding.Decoder} decoder
|
||||||
*
|
* @param {number} info
|
||||||
* This is called when data is received from a remote peer.
|
|
||||||
*
|
|
||||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
|
||||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
_fromBinary (y, decoder) {
|
constructor (decoder, info) {
|
||||||
|
super()
|
||||||
|
const id = readID(decoder)
|
||||||
|
if (id === null) {
|
||||||
|
throw new Error('expected id')
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {ID}
|
||||||
*/
|
*/
|
||||||
const id = ID.decode(decoder)
|
this.id = id
|
||||||
this._id = id
|
/**
|
||||||
this._length = decoding.readVarUint(decoder)
|
* @type {number}
|
||||||
const missing = []
|
*/
|
||||||
if (y.ss.getState(id.user) < id.clock) {
|
this.length = decoding.readVarUint(decoder)
|
||||||
missing.push(ID.createID(id.user, id.clock - 1))
|
|
||||||
}
|
}
|
||||||
return missing
|
missing () {
|
||||||
|
return [
|
||||||
|
createID(this.id.client, this.id.clock - 1)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
_splitAt () {
|
* @return {GC}
|
||||||
return this
|
*/
|
||||||
}
|
toStruct () {
|
||||||
|
return new GC(
|
||||||
_clonePartial (diff) {
|
this.id,
|
||||||
const gc = new GC()
|
this.length
|
||||||
gc._id = ID.createID(this._id.user, this._id.clock + diff)
|
)
|
||||||
gc._length = this._length - diff
|
|
||||||
return gc
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,634 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module structs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getStructReference } from '../utils/structReferences.js'
|
|
||||||
import * as ID from '../utils/ID.js'
|
|
||||||
import { Delete } from './Delete.js'
|
|
||||||
import { writeStructToTransaction } from '../utils/structEncoding.js'
|
|
||||||
import { GC } from './GC.js'
|
|
||||||
import * as encoding from 'lib0/encoding.js'
|
|
||||||
import * as decoding from 'lib0/decoding.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stringify an item id.
|
|
||||||
*
|
|
||||||
* @param {ID.ID | ID.RootID} id
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export const stringifyID = id => id instanceof ID.ID ? `(${id.user},${id.clock})` : `(${id.name},${id.type})`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stringify an item as ID. HHere, an item could also be a Yjs instance (e.g. item._parent).
|
|
||||||
*
|
|
||||||
* @param {Item | Y | null} item
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export const stringifyItemID = item =>
|
|
||||||
item === null ? '()' : (item._id != null ? stringifyID(item._id) : 'y')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper utility to convert an item to a readable format.
|
|
||||||
*
|
|
||||||
* @param {String} name The name of the item class (YText, ItemString, ..).
|
|
||||||
* @param {Item} item The item instance.
|
|
||||||
* @param {String} [append] Additional information to append to the returned
|
|
||||||
* string.
|
|
||||||
* @return {String} A readable string that represents the item object.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const logItemHelper = (name, item, append) => {
|
|
||||||
const left = item._left !== null ? stringifyID(item._left._lastId) : '()'
|
|
||||||
const origin = item._origin !== null ? stringifyID(item._origin._lastId) : '()'
|
|
||||||
return `${name}(id:${stringifyItemID(item)},left:${left},origin:${origin},right:${stringifyItemID(item._right)},parent:${stringifyItemID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export const transactionTypeChanged = (y, type, sub) => {
|
|
||||||
if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) {
|
|
||||||
const changedTypes = y._transaction.changedTypes
|
|
||||||
let subs = changedTypes.get(type)
|
|
||||||
if (subs === undefined) {
|
|
||||||
// create if it doesn't exist yet
|
|
||||||
subs = new Set()
|
|
||||||
changedTypes.set(type, subs)
|
|
||||||
}
|
|
||||||
subs.add(sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper utility to split an Item (see {@link Item#_splitAt})
|
|
||||||
* - copies all properties from a to b
|
|
||||||
* - connects a to b
|
|
||||||
* - assigns the correct _id
|
|
||||||
* - saves b to os
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export const splitHelper = (y, a, b, diff) => {
|
|
||||||
const aID = a._id
|
|
||||||
b._id = ID.createID(aID.user, aID.clock + diff)
|
|
||||||
b._origin = a
|
|
||||||
b._left = a
|
|
||||||
b._right = a._right
|
|
||||||
if (b._right !== null) {
|
|
||||||
b._right._left = b
|
|
||||||
}
|
|
||||||
b._right_origin = a._right_origin
|
|
||||||
// do not set a._right_origin, as this will lead to problems when syncing
|
|
||||||
a._right = b
|
|
||||||
b._parent = a._parent
|
|
||||||
b._parentSub = a._parentSub
|
|
||||||
b._deleted = a._deleted
|
|
||||||
// now search all relevant items to the right and update origin
|
|
||||||
// if origin is not it foundOrigins, we don't have to search any longer
|
|
||||||
let foundOrigins = new Set()
|
|
||||||
foundOrigins.add(a)
|
|
||||||
let o = b._right
|
|
||||||
while (o !== null && foundOrigins.has(o._origin)) {
|
|
||||||
if (o._origin === a) {
|
|
||||||
o._origin = b
|
|
||||||
}
|
|
||||||
foundOrigins.add(o)
|
|
||||||
o = o._right
|
|
||||||
}
|
|
||||||
y.os.put(b)
|
|
||||||
if (y._transaction !== null) {
|
|
||||||
if (y._transaction.newTypes.has(a)) {
|
|
||||||
y._transaction.newTypes.add(b)
|
|
||||||
} else if (y._transaction.deletedStructs.has(a)) {
|
|
||||||
y._transaction.deletedStructs.add(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class that represents any content.
|
|
||||||
*/
|
|
||||||
export class Item {
|
|
||||||
constructor () {
|
|
||||||
/**
|
|
||||||
* The uniqe identifier of this type.
|
|
||||||
* @type {ID.ID | ID.RootID | null}
|
|
||||||
*/
|
|
||||||
this._id = null
|
|
||||||
/**
|
|
||||||
* The item that was originally to the left of this item.
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
this._origin = null
|
|
||||||
/**
|
|
||||||
* The item that is currently to the left of this item.
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
this._left = null
|
|
||||||
/**
|
|
||||||
* The item that is currently to the right of this item.
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
this._right = null
|
|
||||||
/**
|
|
||||||
* The item that was originally to the right of this item.
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
this._right_origin = null
|
|
||||||
/**
|
|
||||||
* The parent type.
|
|
||||||
* @type {Y | Type | null}
|
|
||||||
*/
|
|
||||||
this._parent = null
|
|
||||||
/**
|
|
||||||
* If the parent refers to this item with some kind of key (e.g. YMap, the
|
|
||||||
* key is specified here. The key is then used to refer to the list in which
|
|
||||||
* to insert this item. If `parentSub = null` type._start is the list in
|
|
||||||
* which to insert to. Otherwise it is `parent._map`.
|
|
||||||
* @type {String | null}
|
|
||||||
*/
|
|
||||||
this._parentSub = null
|
|
||||||
/**
|
|
||||||
* Whether this item was deleted or not.
|
|
||||||
* @type {Boolean}
|
|
||||||
*/
|
|
||||||
this._deleted = false
|
|
||||||
/**
|
|
||||||
* If this type's effect is reundone this type refers to the type that undid
|
|
||||||
* this operation.
|
|
||||||
* @type {Type | null}
|
|
||||||
*/
|
|
||||||
this._redone = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next non-deleted item
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
get _next () {
|
|
||||||
let n = this._right
|
|
||||||
while (n !== null && n._deleted) {
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the previous non-deleted item
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
get _prev () {
|
|
||||||
let n = this._left
|
|
||||||
while (n !== null && n._deleted) {
|
|
||||||
n = n._left
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an Item with the same effect as this Item (without position effect)
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_copy () {
|
|
||||||
const C = this.constructor
|
|
||||||
// @ts-ignore
|
|
||||||
return new C()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redoes the effect of this operation.
|
|
||||||
*
|
|
||||||
* @param {Y} y The Yjs instance.
|
|
||||||
* @param {Set<Item>} redoitems
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_redo (y, redoitems) {
|
|
||||||
if (this._redone !== null) {
|
|
||||||
return this._redone
|
|
||||||
}
|
|
||||||
if (!(this._parent instanceof Item)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let struct = this._copy()
|
|
||||||
let left, right
|
|
||||||
if (this._parentSub === null) {
|
|
||||||
// Is an array item. Insert at the old position
|
|
||||||
left = this._left
|
|
||||||
right = this
|
|
||||||
} else {
|
|
||||||
// Is a map item. Insert at the start
|
|
||||||
left = null
|
|
||||||
right = this._parent._map.get(this._parentSub)
|
|
||||||
right._delete(y)
|
|
||||||
}
|
|
||||||
let parent = this._parent
|
|
||||||
// make sure that parent is redone
|
|
||||||
if (parent._deleted === true && parent._redone === null) {
|
|
||||||
// try to undo parent if it will be undone anyway
|
|
||||||
if (!redoitems.has(parent) || !parent._redo(y, redoitems)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parent._redone !== null) {
|
|
||||||
parent = parent._redone
|
|
||||||
// find next cloned_redo items
|
|
||||||
while (left !== null) {
|
|
||||||
if (left._redone !== null && left._redone._parent === parent) {
|
|
||||||
left = left._redone
|
|
||||||
break
|
|
||||||
}
|
|
||||||
left = left._left
|
|
||||||
}
|
|
||||||
while (right !== null) {
|
|
||||||
if (right._redone !== null && right._redone._parent === parent) {
|
|
||||||
right = right._redone
|
|
||||||
}
|
|
||||||
right = right._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
struct._origin = left
|
|
||||||
struct._left = left
|
|
||||||
struct._right = right
|
|
||||||
struct._right_origin = right
|
|
||||||
struct._parent = parent
|
|
||||||
struct._parentSub = this._parentSub
|
|
||||||
struct._integrate(y)
|
|
||||||
this._redone = struct
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the last content address of this Item.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
get _lastId () {
|
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
const id = this._id
|
|
||||||
return ID.createID(id.user, id.clock + this._length - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the length of this Item.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
get _length () {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should return false if this Item is some kind of meta information
|
|
||||||
* (e.g. format information).
|
|
||||||
*
|
|
||||||
* * Whether this Item should be addressable via `yarray.get(i)`
|
|
||||||
* * Whether this Item should be counted when computing yarray.length
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
get _countable () {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits this Item so that another Items can be inserted in-between.
|
|
||||||
* This must be overwritten if _length > 1
|
|
||||||
* Returns right part after split
|
|
||||||
* * diff === 0 => this
|
|
||||||
* * diff === length => this._right
|
|
||||||
* * otherwise => split _content and return right part of split
|
|
||||||
* (see {@link ItemJSON}/{@link ItemString} for implementation)
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_splitAt (y, diff) {
|
|
||||||
if (diff === 0) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark this Item as deleted.
|
|
||||||
*
|
|
||||||
* @param {Y} y The Yjs instance
|
|
||||||
* @param {boolean} createDelete Whether to propagate a message that this
|
|
||||||
* Type was deleted.
|
|
||||||
* @param {boolean} [gcChildren]
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_delete (y, createDelete = true, gcChildren) {
|
|
||||||
if (!this._deleted) {
|
|
||||||
/**
|
|
||||||
* @type { Type }
|
|
||||||
*/
|
|
||||||
const parent = this._parent
|
|
||||||
const len = this._length
|
|
||||||
// adjust the length of parent
|
|
||||||
if (parent.length !== undefined && this._countable) {
|
|
||||||
parent.length -= len
|
|
||||||
}
|
|
||||||
this._deleted = true
|
|
||||||
y.ds.mark(this._id, this._length, false)
|
|
||||||
let del = new Delete()
|
|
||||||
del._targetID = this._id
|
|
||||||
del._length = len
|
|
||||||
if (createDelete) {
|
|
||||||
// broadcast and persists Delete
|
|
||||||
del._integrate(y, true)
|
|
||||||
}
|
|
||||||
transactionTypeChanged(y, parent, this._parentSub)
|
|
||||||
y._transaction.deletedStructs.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_gcChildren (y) {}
|
|
||||||
|
|
||||||
_gc (y) {
|
|
||||||
const gc = new GC()
|
|
||||||
gc._id = this._id
|
|
||||||
gc._length = this._length
|
|
||||||
y.os.delete(this._id)
|
|
||||||
gc._integrate(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is called right before this Item receives any children.
|
|
||||||
* It can be overwritten to apply pending changes before applying remote changes
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_beforeChange () {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integrates this Item into the shared structure.
|
|
||||||
*
|
|
||||||
* This method actually applies the change to the Yjs instance. In case of
|
|
||||||
* Item it connects _left and _right to this Item and calls the
|
|
||||||
* {@link Item#beforeChange} method.
|
|
||||||
*
|
|
||||||
* * Integrate the struct so that other types/structs can see it
|
|
||||||
* * Add this struct to y.os
|
|
||||||
* * Check if this is struct deleted
|
|
||||||
*
|
|
||||||
* @param {Y} y
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_integrate (y) {
|
|
||||||
y._transaction.newTypes.add(this)
|
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
const parent = this._parent
|
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
const selfID = this._id
|
|
||||||
const user = selfID === null ? y.userID : selfID.user
|
|
||||||
const userState = y.ss.getState(user)
|
|
||||||
if (selfID === null) {
|
|
||||||
this._id = y.ss.getNextID(this._length)
|
|
||||||
} else if (selfID.user === ID.RootFakeUserID) {
|
|
||||||
// is parent
|
|
||||||
return
|
|
||||||
} else if (selfID.clock < userState) {
|
|
||||||
// already applied..
|
|
||||||
return
|
|
||||||
} else if (selfID.clock === userState) {
|
|
||||||
y.ss.setState(selfID.user, userState + this._length)
|
|
||||||
} else {
|
|
||||||
// missing content from user
|
|
||||||
throw new Error('Can not apply yet!')
|
|
||||||
}
|
|
||||||
if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
|
|
||||||
// this is the first time parent is updated
|
|
||||||
// or this types is new
|
|
||||||
parent._beforeChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
# $this has to find a unique position between origin and the next known character
|
|
||||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
|
||||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
|
||||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
|
||||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
|
||||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
|
||||||
# therefore $this would be always to the right of o3
|
|
||||||
# case 2: $origin < $o.origin
|
|
||||||
# if current $this insert_position > $o origin: $this ins
|
|
||||||
# else $insert_position will not change
|
|
||||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
|
||||||
# case 3: $origin > $o.origin
|
|
||||||
# $this insert_position is to the left of $o (forever!)
|
|
||||||
*/
|
|
||||||
// handle conflicts
|
|
||||||
let o
|
|
||||||
// set o to the first conflicting item
|
|
||||||
if (this._left !== null) {
|
|
||||||
o = this._left._right
|
|
||||||
} else if (this._parentSub !== null) {
|
|
||||||
o = parent._map.get(this._parentSub) || null
|
|
||||||
} else {
|
|
||||||
o = parent._start
|
|
||||||
}
|
|
||||||
let conflictingItems = new Set()
|
|
||||||
let itemsBeforeOrigin = new Set()
|
|
||||||
// Let c in conflictingItems, b in itemsBeforeOrigin
|
|
||||||
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
|
|
||||||
// Note that conflictingItems is a subset of itemsBeforeOrigin
|
|
||||||
while (o !== null && o !== this._right) {
|
|
||||||
itemsBeforeOrigin.add(o)
|
|
||||||
conflictingItems.add(o)
|
|
||||||
if (this._origin === o._origin) {
|
|
||||||
// case 1
|
|
||||||
if (o._id.user < this._id.user) {
|
|
||||||
this._left = o
|
|
||||||
conflictingItems.clear()
|
|
||||||
}
|
|
||||||
} else if (itemsBeforeOrigin.has(o._origin)) {
|
|
||||||
// case 2
|
|
||||||
if (!conflictingItems.has(o._origin)) {
|
|
||||||
this._left = o
|
|
||||||
conflictingItems.clear()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// TODO: try to use right_origin instead.
|
|
||||||
// Then you could basically omit conflictingItems!
|
|
||||||
// Note: you probably can't use right_origin in every case.. only when setting _left
|
|
||||||
o = o._right
|
|
||||||
}
|
|
||||||
// reconnect left/right + update parent map/start if necessary
|
|
||||||
const parentSub = this._parentSub
|
|
||||||
if (this._left === null) {
|
|
||||||
let right
|
|
||||||
if (parentSub !== null) {
|
|
||||||
const pmap = parent._map
|
|
||||||
right = pmap.get(parentSub) || null
|
|
||||||
pmap.set(parentSub, this)
|
|
||||||
} else {
|
|
||||||
right = parent._start
|
|
||||||
parent._start = this
|
|
||||||
}
|
|
||||||
this._right = right
|
|
||||||
if (right !== null) {
|
|
||||||
right._left = this
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const left = this._left
|
|
||||||
const right = left._right
|
|
||||||
this._right = right
|
|
||||||
left._right = this
|
|
||||||
if (right !== null) {
|
|
||||||
right._left = this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// adjust the length of parent
|
|
||||||
if (parentSub === null && parent.length !== undefined && this._countable) {
|
|
||||||
parent.length += this._length
|
|
||||||
}
|
|
||||||
if (parent._deleted) {
|
|
||||||
this._delete(y, false, true)
|
|
||||||
}
|
|
||||||
y.os.put(this)
|
|
||||||
transactionTypeChanged(y, parent, parentSub)
|
|
||||||
if (this._id.user !== ID.RootFakeUserID) {
|
|
||||||
writeStructToTransaction(y._transaction, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform the properties of this type to binary and write it to an
|
|
||||||
* BinaryEncoder.
|
|
||||||
*
|
|
||||||
* This is called when this Item is sent to a remote peer.
|
|
||||||
*
|
|
||||||
* @param {encoding.Encoder} encoder The encoder to write data to.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_toBinary (encoder) {
|
|
||||||
encoding.writeUint8(encoder, getStructReference(this.constructor))
|
|
||||||
let info = 0
|
|
||||||
if (this._origin !== null) {
|
|
||||||
info += 0b1 // origin is defined
|
|
||||||
}
|
|
||||||
// TODO: remove
|
|
||||||
/* no longer send _left
|
|
||||||
if (this._left !== this._origin) {
|
|
||||||
info += 0b10 // do not copy origin to left
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (this._right_origin !== null) {
|
|
||||||
info += 0b100
|
|
||||||
}
|
|
||||||
if (this._parentSub !== null) {
|
|
||||||
info += 0b1000
|
|
||||||
}
|
|
||||||
encoding.writeUint8(encoder, info)
|
|
||||||
this._id.encode(encoder)
|
|
||||||
if (info & 0b1) {
|
|
||||||
this._origin._lastId.encode(encoder)
|
|
||||||
}
|
|
||||||
// TODO: remove
|
|
||||||
/* see above
|
|
||||||
if (info & 0b10) {
|
|
||||||
encoder.writeID(this._left._lastId)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (info & 0b100) {
|
|
||||||
this._right_origin._id.encode(encoder)
|
|
||||||
}
|
|
||||||
if ((info & 0b101) === 0) {
|
|
||||||
// neither origin nor right is defined
|
|
||||||
this._parent._id.encode(encoder)
|
|
||||||
}
|
|
||||||
if (info & 0b1000) {
|
|
||||||
encoding.writeVarString(encoder, JSON.stringify(this._parentSub))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the next Item in a Decoder and fill this Item with the read data.
|
|
||||||
*
|
|
||||||
* This is called when data is received from a remote peer.
|
|
||||||
*
|
|
||||||
* @param {Y} y The Yjs instance that this Item belongs to.
|
|
||||||
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
let missing = []
|
|
||||||
const info = decoding.readUint8(decoder)
|
|
||||||
const id = ID.decode(decoder)
|
|
||||||
this._id = id
|
|
||||||
// read origin
|
|
||||||
if (info & 0b1) {
|
|
||||||
// origin != null
|
|
||||||
const originID = ID.decode(decoder)
|
|
||||||
// we have to query for left again because it might have been split/merged..
|
|
||||||
const origin = y.os.getItemCleanEnd(originID)
|
|
||||||
if (origin === null) {
|
|
||||||
missing.push(originID)
|
|
||||||
} else {
|
|
||||||
this._origin = origin
|
|
||||||
this._left = this._origin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// read right
|
|
||||||
if (info & 0b100) {
|
|
||||||
// right != null
|
|
||||||
const rightID = ID.decode(decoder)
|
|
||||||
// we have to query for right again because it might have been split/merged..
|
|
||||||
const right = y.os.getItemCleanStart(rightID)
|
|
||||||
if (right === null) {
|
|
||||||
missing.push(rightID)
|
|
||||||
} else {
|
|
||||||
this._right = right
|
|
||||||
this._right_origin = right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// read parent
|
|
||||||
if ((info & 0b101) === 0) {
|
|
||||||
// neither origin nor right is defined
|
|
||||||
const parentID = ID.decode(decoder)
|
|
||||||
// parent does not change, so we don't have to search for it again
|
|
||||||
if (this._parent === null) {
|
|
||||||
let parent
|
|
||||||
if (parentID.constructor === ID.RootID) {
|
|
||||||
parent = y.os.get(parentID)
|
|
||||||
} else {
|
|
||||||
parent = y.os.getItem(parentID)
|
|
||||||
}
|
|
||||||
if (parent === null) {
|
|
||||||
missing.push(parentID)
|
|
||||||
} else {
|
|
||||||
this._parent = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this._parent === null) {
|
|
||||||
if (this._origin !== null) {
|
|
||||||
this._parent = this._origin._parent
|
|
||||||
} else if (this._right_origin !== null) {
|
|
||||||
this._parent = this._right_origin._parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (info & 0b1000) {
|
|
||||||
// TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
|
|
||||||
this._parentSub = JSON.parse(decoding.readVarString(decoder))
|
|
||||||
}
|
|
||||||
if (id instanceof ID.ID && y.ss.getState(id.user) < id.clock) {
|
|
||||||
missing.push(ID.createID(id.user, id.clock - 1))
|
|
||||||
}
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,44 +4,83 @@
|
|||||||
|
|
||||||
// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency
|
// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency
|
||||||
|
|
||||||
import { Item, logItemHelper } from './Item.js'
|
import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js'
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import { ID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { ItemType } from './ItemType.js' // eslint-disable-line
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
|
||||||
|
|
||||||
export class ItemBinary extends Item {
|
export const structBinaryRefNumber = 1
|
||||||
constructor () {
|
|
||||||
super()
|
export class ItemBinary extends AbstractItem {
|
||||||
this._content = null
|
|
||||||
}
|
|
||||||
_copy () {
|
|
||||||
let struct = super._copy()
|
|
||||||
struct._content = this._content
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* @param {Y} y
|
* @param {ID} id
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @param {ArrayBuffer} content
|
||||||
*/
|
*/
|
||||||
_fromBinary (y, decoder) {
|
constructor (id, left, right, parent, parentSub, content) {
|
||||||
const missing = super._fromBinary(y, decoder)
|
super(id, left, right, parent, parentSub)
|
||||||
this._content = decoding.readPayload(decoder)
|
this.content = content
|
||||||
return missing
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param {encoding.Encoder} encoder
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
*/
|
*/
|
||||||
_toBinary (encoder) {
|
copy (id, left, right, parent, parentSub) {
|
||||||
super._toBinary(encoder)
|
return new ItemBinary(id, left, right, parent, parentSub, this.content)
|
||||||
encoding.writePayload(encoder, this._content)
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Transform this YXml Type to a readable format.
|
* Transform this Type to a readable format.
|
||||||
* Useful for logging as all Items and Delete implement this method.
|
* Useful for logging as all Items and Delete implement this method.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
logString () {
|
||||||
return logItemHelper('ItemBinary', this)
|
return logItemHelper('ItemBinary', this)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
write (encoder) {
|
||||||
|
super.write(encoder, structBinaryRefNumber)
|
||||||
|
encoding.writePayload(encoder, this.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemBinaryRef extends AbstractItemRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, info) {
|
||||||
|
super(decoder, info)
|
||||||
|
/**
|
||||||
|
* @type {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
this.content = decoding.readPayload(decoder)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @return {ItemBinary}
|
||||||
|
*/
|
||||||
|
toStruct (transaction) {
|
||||||
|
const store = transaction.y.store
|
||||||
|
return new ItemBinary(
|
||||||
|
this.id,
|
||||||
|
this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
|
||||||
|
this.right === null ? null : getItemCleanStart(store, transaction, this.right),
|
||||||
|
this.parent === null ? null : getItemType(store, this.parent),
|
||||||
|
this.parentSub,
|
||||||
|
this.content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
83
src/structs/ItemDeleted.js
Normal file
83
src/structs/ItemDeleted.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* @module structs
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency
|
||||||
|
|
||||||
|
import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import { ID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { ItemType } from './ItemType.js' // eslint-disable-line
|
||||||
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
|
||||||
|
export const structDeletedRefNumber = 2
|
||||||
|
|
||||||
|
export class ItemBinary extends AbstractItem {
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @param {ArrayBuffer} content
|
||||||
|
*/
|
||||||
|
constructor (id, left, right, parent, parentSub, content) {
|
||||||
|
super(id, left, right, parent, parentSub)
|
||||||
|
this.content = content
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
*/
|
||||||
|
copy (id, left, right, parent, parentSub) {
|
||||||
|
return new ItemBinary(id, left, right, parent, parentSub, this.content)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Transform this Type to a readable format.
|
||||||
|
* Useful for logging as all Items and Delete implement this method.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
logString () {
|
||||||
|
return logItemHelper('ItemBinary', this)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
write (encoder) {
|
||||||
|
super.write(encoder, structDeletedRefNumber)
|
||||||
|
encoding.writePayload(encoder, this.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemDeletedRef extends AbstractItemRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, info) {
|
||||||
|
super(decoder, info)
|
||||||
|
/**
|
||||||
|
* @type {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
this.content = decoding.readPayload(decoder)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Y} y
|
||||||
|
* @return {ItemBinary}
|
||||||
|
*/
|
||||||
|
toStruct (y) {
|
||||||
|
return new ItemBinary(
|
||||||
|
this.id,
|
||||||
|
this.left === null ? null : y.os.getItemCleanEnd(this.left),
|
||||||
|
this.right === null ? null : y.os.getItemCleanStart(this.right),
|
||||||
|
this.parent === null ? null : y.os.getItem(this.parent),
|
||||||
|
this.parentSub,
|
||||||
|
this.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -2,47 +2,90 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item, logItemHelper } from './Item.js'
|
import { AbstractItem, AbstractItemRef, logItemHelper } from './AbstractItem.js'
|
||||||
|
import { ItemType } from './ItemType.js' // eslint-disable-line
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { ID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
export class ItemEmbed extends Item {
|
export const structEmbedRefNumber = 3
|
||||||
constructor () {
|
|
||||||
super()
|
export class ItemEmbed extends AbstractItem {
|
||||||
this.embed = null
|
|
||||||
}
|
|
||||||
_copy (undeleteChildren, copyPosition) {
|
|
||||||
let struct = super._copy()
|
|
||||||
struct.embed = this.embed
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* @param {Y} y
|
* @param {ID} id
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @param {Object} embed
|
||||||
*/
|
*/
|
||||||
_fromBinary (y, decoder) {
|
constructor (id, left, right, parent, parentSub, embed) {
|
||||||
const missing = super._fromBinary(y, decoder)
|
super(id, left, right, parent, parentSub)
|
||||||
this.embed = JSON.parse(decoding.readVarString(decoder))
|
this.embed = embed
|
||||||
return missing
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param {encoding.Encoder} encoder
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
*/
|
*/
|
||||||
_toBinary (encoder) {
|
copy (id, left, right, parent, parentSub) {
|
||||||
super._toBinary(encoder)
|
return new ItemEmbed(id, left, right, parent, parentSub, this.embed)
|
||||||
encoding.writeVarString(encoder, JSON.stringify(this.embed))
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Transform this YXml Type to a readable format.
|
* Transform this Type to a readable format.
|
||||||
* Useful for logging as all Items and Delete implement this method.
|
* Useful for logging as all Items and Delete implement this method.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
logString () {
|
||||||
return logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`)
|
return logItemHelper('ItemEmbed', this)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
get _length () {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
write (encoder) {
|
||||||
|
super.write(encoder, structEmbedRefNumber)
|
||||||
|
encoding.writeVarString(encoder, JSON.stringify(this.embed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemEmbedRef extends AbstractItemRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, info) {
|
||||||
|
super(decoder, info)
|
||||||
|
/**
|
||||||
|
* @type {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
this.embed = JSON.parse(decoding.readVarString(decoder))
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @return {ItemEmbed}
|
||||||
|
*/
|
||||||
|
toStruct (transaction) {
|
||||||
|
const store = transaction.y.store
|
||||||
|
return new ItemEmbed(
|
||||||
|
this.id,
|
||||||
|
this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
|
||||||
|
this.right === null ? null : getItemCleanStart(store, transaction, this.right),
|
||||||
|
this.parent === null ? null : getItemType(store, this.parent),
|
||||||
|
this.parentSub,
|
||||||
|
this.embed
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,50 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item, logItemHelper } from './Item.js'
|
import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js'
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { ID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { ItemType } from './ItemType.js' // eslint-disable-line
|
||||||
|
import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
export class ItemFormat extends Item {
|
export const structFormatRefNumber = 4
|
||||||
constructor () {
|
|
||||||
super()
|
export class ItemFormat extends AbstractItem {
|
||||||
this.key = null
|
/**
|
||||||
this.value = null
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @param {string} key
|
||||||
|
* @param {any} value
|
||||||
|
*/
|
||||||
|
constructor (id, left, right, parent, parentSub, key, value) {
|
||||||
|
super(id, left, right, parent, parentSub)
|
||||||
|
this.key = key
|
||||||
|
this.value = value
|
||||||
}
|
}
|
||||||
_copy (undeleteChildren, copyPosition) {
|
/**
|
||||||
let struct = super._copy()
|
* @param {ID} id
|
||||||
struct.key = this.key
|
* @param {AbstractItem | null} left
|
||||||
struct.value = this.value
|
* @param {AbstractItem | null} right
|
||||||
return struct
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
*/
|
||||||
|
copy (id, left, right, parent, parentSub) {
|
||||||
|
return new ItemFormat(id, left, right, parent, parentSub, this.key, this.value)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Transform this Type to a readable format.
|
||||||
|
* Useful for logging as all Items and Delete implement this method.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
logString () {
|
||||||
|
return logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`)
|
||||||
}
|
}
|
||||||
get _length () {
|
get _length () {
|
||||||
return 1
|
return 1
|
||||||
@ -25,31 +53,43 @@ export class ItemFormat extends Item {
|
|||||||
get _countable () {
|
get _countable () {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {Y} y
|
|
||||||
* @param {decoding.Decoder} decoder
|
|
||||||
*/
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
const missing = super._fromBinary(y, decoder)
|
|
||||||
this.key = decoding.readVarString(decoder)
|
|
||||||
this.value = JSON.parse(decoding.readVarString(decoder))
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* @param {encoding.Encoder} encoder
|
* @param {encoding.Encoder} encoder
|
||||||
*/
|
*/
|
||||||
_toBinary (encoder) {
|
write (encoder) {
|
||||||
super._toBinary(encoder)
|
super.write(encoder, structFormatRefNumber)
|
||||||
encoding.writeVarString(encoder, this.key)
|
encoding.writeVarString(encoder, this.key)
|
||||||
encoding.writeVarString(encoder, JSON.stringify(this.value))
|
encoding.writeVarString(encoder, JSON.stringify(this.value))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemFormatRef extends AbstractItemRef {
|
||||||
/**
|
/**
|
||||||
* Transform this YXml Type to a readable format.
|
* @param {decoding.Decoder} decoder
|
||||||
* Useful for logging as all Items and Delete implement this method.
|
* @param {number} info
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
_logString () {
|
constructor (decoder, info) {
|
||||||
return stringify.logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`)
|
super(decoder, info)
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.key = decoding.readVarString(decoder)
|
||||||
|
this.value = JSON.parse(decoding.readVarString(decoder))
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @return {ItemFormat}
|
||||||
|
*/
|
||||||
|
toStruct (transaction) {
|
||||||
|
const store = transaction.y.store
|
||||||
|
return new ItemFormat(
|
||||||
|
this.id,
|
||||||
|
this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
|
||||||
|
this.right === null ? null : getItemCleanStart(store, transaction, this.right),
|
||||||
|
this.parent === null ? null : getItemType(store, this.parent),
|
||||||
|
this.parentSub,
|
||||||
|
this.key,
|
||||||
|
this.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,81 +2,109 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item, splitHelper, logItemHelper } from './Item.js'
|
import { AbstractItem, logItemHelper, AbstractItemRef, splitItem } from './AbstractItem.js'
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { ID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { ItemType } from './ItemType.js' // eslint-disable-line
|
||||||
|
import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
export class ItemJSON extends Item {
|
export const structJSONRefNumber = 5
|
||||||
constructor () {
|
|
||||||
super()
|
export class ItemJSON extends AbstractItem {
|
||||||
this._content = null
|
|
||||||
}
|
|
||||||
_copy () {
|
|
||||||
let struct = super._copy()
|
|
||||||
struct._content = this._content
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
const c = this._content
|
|
||||||
return c !== null ? c.length : 0
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* @param {Y} y
|
* @param {ID} id
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @param {Array<any>} content
|
||||||
*/
|
*/
|
||||||
_fromBinary (y, decoder) {
|
constructor (id, left, right, parent, parentSub, content) {
|
||||||
let missing = super._fromBinary(y, decoder)
|
super(id, left, right, parent, parentSub)
|
||||||
let len = decoding.readVarUint(decoder)
|
this.content = content
|
||||||
this._content = new Array(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const ctnt = decoding.readVarString(decoder)
|
|
||||||
let parsed
|
|
||||||
if (ctnt === 'undefined') {
|
|
||||||
parsed = undefined
|
|
||||||
} else {
|
|
||||||
parsed = JSON.parse(ctnt)
|
|
||||||
}
|
|
||||||
this._content[i] = parsed
|
|
||||||
}
|
|
||||||
return missing
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param {encoding.Encoder} encoder
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
*/
|
*/
|
||||||
_toBinary (encoder) {
|
copy (id, left, right, parent, parentSub) {
|
||||||
super._toBinary(encoder)
|
return new ItemJSON(id, left, right, parent, parentSub, this.content)
|
||||||
const len = this._length
|
|
||||||
encoding.writeVarUint(encoder, len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let encoded
|
|
||||||
const content = this._content[i]
|
|
||||||
if (content === undefined) {
|
|
||||||
encoded = 'undefined'
|
|
||||||
} else {
|
|
||||||
encoded = JSON.stringify(content)
|
|
||||||
}
|
|
||||||
encoding.writeVarString(encoder, encoded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Transform this YXml Type to a readable format.
|
* Transform this Type to a readable format.
|
||||||
* Useful for logging as all Items and Delete implement this method.
|
* Useful for logging as all Items and Delete implement this method.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
logString () {
|
||||||
return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
|
return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this.content)}`)
|
||||||
}
|
}
|
||||||
_splitAt (y, diff) {
|
get length () {
|
||||||
if (diff === 0) {
|
return this.content.length
|
||||||
return this
|
|
||||||
} else if (diff >= this._length) {
|
|
||||||
return this._right
|
|
||||||
}
|
}
|
||||||
let item = new ItemJSON()
|
/**
|
||||||
item._content = this._content.splice(diff)
|
* @param {number} diff
|
||||||
splitHelper(y, this, item, diff)
|
*/
|
||||||
return item
|
splitAt (diff) {
|
||||||
|
/**
|
||||||
|
* @type {ItemJSON}
|
||||||
|
*/
|
||||||
|
const right = splitItem(this, diff)
|
||||||
|
right.content = this.content.splice(diff)
|
||||||
|
return right
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
write (encoder) {
|
||||||
|
super.write(encoder, structJSONRefNumber)
|
||||||
|
const len = this.content.length
|
||||||
|
encoding.writeVarUint(encoder, len)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const c = this.content[i]
|
||||||
|
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemJSONRef extends AbstractItemRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, info) {
|
||||||
|
super(decoder, info)
|
||||||
|
const len = decoding.readVarUint(decoder)
|
||||||
|
const cs = []
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const c = decoding.readVarString(decoder)
|
||||||
|
if (c === 'undefined') {
|
||||||
|
cs.push(undefined)
|
||||||
|
} else {
|
||||||
|
cs.push(JSON.parse(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.content = cs
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @return {ItemJSON}
|
||||||
|
*/
|
||||||
|
toStruct (transaction) {
|
||||||
|
const store = transaction.y.store
|
||||||
|
return new ItemJSON(
|
||||||
|
this.id,
|
||||||
|
this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
|
||||||
|
this.right === null ? null : getItemCleanStart(store, transaction, this.right),
|
||||||
|
this.parent === null ? null : getItemType(store, this.parent),
|
||||||
|
this.parentSub,
|
||||||
|
this.content
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,59 +2,100 @@
|
|||||||
* @module structs
|
* @module structs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item, splitHelper, logItemHelper } from './Item.js'
|
import { AbstractItem, logItemHelper, AbstractItemRef, splitItem } from './AbstractItem.js'
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { ID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { ItemType } from './ItemType.js' // eslint-disable-line
|
||||||
|
import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
export class ItemString extends Item {
|
export const structStringRefNumber = 6
|
||||||
constructor () {
|
|
||||||
super()
|
export class ItemString extends AbstractItem {
|
||||||
this._content = null
|
|
||||||
}
|
|
||||||
_copy () {
|
|
||||||
let struct = super._copy()
|
|
||||||
struct._content = this._content
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
return this._content.length
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* @param {Y} y
|
* @param {ID} id
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @param {string} string
|
||||||
*/
|
*/
|
||||||
_fromBinary (y, decoder) {
|
constructor (id, left, right, parent, parentSub, string) {
|
||||||
let missing = super._fromBinary(y, decoder)
|
super(id, left, right, parent, parentSub)
|
||||||
this._content = decoding.readVarString(decoder)
|
this.string = string
|
||||||
return missing
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param {encoding.Encoder} encoder
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
*/
|
*/
|
||||||
_toBinary (encoder) {
|
copy (id, left, right, parent, parentSub) {
|
||||||
super._toBinary(encoder)
|
return new ItemString(id, left, right, parent, parentSub, this.string)
|
||||||
encoding.writeVarString(encoder, this._content)
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Transform this YXml Type to a readable format.
|
* Transform this Type to a readable format.
|
||||||
* Useful for logging as all Items and Delete implement this method.
|
* Useful for logging as all Items and Delete implement this method.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_logString () {
|
logString () {
|
||||||
return logItemHelper('ItemString', this, `content:"${this._content}"`)
|
return logItemHelper('ItemString', this, `content:"${this.string}"`)
|
||||||
}
|
}
|
||||||
_splitAt (y, diff) {
|
get length () {
|
||||||
|
return this.string.length
|
||||||
|
}
|
||||||
|
splitAt (y, diff) {
|
||||||
if (diff === 0) {
|
if (diff === 0) {
|
||||||
return this
|
return this
|
||||||
} else if (diff >= this._length) {
|
} else if (diff >= this.string.length) {
|
||||||
return this._right
|
return this.right
|
||||||
}
|
}
|
||||||
let item = new ItemString()
|
/**
|
||||||
item._content = this._content.slice(diff)
|
* @type {ItemString}
|
||||||
this._content = this._content.slice(0, diff)
|
*/
|
||||||
splitHelper(y, this, item, diff)
|
const right = splitItem(this, y, diff)
|
||||||
return item
|
right.string = this.string.slice(diff)
|
||||||
|
right.string = this.string.slice(0, diff)
|
||||||
|
return right
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
write (encoder) {
|
||||||
|
super.write(encoder, structStringRefNumber)
|
||||||
|
encoding.writeVarString(encoder, this.string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemStringRef extends AbstractItemRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, info) {
|
||||||
|
super(decoder, info)
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.string = decoding.readVarString(decoder)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @return {ItemString}
|
||||||
|
*/
|
||||||
|
toStruct (transaction) {
|
||||||
|
const store = transaction.y.store
|
||||||
|
return new ItemString(
|
||||||
|
this.id,
|
||||||
|
this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
|
||||||
|
this.right === null ? null : getItemCleanStart(store, transaction, this.right),
|
||||||
|
this.parent === null ? null : getItemType(store, this.parent),
|
||||||
|
this.parentSub,
|
||||||
|
this.string
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
169
src/structs/ItemType.js
Normal file
169
src/structs/ItemType.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* @module structs
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency
|
||||||
|
|
||||||
|
import { ID } from '../utils/ID.js' // eslint-disable-line
|
||||||
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
|
||||||
|
import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import { readYArray } from '../types/YArray.js'
|
||||||
|
import { readYMap } from '../types/YMap.js'
|
||||||
|
import { readYText } from '../types/YText.js'
|
||||||
|
import { readYXmlElement, readYXmlFragment } from '../types/YXmlElement.js'
|
||||||
|
import { readYXmlHook } from '../types/YXmlHook.js'
|
||||||
|
import { readYXmlText } from '../types/YXmlText.js'
|
||||||
|
import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
|
||||||
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
|
|
||||||
|
const gcChildren = (y, item) => {
|
||||||
|
while (item !== null) {
|
||||||
|
item._delete(y, false, true)
|
||||||
|
item._gc(y)
|
||||||
|
item = item._right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const structTypeRefNumber = 7
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<function(decoding.Decoder):AbstractType>}
|
||||||
|
*/
|
||||||
|
export const typeRefs = [
|
||||||
|
readYArray,
|
||||||
|
readYMap,
|
||||||
|
readYText,
|
||||||
|
readYXmlElement,
|
||||||
|
readYXmlFragment,
|
||||||
|
readYXmlHook,
|
||||||
|
readYXmlText
|
||||||
|
]
|
||||||
|
|
||||||
|
export class ItemType extends AbstractItem {
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @param {AbstractType} type
|
||||||
|
*/
|
||||||
|
constructor (id, left, right, parent, parentSub, type) {
|
||||||
|
super(id, left, right, parent, parentSub)
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {AbstractItem | null} left
|
||||||
|
* @param {AbstractItem | null} right
|
||||||
|
* @param {ItemType | null} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @return {AbstractItem} TODO, returns itemtype
|
||||||
|
*/
|
||||||
|
copy (id, left, right, parent, parentSub) {
|
||||||
|
return new ItemType(id, left, right, parent, parentSub, this.type._copy())
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Transform this Type to a readable format.
|
||||||
|
* Useful for logging as all Items and Delete implement this method.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
logString () {
|
||||||
|
return logItemHelper('ItemType', this)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
write (encoder) {
|
||||||
|
super.write(encoder, structTypeRefNumber)
|
||||||
|
this.type._write(encoder)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Mark this Item as deleted.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction The Yjs instance
|
||||||
|
* @param {boolean} createDelete Whether to propagate a message that this
|
||||||
|
* Type was deleted.
|
||||||
|
* @param {boolean} [gcChildren=(y._hasUndoManager===false)] Whether to garbage
|
||||||
|
* collect the children of this type.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
delete (transaction, createDelete, gcChildren = transaction.y.gcEnabled) {
|
||||||
|
const y = transaction.y
|
||||||
|
super.delete(transaction, createDelete, gcChildren)
|
||||||
|
transaction.changed.delete(this)
|
||||||
|
// delete map types
|
||||||
|
for (let value of this.type._map.values()) {
|
||||||
|
if (!value._deleted) {
|
||||||
|
value._delete(y, false, gcChildren)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// delete array types
|
||||||
|
let t = this.type._start
|
||||||
|
while (t !== null) {
|
||||||
|
if (!t._deleted) {
|
||||||
|
t._delete(y, false, gcChildren)
|
||||||
|
}
|
||||||
|
t = t._right
|
||||||
|
}
|
||||||
|
if (gcChildren) {
|
||||||
|
this.gcChildren(y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Y} y
|
||||||
|
*/
|
||||||
|
gcChildren (y) {
|
||||||
|
gcChildren(y, this.type._start)
|
||||||
|
this.type._start = null
|
||||||
|
this.type._map.forEach(item => {
|
||||||
|
gcChildren(y, item)
|
||||||
|
})
|
||||||
|
this._map = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Y} y
|
||||||
|
*/
|
||||||
|
gc (y) {
|
||||||
|
this.gcChildren(y)
|
||||||
|
super.gc(y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemBinaryRef extends AbstractItemRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, info) {
|
||||||
|
super(decoder, info)
|
||||||
|
const typeRef = decoding.readVarUint(decoder)
|
||||||
|
/**
|
||||||
|
* @type {AbstractType}
|
||||||
|
*/
|
||||||
|
this.type = typeRefs[typeRef](decoder)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @return {ItemType}
|
||||||
|
*/
|
||||||
|
toStruct (transaction) {
|
||||||
|
const store = transaction.y.store
|
||||||
|
return new ItemType(
|
||||||
|
this.id,
|
||||||
|
this.left === null ? null : getItemCleanEnd(store, transaction, this.left),
|
||||||
|
this.right === null ? null : getItemCleanStart(store, transaction, this.right),
|
||||||
|
this.parent === null ? null : getItemType(store, this.parent),
|
||||||
|
this.parentSub,
|
||||||
|
this.type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,277 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module structs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Item } from './Item.js'
|
|
||||||
import { EventHandler } from '../utils/EventHandler.js'
|
|
||||||
import { createID } from '../utils/ID.js'
|
|
||||||
import { YEvent } from '../utils/YEvent.js'
|
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
|
||||||
|
|
||||||
// restructure children as if they were inserted one after another
|
|
||||||
const integrateChildren = (y, start) => {
|
|
||||||
let right
|
|
||||||
do {
|
|
||||||
right = start._right
|
|
||||||
start._right = null
|
|
||||||
start._right_origin = null
|
|
||||||
start._origin = start._left
|
|
||||||
start._integrate(y)
|
|
||||||
start = right
|
|
||||||
} while (right !== null)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getListItemIDByPosition = (type, i) => {
|
|
||||||
let pos = 0
|
|
||||||
let n = type._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
if (pos <= i && i < pos + n._length) {
|
|
||||||
const id = n._id
|
|
||||||
return createID(id.user, id.clock + i - pos)
|
|
||||||
}
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const gcChildren = (y, item) => {
|
|
||||||
while (item !== null) {
|
|
||||||
item._delete(y, false, true)
|
|
||||||
item._gc(y)
|
|
||||||
item = item._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract Yjs Type class
|
|
||||||
*/
|
|
||||||
export class Type extends Item {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._map = new Map()
|
|
||||||
this._start = null
|
|
||||||
this._y = null
|
|
||||||
this._eventHandler = new EventHandler()
|
|
||||||
this._deepEventHandler = new EventHandler()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The first non-deleted item
|
|
||||||
*/
|
|
||||||
get _first () {
|
|
||||||
let n = this._start
|
|
||||||
while (n !== null && n._deleted) {
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the path from this type to the specified target.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* It should be accessible via `this.get(result[0]).get(result[1])..`
|
|
||||||
* const path = type.getPathTo(child)
|
|
||||||
* // assuming `type instanceof YArray`
|
|
||||||
* console.log(path) // might look like => [2, 'key1']
|
|
||||||
* child === type.get(path[0]).get(path[1])
|
|
||||||
*
|
|
||||||
* @param {Type | Y | any} type Type target
|
|
||||||
* @return {Array<string>} Path to the target
|
|
||||||
*/
|
|
||||||
getPathTo (type) {
|
|
||||||
if (type === this) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const path = []
|
|
||||||
const y = this._y
|
|
||||||
while (type !== this && type !== y) {
|
|
||||||
let parent = type._parent
|
|
||||||
if (type._parentSub !== null) {
|
|
||||||
path.unshift(type._parentSub)
|
|
||||||
} else {
|
|
||||||
// parent is array-ish
|
|
||||||
for (let [i, child] of parent) {
|
|
||||||
if (child === type) {
|
|
||||||
path.unshift(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type = parent
|
|
||||||
}
|
|
||||||
if (type !== this) {
|
|
||||||
throw new Error('The type is not a child of this node')
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates YArray Event and calls observers.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
|
||||||
this._callEventHandler(transaction, new YEvent(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call event listeners with an event. This will also add an event to all
|
|
||||||
* parents (for `.observeDeep` handlers).
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_callEventHandler (transaction, event) {
|
|
||||||
const changedParentTypes = transaction.changedParentTypes
|
|
||||||
this._eventHandler.callEventListeners(transaction, event)
|
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
let type = this
|
|
||||||
while (type !== this._y) {
|
|
||||||
let events = changedParentTypes.get(type)
|
|
||||||
if (events === undefined) {
|
|
||||||
events = []
|
|
||||||
changedParentTypes.set(type, events)
|
|
||||||
}
|
|
||||||
events.push(event)
|
|
||||||
type = type._parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to transact if the y instance is available.
|
|
||||||
*
|
|
||||||
* TODO: Currently event handlers are not thrown when a type is not registered
|
|
||||||
* with a Yjs instance.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_transact (f) {
|
|
||||||
const y = this._y
|
|
||||||
if (y !== null) {
|
|
||||||
y.transact(f)
|
|
||||||
} else {
|
|
||||||
f(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observe all events that are created on this type.
|
|
||||||
*
|
|
||||||
* @param {Function} f Observer function
|
|
||||||
*/
|
|
||||||
observe (f) {
|
|
||||||
this._eventHandler.addEventListener(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observe all events that are created by this type and its children.
|
|
||||||
*
|
|
||||||
* @param {Function} f Observer function
|
|
||||||
*/
|
|
||||||
observeDeep (f) {
|
|
||||||
this._deepEventHandler.addEventListener(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregister an observer function.
|
|
||||||
*
|
|
||||||
* @param {Function} f Observer function
|
|
||||||
*/
|
|
||||||
unobserve (f) {
|
|
||||||
this._eventHandler.removeEventListener(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregister an observer function.
|
|
||||||
*
|
|
||||||
* @param {Function} f Observer function
|
|
||||||
*/
|
|
||||||
unobserveDeep (f) {
|
|
||||||
this._deepEventHandler.removeEventListener(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {Y} y The Yjs instance
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_integrate (y) {
|
|
||||||
super._integrate(y)
|
|
||||||
this._y = y
|
|
||||||
// when integrating children we must make sure to
|
|
||||||
// integrate start
|
|
||||||
const start = this._start
|
|
||||||
if (start !== null) {
|
|
||||||
this._start = null
|
|
||||||
integrateChildren(y, start)
|
|
||||||
}
|
|
||||||
// integrate map children_integrate
|
|
||||||
const map = this._map
|
|
||||||
this._map = new Map()
|
|
||||||
for (let t of map.values()) {
|
|
||||||
// TODO make sure that right elements are deleted!
|
|
||||||
integrateChildren(y, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_gcChildren (y) {
|
|
||||||
gcChildren(y, this._start)
|
|
||||||
this._start = null
|
|
||||||
this._map.forEach(item => {
|
|
||||||
gcChildren(y, item)
|
|
||||||
})
|
|
||||||
this._map = new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
_gc (y) {
|
|
||||||
this._gcChildren(y)
|
|
||||||
super._gc(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @abstract
|
|
||||||
* @return {Object | Array | number | string}
|
|
||||||
*/
|
|
||||||
toJSON () {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark this Item as deleted.
|
|
||||||
*
|
|
||||||
* @param {Y} y The Yjs instance
|
|
||||||
* @param {boolean} createDelete Whether to propagate a message that this
|
|
||||||
* Type was deleted.
|
|
||||||
* @param {boolean} [gcChildren=(y._hasUndoManager===false)] Whether to garbage
|
|
||||||
* collect the children of this type.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_delete (y, createDelete, gcChildren) {
|
|
||||||
if (gcChildren === undefined || !y.gcEnabled) {
|
|
||||||
gcChildren = y._hasUndoManager === false && y.gcEnabled
|
|
||||||
}
|
|
||||||
super._delete(y, createDelete, gcChildren)
|
|
||||||
y._transaction.changedTypes.delete(this)
|
|
||||||
// delete map types
|
|
||||||
for (let value of this._map.values()) {
|
|
||||||
if (value instanceof Item && !value._deleted) {
|
|
||||||
value._delete(y, false, gcChildren)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// delete array types
|
|
||||||
let t = this._start
|
|
||||||
while (t !== null) {
|
|
||||||
if (!t._deleted) {
|
|
||||||
t._delete(y, false, gcChildren)
|
|
||||||
}
|
|
||||||
t = t._right
|
|
||||||
}
|
|
||||||
if (gcChildren) {
|
|
||||||
this._gcChildren(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
237
src/types/AbstractType.js
Normal file
237
src/types/AbstractType.js
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* @module structs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
|
import { EventHandler } from '../utils/EventHandler.js'
|
||||||
|
import { YEvent } from '../utils/YEvent.js'
|
||||||
|
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
|
||||||
|
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
|
||||||
|
import { Encoder } from 'lib0/encoding.js' // eslint-disable-line
|
||||||
|
import { Transaction, nextID } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restructure children as if they were inserted one after another
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractItem} start
|
||||||
|
*/
|
||||||
|
const integrateChildren = (transaction, start) => {
|
||||||
|
let right
|
||||||
|
while (true) {
|
||||||
|
right = start.right
|
||||||
|
start.id = nextID(transaction)
|
||||||
|
start.right = null
|
||||||
|
start.rightOrigin = null
|
||||||
|
start.origin = start.left
|
||||||
|
start.integrate(transaction)
|
||||||
|
if (right !== null) {
|
||||||
|
start = right
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Yjs Type class
|
||||||
|
*/
|
||||||
|
export class AbstractType {
|
||||||
|
constructor () {
|
||||||
|
/**
|
||||||
|
* @type {ItemType|null}
|
||||||
|
*/
|
||||||
|
this._item = null
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {Map<string,AbstractItem>}
|
||||||
|
*/
|
||||||
|
this._map = new Map()
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {AbstractItem|null}
|
||||||
|
*/
|
||||||
|
this._start = null
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {Y|null}
|
||||||
|
*/
|
||||||
|
this._y = null
|
||||||
|
this._length = 0
|
||||||
|
this._eventHandler = new EventHandler()
|
||||||
|
this._deepEventHandler = new EventHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {Transaction} transaction The Yjs instance
|
||||||
|
* @param {ItemType} item
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_integrate (transaction, item) {
|
||||||
|
this._y = transaction.y
|
||||||
|
this._item = item
|
||||||
|
// when integrating children we must make sure to
|
||||||
|
// integrate start
|
||||||
|
const start = this._start
|
||||||
|
if (start !== null) {
|
||||||
|
this._start = null
|
||||||
|
integrateChildren(transaction, start)
|
||||||
|
}
|
||||||
|
// integrate map children_integrate
|
||||||
|
const map = this._map
|
||||||
|
this._map = new Map()
|
||||||
|
map.forEach(t => {
|
||||||
|
t.right = null
|
||||||
|
t.rightOrigin = null
|
||||||
|
integrateChildren(transaction, t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {AbstractType}
|
||||||
|
*/
|
||||||
|
_copy () {
|
||||||
|
throw new Error('unimplemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Encoder} encoder
|
||||||
|
*/
|
||||||
|
_write (encoder) {
|
||||||
|
throw new Error('unimplemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first non-deleted item
|
||||||
|
*/
|
||||||
|
get _first () {
|
||||||
|
let n = this._start
|
||||||
|
while (n !== null && n.deleted) {
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates YArray Event and calls observers.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_callObserver (transaction, parentSubs, remote) {
|
||||||
|
this._callEventHandler(transaction, new YEvent(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call event listeners with an event. This will also add an event to all
|
||||||
|
* parents (for `.observeDeep` handlers).
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_callEventHandler (transaction, event) {
|
||||||
|
const changedParentTypes = transaction.changedParentTypes
|
||||||
|
this._eventHandler.callEventListeners(transaction, event)
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let type = this
|
||||||
|
while (type !== this._y) {
|
||||||
|
let events = changedParentTypes.get(type)
|
||||||
|
if (events === undefined) {
|
||||||
|
events = []
|
||||||
|
changedParentTypes.set(type, events)
|
||||||
|
}
|
||||||
|
events.push(event)
|
||||||
|
type = type._parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to transact if the y instance is available.
|
||||||
|
*
|
||||||
|
* TODO: Currently event handlers are not thrown when a type is not registered
|
||||||
|
* with a Yjs instance.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_transact (f) {
|
||||||
|
const y = this._y
|
||||||
|
if (y !== null) {
|
||||||
|
y.transact(f)
|
||||||
|
} else {
|
||||||
|
f(y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe all events that are created on this type.
|
||||||
|
*
|
||||||
|
* @param {Function} f Observer function
|
||||||
|
*/
|
||||||
|
observe (f) {
|
||||||
|
this._eventHandler.addEventListener(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe all events that are created by this type and its children.
|
||||||
|
*
|
||||||
|
* @param {Function} f Observer function
|
||||||
|
*/
|
||||||
|
observeDeep (f) {
|
||||||
|
this._deepEventHandler.addEventListener(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an observer function.
|
||||||
|
*
|
||||||
|
* @param {Function} f Observer function
|
||||||
|
*/
|
||||||
|
unobserve (f) {
|
||||||
|
this._eventHandler.removeEventListener(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an observer function.
|
||||||
|
*
|
||||||
|
* @param {Function} f Observer function
|
||||||
|
*/
|
||||||
|
unobserveDeep (f) {
|
||||||
|
this._deepEventHandler.removeEventListener(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
* @return {Object | Array | number | string}
|
||||||
|
*/
|
||||||
|
toJSON () {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType} type
|
||||||
|
* @return {Array<any>}
|
||||||
|
*/
|
||||||
|
export const typeToArray = type => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
|
*
|
||||||
|
* @param {AbstractType} type
|
||||||
|
* @param {function(any,number,AbstractType):void} f A function to execute on every element of this YArray.
|
||||||
|
* @param {HistorySnapshot} [snapshot]
|
||||||
|
*/
|
||||||
|
export const typeForEach = (type, f, snapshot) => {
|
||||||
|
let index = 0
|
||||||
|
let n = type._start
|
||||||
|
while (n !== null) {
|
||||||
|
if (isVisible(n, snapshot) && n._countable) {
|
||||||
|
const c = n.getContent()
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
f(c[i], index++, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = n._right
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,14 @@
|
|||||||
* @module types
|
* @module types
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Type } from '../structs/Type.js'
|
import { AbstractType } from './AbstractType.js'
|
||||||
import { ItemJSON } from '../structs/ItemJSON.js'
|
import { ItemJSON } from '../structs/ItemJSON.js'
|
||||||
import { ItemString } from '../structs/ItemString.js'
|
import { ItemString } from '../structs/ItemString.js'
|
||||||
|
import { ItemBinary } from '../structs/ItemBinary.js'
|
||||||
|
import { stringifyItemID, logItemHelper } from '../structs/AbstractItem.js' // eslint-disable-line
|
||||||
import { YEvent } from '../utils/YEvent.js'
|
import { YEvent } from '../utils/YEvent.js'
|
||||||
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
import { Item, stringifyItemID, logItemHelper } from '../structs/Item.js' // eslint-disable-line
|
import { isVisible, HistorySnapshot } from '../utils/snapshot.js' // eslint-disable-line
|
||||||
import { ItemBinary } from '../structs/ItemBinary.js'
|
|
||||||
import { isVisible } from '../utils/snapshot.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event that describes the changes on a YArray
|
* Event that describes the changes on a YArray
|
||||||
@ -38,8 +38,8 @@ export class YArrayEvent extends YEvent {
|
|||||||
const target = this.target
|
const target = this.target
|
||||||
const transaction = this._transaction
|
const transaction = this._transaction
|
||||||
const addedElements = new Set()
|
const addedElements = new Set()
|
||||||
transaction.newTypes.forEach(type => {
|
transaction.added.forEach(type => {
|
||||||
if (type._parent === target && !transaction.deletedStructs.has(type)) {
|
if (type._parent === target && !transaction.deleted.has(type)) {
|
||||||
addedElements.add(type)
|
addedElements.add(type)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -58,8 +58,8 @@ export class YArrayEvent extends YEvent {
|
|||||||
const target = this.target
|
const target = this.target
|
||||||
const transaction = this._transaction
|
const transaction = this._transaction
|
||||||
const removedElements = new Set()
|
const removedElements = new Set()
|
||||||
transaction.deletedStructs.forEach(struct => {
|
transaction.deleted.forEach(struct => {
|
||||||
if (struct._parent === target && !transaction.newTypes.has(struct)) {
|
if (struct._parent === target && !transaction.added.has(struct)) {
|
||||||
removedElements.add(struct)
|
removedElements.add(struct)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -72,7 +72,7 @@ export class YArrayEvent extends YEvent {
|
|||||||
/**
|
/**
|
||||||
* A shared Array implementation.
|
* A shared Array implementation.
|
||||||
*/
|
*/
|
||||||
export class YArray extends Type {
|
export class YArray extends AbstractType {
|
||||||
constructor () {
|
constructor () {
|
||||||
super()
|
super()
|
||||||
this.length = 0
|
this.length = 0
|
||||||
@ -128,7 +128,7 @@ export class YArray extends Type {
|
|||||||
*/
|
*/
|
||||||
toJSON () {
|
toJSON () {
|
||||||
return this.map(c => {
|
return this.map(c => {
|
||||||
if (c instanceof Type) {
|
if (c instanceof AbstractType) {
|
||||||
return c.toJSON()
|
return c.toJSON()
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
@ -140,7 +140,7 @@ export class YArray extends Type {
|
|||||||
* element of this YArray.
|
* element of this YArray.
|
||||||
*
|
*
|
||||||
* @param {Function} f Function that produces an element of the new Array
|
* @param {Function} f Function that produces an element of the new Array
|
||||||
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
|
* @param {HistorySnapshot} [snapshot]
|
||||||
* @return {Array} A new array with each element being the result of the
|
* @return {Array} A new array with each element being the result of the
|
||||||
* callback function
|
* callback function
|
||||||
*/
|
*/
|
||||||
@ -156,7 +156,7 @@ export class YArray extends Type {
|
|||||||
* Executes a provided function on once on overy element of this YArray.
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
*
|
*
|
||||||
* @param {Function} f A function to execute on every element of this YArray.
|
* @param {Function} f A function to execute on every element of this YArray.
|
||||||
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
|
* @param {HistorySnapshot} [snapshot]
|
||||||
*/
|
*/
|
||||||
forEach (f, snapshot) {
|
forEach (f, snapshot) {
|
||||||
let index = 0
|
let index = 0
|
||||||
@ -404,3 +404,5 @@ export class YArray extends Type {
|
|||||||
return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`)
|
return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const readYArray = decoder => new YArray()
|
@ -2,12 +2,11 @@
|
|||||||
* @module types
|
* @module types
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Item, logItemHelper } from '../structs/Item.js'
|
import { AbstractType } from './AbstractType.js'
|
||||||
import { Type } from '../structs/Type.js'
|
|
||||||
import { ItemJSON } from '../structs/ItemJSON.js'
|
import { ItemJSON } from '../structs/ItemJSON.js'
|
||||||
import { YEvent } from '../utils/YEvent.js'
|
import { YEvent } from '../utils/YEvent.js'
|
||||||
import { ItemBinary } from '../structs/ItemBinary.js'
|
import { ItemBinary } from '../structs/ItemBinary.js'
|
||||||
import { isVisible } from '../utils/snapshot.js'
|
import { HistorySnapshot, isVisible } from '../utils/snapshot.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event that describes the changes on a YMap.
|
* Event that describes the changes on a YMap.
|
||||||
@ -28,7 +27,7 @@ export class YMapEvent extends YEvent {
|
|||||||
/**
|
/**
|
||||||
* A shared Map implementation.
|
* A shared Map implementation.
|
||||||
*/
|
*/
|
||||||
export class YMap extends Type {
|
export class YMap extends AbstractType {
|
||||||
/**
|
/**
|
||||||
* Creates YMap Event and calls observers.
|
* Creates YMap Event and calls observers.
|
||||||
*
|
*
|
||||||
@ -68,7 +67,7 @@ export class YMap extends Type {
|
|||||||
/**
|
/**
|
||||||
* Returns the keys for each element in the YMap Type.
|
* Returns the keys for each element in the YMap Type.
|
||||||
*
|
*
|
||||||
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
|
* @param {HistorySnapshot} [snapshot]
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
keys (snapshot) {
|
keys (snapshot) {
|
||||||
@ -156,7 +155,7 @@ export class YMap extends Type {
|
|||||||
* Returns a specified element from this YMap.
|
* Returns a specified element from this YMap.
|
||||||
*
|
*
|
||||||
* @param {string} key The key of the element to return.
|
* @param {string} key The key of the element to return.
|
||||||
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
|
* @param {HistorySnapshot} [snapshot]
|
||||||
*/
|
*/
|
||||||
get (key, snapshot) {
|
get (key, snapshot) {
|
||||||
let v = this._map.get(key)
|
let v = this._map.get(key)
|
||||||
@ -184,7 +183,7 @@ export class YMap extends Type {
|
|||||||
* Returns a boolean indicating whether the specified key exists or not.
|
* Returns a boolean indicating whether the specified key exists or not.
|
||||||
*
|
*
|
||||||
* @param {string} key The key to test.
|
* @param {string} key The key to test.
|
||||||
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
|
* @param {HistorySnapshot} [snapshot]
|
||||||
*/
|
*/
|
||||||
has (key, snapshot) {
|
has (key, snapshot) {
|
||||||
let v = this._map.get(key)
|
let v = this._map.get(key)
|
||||||
@ -210,3 +209,5 @@ export class YMap extends Type {
|
|||||||
return logItemHelper('YMap', this, `mapSize:${this._map.size}`)
|
return logItemHelper('YMap', this, `mapSize:${this._map.size}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const readYMap = decoder => new YMap()
|
@ -2,7 +2,7 @@
|
|||||||
* @module types
|
* @module types
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { logItemHelper } from '../structs/Item.js'
|
import { logItemHelper } from '../structs/AbstractItem.js/index.js'
|
||||||
import { ItemEmbed } from '../structs/ItemEmbed.js'
|
import { ItemEmbed } from '../structs/ItemEmbed.js'
|
||||||
import { ItemString } from '../structs/ItemString.js'
|
import { ItemString } from '../structs/ItemString.js'
|
||||||
import { ItemFormat } from '../structs/ItemFormat.js'
|
import { ItemFormat } from '../structs/ItemFormat.js'
|
||||||
@ -723,3 +723,5 @@ export class YText extends YArray {
|
|||||||
return logItemHelper('YText', this)
|
return logItemHelper('YText', this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const readYText = decoder => new YText()
|
@ -2,7 +2,7 @@
|
|||||||
* @module types
|
* @module types
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { logItemHelper } from '../structs/Item.js'
|
import { logItemHelper } from '../structs/AbstractItem.js/index.js'
|
||||||
import { YMap } from './YMap.js'
|
import { YMap } from './YMap.js'
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
@ -433,3 +433,6 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
return dom
|
return dom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder))
|
||||||
|
export const readYXmlFragment = decoder => new YXmlFragment()
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { YEvent } from '../utils/YEvent.js'
|
import { YEvent } from '../utils/YEvent.js'
|
||||||
|
|
||||||
import { Type } from '../structs/Type.js' // eslint-disable-line
|
import { Type } from './AbstractType.js/index.js.js.js.js' // eslint-disable-line
|
||||||
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,3 +114,5 @@ export class YXmlHook extends YMap {
|
|||||||
super._integrate(y)
|
super._integrate(y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const readYXmlHook = decoder => new YXmlHook()
|
@ -48,3 +48,5 @@ export class YXmlText extends YText {
|
|||||||
super._delete(y, createDelete, gcChildren)
|
super._delete(y, createDelete, gcChildren)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const readYXmlText = decoder => new YXmlText()
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
/**
|
|
||||||
* Type that maps from Yjs type to Target type.
|
|
||||||
* Used to implement double bindings.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @template Y
|
|
||||||
* @template T
|
|
||||||
*/
|
|
||||||
export class BindMapping {
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
constructor () {
|
|
||||||
/**
|
|
||||||
* @type Map<Y, T>
|
|
||||||
*/
|
|
||||||
this.yt = new Map()
|
|
||||||
/**
|
|
||||||
* @type Map<T, Y>
|
|
||||||
*/
|
|
||||||
this.ty = new Map()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Map y to t. Removes all existing bindings from y and t
|
|
||||||
* @param {Y} y
|
|
||||||
* @param {T} t
|
|
||||||
*/
|
|
||||||
bind (y, t) {
|
|
||||||
const existingT = this.yt.get(y)
|
|
||||||
if (existingT !== undefined) {
|
|
||||||
this.ty.delete(existingT)
|
|
||||||
}
|
|
||||||
const existingY = this.ty.get(t)
|
|
||||||
if (existingY !== undefined) {
|
|
||||||
this.yt.delete(existingY)
|
|
||||||
}
|
|
||||||
this.yt.set(y, t)
|
|
||||||
this.ty.set(t, y)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {Y} y
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
hasY (y) {
|
|
||||||
return this.yt.has(y)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {T} t
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
hasT (t) {
|
|
||||||
return this.ty.has(t)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {Y} y
|
|
||||||
* @return {T}
|
|
||||||
*/
|
|
||||||
getY (y) {
|
|
||||||
return this.yt.get(y)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {T} t
|
|
||||||
* @return {Y}
|
|
||||||
*/
|
|
||||||
getT (t) {
|
|
||||||
return this.ty.get(t)
|
|
||||||
}
|
|
||||||
}
|
|
142
src/utils/DeleteSet.js
Normal file
142
src/utils/DeleteSet.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import * as map from 'lib0/map.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import { StructStore, getItemRange } from './StructStore.js' // eslint-disable-line
|
||||||
|
import { Transaction } from './Transaction.js' // eslint-disable-line
|
||||||
|
|
||||||
|
class DeleteItem {
|
||||||
|
/**
|
||||||
|
* @param {number} clock
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
constructor (clock, len) {
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.clock = clock
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.len = len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed.
|
||||||
|
* - When created in a transaction, it must only be accessed after sorting, and merging
|
||||||
|
* - This DeleteSet is send to other clients
|
||||||
|
* - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
|
||||||
|
* - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class DeleteSet {
|
||||||
|
constructor () {
|
||||||
|
/**
|
||||||
|
* @type {Map<number,Array<DeleteItem>>}
|
||||||
|
*/
|
||||||
|
this.clients = new Map()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
*/
|
||||||
|
export const sortAndMergeDeleteSet = ds => {
|
||||||
|
ds.clients.forEach(dels => {
|
||||||
|
dels.sort((a, b) => a.clock - b.clock)
|
||||||
|
// i is the current pointer
|
||||||
|
// j refers to the current insert position for the pointed item
|
||||||
|
// try to merge dels[i] with dels[i-1]
|
||||||
|
let i, j
|
||||||
|
for (i = 1, j = 1; i < dels.length; i++) {
|
||||||
|
const left = dels[i - 1]
|
||||||
|
const right = dels[i]
|
||||||
|
if (left.clock + left.len === right.clock) {
|
||||||
|
left.len += right.len
|
||||||
|
} else {
|
||||||
|
if (j < i) {
|
||||||
|
dels[j] = right
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dels.length = j
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
export const createDeleteSetFromTransaction = transaction => {
|
||||||
|
const ds = new DeleteSet()
|
||||||
|
transaction.deleted.forEach(item => {
|
||||||
|
map.setTfUndefined(ds.clients, item.id.client, () => []).push(new DeleteItem(item.id.clock, item.length))
|
||||||
|
})
|
||||||
|
sortAndMergeDeleteSet(ds)
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} ss
|
||||||
|
* @return {DeleteSet} Merged and sorted DeleteSet
|
||||||
|
*/
|
||||||
|
export const createDeleteSetFromStructStore = ss => {
|
||||||
|
const ds = new DeleteSet()
|
||||||
|
ss.clients.forEach((structs, client) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<DeleteItem>}
|
||||||
|
*/
|
||||||
|
const dsitems = []
|
||||||
|
for (let i = 0; i < structs.length; i++) {
|
||||||
|
const struct = structs[i]
|
||||||
|
const clock = struct.id.clock
|
||||||
|
let len = struct.length
|
||||||
|
if (i + 1 < structs.length) {
|
||||||
|
for (let next = structs[i + 1]; i + 1 < structs.length && next.id.clock === clock + len; i++) {
|
||||||
|
len += next.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dsitems.push(new DeleteItem(clock, len))
|
||||||
|
}
|
||||||
|
if (dsitems.length > 0) {
|
||||||
|
ds.clients.set(client, dsitems)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
*/
|
||||||
|
export const writeDeleteSet = (encoder, ds) => {
|
||||||
|
encoding.writeVarUint(encoder, ds.clients.size)
|
||||||
|
ds.clients.forEach((dsitems, client) => {
|
||||||
|
encoding.writeVarUint(encoder, client)
|
||||||
|
const len = dsitems.length
|
||||||
|
encoding.writeVarUint(encoder, len)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const item = dsitems[i]
|
||||||
|
encoding.writeVarUint(encoder, item.clock)
|
||||||
|
encoding.writeVarUint(encoder, item.len)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {StructStore} ss
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
export const readDeleteSet = (decoder, ss, transaction) => {
|
||||||
|
const numClients = decoding.readVarUint(decoder)
|
||||||
|
for (let i = 0; i < numClients; i++) {
|
||||||
|
const client = decoding.readVarUint(decoder)
|
||||||
|
const len = decoding.readVarUint(decoder)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const clock = decoding.readVarUint(decoder)
|
||||||
|
const len = decoding.readVarUint(decoder)
|
||||||
|
getItemRange(ss, transaction, client, clock, len).forEach(struct => struct.delete(transaction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module utils
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Tree } from 'lib0/tree.js'
|
|
||||||
import * as ID from './ID.js'
|
|
||||||
|
|
||||||
export class DSNode {
|
|
||||||
constructor (id, len, gc) {
|
|
||||||
this._id = id
|
|
||||||
this.len = len
|
|
||||||
this.gc = gc
|
|
||||||
}
|
|
||||||
clone () {
|
|
||||||
return new DSNode(this._id, this.len, this.gc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DeleteStore extends Tree {
|
|
||||||
logTable () {
|
|
||||||
const deletes = []
|
|
||||||
this.iterate(null, null, n => {
|
|
||||||
deletes.push({
|
|
||||||
user: n._id.user,
|
|
||||||
clock: n._id.clock,
|
|
||||||
len: n.len,
|
|
||||||
gc: n.gc
|
|
||||||
})
|
|
||||||
})
|
|
||||||
console.table(deletes)
|
|
||||||
}
|
|
||||||
isDeleted (id) {
|
|
||||||
var n = this.findWithUpperBound(id)
|
|
||||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
|
||||||
}
|
|
||||||
mark (id, length, gc) {
|
|
||||||
if (length === 0) return
|
|
||||||
// Step 1. Unmark range
|
|
||||||
const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1))
|
|
||||||
// Resize left DSNode if necessary
|
|
||||||
if (leftD !== null && leftD._id.user === id.user) {
|
|
||||||
if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
|
|
||||||
// node is overlapping. need to resize
|
|
||||||
if (id.clock + length < leftD._id.clock + leftD.len) {
|
|
||||||
// overlaps new mark range and some more
|
|
||||||
// create another DSNode to the right of new mark
|
|
||||||
this.put(new DSNode(ID.createID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
|
|
||||||
}
|
|
||||||
// resize left DSNode
|
|
||||||
leftD.len = id.clock - leftD._id.clock
|
|
||||||
} // Otherwise there is no overlapping
|
|
||||||
}
|
|
||||||
// Resize right DSNode if necessary
|
|
||||||
const upper = ID.createID(id.user, id.clock + length - 1)
|
|
||||||
const rightD = this.findWithUpperBound(upper)
|
|
||||||
if (rightD !== null && rightD._id.user === id.user) {
|
|
||||||
if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
|
|
||||||
const d = id.clock + length - rightD._id.clock
|
|
||||||
rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d)
|
|
||||||
rightD.len -= d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Now we only have to delete all inner marks
|
|
||||||
const deleteNodeIds = []
|
|
||||||
this.iterate(id, upper, m => {
|
|
||||||
deleteNodeIds.push(m._id)
|
|
||||||
})
|
|
||||||
for (let i = deleteNodeIds.length - 1; i >= 0; i--) {
|
|
||||||
this.delete(deleteNodeIds[i])
|
|
||||||
}
|
|
||||||
let newMark = new DSNode(id, length, gc)
|
|
||||||
// Step 2. Check if we can extend left or right
|
|
||||||
if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) {
|
|
||||||
// We can extend left
|
|
||||||
leftD.len += length
|
|
||||||
newMark = leftD
|
|
||||||
}
|
|
||||||
const rightNext = this.find(ID.createID(id.user, id.clock + length))
|
|
||||||
if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) {
|
|
||||||
// We can merge newMark and rightNext
|
|
||||||
newMark.len += rightNext.len
|
|
||||||
this.delete(rightNext._id)
|
|
||||||
}
|
|
||||||
if (leftD !== newMark) {
|
|
||||||
// only put if we didn't extend left
|
|
||||||
this.put(newMark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
117
src/utils/ID.js
117
src/utils/ID.js
@ -2,81 +2,72 @@
|
|||||||
* @module utils
|
* @module utils
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getStructReference } from '../utils/structReferences.js'
|
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
|
||||||
export class ID {
|
export class ID {
|
||||||
constructor (user, clock) {
|
/**
|
||||||
this.user = user // TODO: rename to client
|
* @param {number} client client id
|
||||||
|
* @param {number} clock unique per client id, continuous number
|
||||||
|
*/
|
||||||
|
constructor (client, clock) {
|
||||||
|
/**
|
||||||
|
* @type {number} Client id
|
||||||
|
*/
|
||||||
|
this.client = client
|
||||||
|
/**
|
||||||
|
* @type {number} unique per client id, continuous number
|
||||||
|
*/
|
||||||
this.clock = clock
|
this.clock = clock
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @return {ID}
|
||||||
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
return new ID(this.user, this.clock)
|
return new ID(this.client, this.clock)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
equals (id) {
|
equals (id) {
|
||||||
return id !== null && id.user === this.user && id.clock === this.clock
|
return id !== null && id.client === this.client && id.clock === this.clock
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
lessThan (id) {
|
lessThan (id) {
|
||||||
if (id.constructor === ID) {
|
if (id.constructor === ID) {
|
||||||
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
|
return this.client < id.client || (this.client === id.client && this.clock < id.clock)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} client
|
||||||
|
* @param {number} clock
|
||||||
|
*/
|
||||||
|
export const createID = (client, clock) => new ID(client, clock)
|
||||||
|
|
||||||
|
const isNullID = 0xFFFFFF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {ID} id
|
||||||
|
*/
|
||||||
|
export const writeID = (encoder, id) => {
|
||||||
|
encoding.writeVarUint(encoder, id.client)
|
||||||
|
encoding.writeVarUint(encoder, id.clock)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {encoding.Encoder} encoder
|
* @param {encoding.Encoder} encoder
|
||||||
*/
|
*/
|
||||||
encode (encoder) {
|
export const writeNullID = (encoder) =>
|
||||||
encoding.writeVarUint(encoder, this.user)
|
encoding.writeVarUint(encoder, isNullID)
|
||||||
encoding.writeVarUint(encoder, this.clock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createID = (user, clock) => new ID(user, clock)
|
|
||||||
|
|
||||||
export const RootFakeUserID = 0xFFFFFF
|
|
||||||
|
|
||||||
export class RootID {
|
|
||||||
/**
|
|
||||||
* @param {string} name
|
|
||||||
* @param {Function?} typeConstructor
|
|
||||||
*/
|
|
||||||
constructor (name, typeConstructor) {
|
|
||||||
this.user = RootFakeUserID
|
|
||||||
this.name = name
|
|
||||||
this.type = typeConstructor === null ? null : getStructReference(typeConstructor)
|
|
||||||
}
|
|
||||||
equals (id) {
|
|
||||||
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
|
|
||||||
}
|
|
||||||
lessThan (id) {
|
|
||||||
if (id.constructor === RootID) {
|
|
||||||
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {encoding.Encoder} encoder
|
|
||||||
*/
|
|
||||||
encode (encoder) {
|
|
||||||
encoding.writeVarUint(encoder, this.user)
|
|
||||||
encoding.writeVarString(encoder, this.name)
|
|
||||||
encoding.writeVarUint(encoder, this.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new root id.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* y.define('name', Y.Array) // name, and typeConstructor
|
|
||||||
*
|
|
||||||
* @param {string} name
|
|
||||||
* @param {Function?} typeConstructor must be defined in structReferences
|
|
||||||
*/
|
|
||||||
export const createRootID = (name, typeConstructor) => new RootID(name, typeConstructor)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read ID.
|
* Read ID.
|
||||||
@ -84,15 +75,9 @@ export const createRootID = (name, typeConstructor) => new RootID(name, typeCons
|
|||||||
* * Otherwise an ID is returned
|
* * Otherwise an ID is returned
|
||||||
*
|
*
|
||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
* @return {ID|RootID}
|
* @return {ID | null}
|
||||||
*/
|
*/
|
||||||
export const decode = decoder => {
|
export const readID = decoder => {
|
||||||
const user = decoding.readVarUint(decoder)
|
const client = decoding.readVarUint(decoder)
|
||||||
if (user === RootFakeUserID) {
|
return client === isNullID ? null : createID(client, decoding.readVarUint(decoder))
|
||||||
// read property name and type id
|
|
||||||
const rid = createRootID(decoding.readVarString(decoder), null)
|
|
||||||
rid.type = decoding.readVarUint(decoder)
|
|
||||||
return rid
|
|
||||||
}
|
|
||||||
return createID(user, decoding.readVarUint(decoder))
|
|
||||||
}
|
}
|
||||||
|
@ -1,98 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module utils
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Tree } from 'lib0/tree.js'
|
|
||||||
import * as ID from '../utils/ID.js'
|
|
||||||
import { getStruct } from './structReferences.js'
|
|
||||||
import { GC } from '../structs/GC.js'
|
|
||||||
import * as Item from '../structs/Item.js'
|
|
||||||
|
|
||||||
export class OperationStore extends Tree {
|
|
||||||
constructor (y) {
|
|
||||||
super()
|
|
||||||
this.y = y
|
|
||||||
}
|
|
||||||
logTable () {
|
|
||||||
const items = []
|
|
||||||
this.iterate(null, null, item => {
|
|
||||||
if (item.constructor === GC) {
|
|
||||||
items.push({
|
|
||||||
id: Item.stringifyItemID(item),
|
|
||||||
content: item._length,
|
|
||||||
deleted: 'GC'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
items.push({
|
|
||||||
id: Item.stringifyItemID(item),
|
|
||||||
origin: item._origin === null ? '()' : Item.stringifyID(item._origin._lastId),
|
|
||||||
left: item._left === null ? '()' : Item.stringifyID(item._left._lastId),
|
|
||||||
right: Item.stringifyItemID(item._right),
|
|
||||||
right_origin: Item.stringifyItemID(item._right_origin),
|
|
||||||
parent: Item.stringifyItemID(item._parent),
|
|
||||||
parentSub: item._parentSub,
|
|
||||||
deleted: item._deleted,
|
|
||||||
content: JSON.stringify(item._content)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.table(items)
|
|
||||||
}
|
|
||||||
get (id) {
|
|
||||||
let struct = this.find(id)
|
|
||||||
if (struct === null && id instanceof ID.RootID) {
|
|
||||||
const Constr = getStruct(id.type)
|
|
||||||
const y = this.y
|
|
||||||
struct = new Constr()
|
|
||||||
struct._id = id
|
|
||||||
struct._parent = y
|
|
||||||
y.transact(() => {
|
|
||||||
struct._integrate(y)
|
|
||||||
})
|
|
||||||
this.put(struct)
|
|
||||||
}
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
// Use getItem for structs with _length > 1
|
|
||||||
getItem (id) {
|
|
||||||
var item = this.findWithUpperBound(id)
|
|
||||||
if (item === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const itemID = item._id
|
|
||||||
if (id.user === itemID.user && id.clock < itemID.clock + item._length) {
|
|
||||||
return item
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return an insertion such that id is the first element of content
|
|
||||||
// This function manipulates an item, if necessary
|
|
||||||
getItemCleanStart (id) {
|
|
||||||
var ins = this.getItem(id)
|
|
||||||
if (ins === null || ins._length === 1) {
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
const insID = ins._id
|
|
||||||
if (insID.clock === id.clock) {
|
|
||||||
return ins
|
|
||||||
} else {
|
|
||||||
return ins._splitAt(this.y, id.clock - insID.clock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return an insertion such that id is the last element of content
|
|
||||||
// This function manipulates an operation, if necessary
|
|
||||||
getItemCleanEnd (id) {
|
|
||||||
var ins = this.getItem(id)
|
|
||||||
if (ins === null || ins._length === 1) {
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
const insID = ins._id
|
|
||||||
if (insID.clock + ins._length - 1 === id.clock) {
|
|
||||||
return ins
|
|
||||||
} else {
|
|
||||||
ins._splitAt(this.y, id.clock - insID.clock + 1)
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module utils
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as ID from '../utils/ID.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
export class StateStore {
|
|
||||||
constructor (y) {
|
|
||||||
this.y = y
|
|
||||||
this.state = new Map()
|
|
||||||
}
|
|
||||||
logTable () {
|
|
||||||
const entries = []
|
|
||||||
for (let [user, state] of this.state) {
|
|
||||||
entries.push({
|
|
||||||
user, state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
console.table(entries)
|
|
||||||
}
|
|
||||||
getNextID (len) {
|
|
||||||
const user = this.y.userID
|
|
||||||
const state = this.getState(user)
|
|
||||||
this.setState(user, state + len)
|
|
||||||
return ID.createID(user, state)
|
|
||||||
}
|
|
||||||
updateRemoteState (struct) {
|
|
||||||
let user = struct._id.user
|
|
||||||
let userState = this.state.get(user)
|
|
||||||
while (struct !== null && struct._id.clock === userState) {
|
|
||||||
userState += struct._length
|
|
||||||
struct = this.y.os.get(ID.createID(user, userState))
|
|
||||||
}
|
|
||||||
this.state.set(user, userState)
|
|
||||||
}
|
|
||||||
getState (user) {
|
|
||||||
let state = this.state.get(user)
|
|
||||||
if (state == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
setState (user, state) {
|
|
||||||
// TODO: modify missingi structs here
|
|
||||||
const beforeState = this.y._transaction.beforeState
|
|
||||||
if (!beforeState.has(user)) {
|
|
||||||
beforeState.set(user, this.getState(user))
|
|
||||||
}
|
|
||||||
this.state.set(user, state)
|
|
||||||
}
|
|
||||||
}
|
|
223
src/utils/StructStore.js
Normal file
223
src/utils/StructStore.js
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import { AbstractStruct } from '../structs/AbstractStruct.js' // eslint-disable-line
|
||||||
|
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
|
||||||
|
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
|
||||||
|
import { ID } from './ID.js' // eslint-disable-line
|
||||||
|
import { Transaction } from './Transaction.js' // eslint-disable-line
|
||||||
|
import * as map from 'lib0/map.js'
|
||||||
|
import * as math from 'lib0/math.js'
|
||||||
|
|
||||||
|
export class StructStore {
|
||||||
|
constructor () {
|
||||||
|
/**
|
||||||
|
* @type {Map<number,Array<AbstractStruct>>}
|
||||||
|
*/
|
||||||
|
this.clients = new Map()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the states as an array of {client,clock} pairs.
|
||||||
|
* Note that clock refers to the next expected clock id.
|
||||||
|
*
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @return {Array<{client:number,clock:number}>}
|
||||||
|
*/
|
||||||
|
export const getStates = store =>
|
||||||
|
map.map(store.clients, (structs, client) => {
|
||||||
|
const struct = structs[structs.length - 1]
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
clock: struct.id.clock + struct.length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
*/
|
||||||
|
export const integretyCheck = store => {
|
||||||
|
store.clients.forEach(structs => {
|
||||||
|
for (let i = 1; i < structs.length; i++) {
|
||||||
|
const l = structs[i - 1]
|
||||||
|
const r = structs[i]
|
||||||
|
if (l.id.clock + l.length !== r.id.clock) {
|
||||||
|
throw new Error('StructStore failed integrety check')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {AbstractStruct} struct
|
||||||
|
*/
|
||||||
|
export const addStruct = (store, struct) => {
|
||||||
|
map.setTfUndefined(store.clients, struct.id.client, () => []).push(struct)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
* @param {Array<AbstractStruct>} structs // ordered structs without holes
|
||||||
|
* @param {number} clock
|
||||||
|
* @return {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const findIndex = (structs, clock) => {
|
||||||
|
let left = 0
|
||||||
|
let right = structs.length
|
||||||
|
while (left <= right) {
|
||||||
|
const midindex = math.floor((left + right) / 2)
|
||||||
|
const mid = structs[midindex]
|
||||||
|
const midclock = mid.id.clock
|
||||||
|
if (midclock <= clock) {
|
||||||
|
if (clock < midclock + mid.length) {
|
||||||
|
return midindex
|
||||||
|
}
|
||||||
|
left = midindex
|
||||||
|
} else {
|
||||||
|
right = midindex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('ID does not exist')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
*
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {AbstractStruct}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const find = (store, id) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<AbstractStruct>}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
const structs = store.clients.get(id.client)
|
||||||
|
return structs[findIndex(structs, id.clock)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
*
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {ItemType}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
export const getItemType = (store, id) => find(store, id)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractItem} struct
|
||||||
|
* @param {number} diff
|
||||||
|
*/
|
||||||
|
const splitStruct = (transaction, struct, diff) => {
|
||||||
|
const right = struct.splitAt(diff)
|
||||||
|
if (transaction.added.has(struct)) {
|
||||||
|
transaction.added.add(right)
|
||||||
|
} else if (transaction.deleted.has(struct)) {
|
||||||
|
transaction.deleted.add(right)
|
||||||
|
}
|
||||||
|
return right
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {AbstractItem}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const getItemCleanStart = (store, transaction, id) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<AbstractItem>}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
const structs = store.clients.get(id.client)
|
||||||
|
const index = findIndex(structs, id.clock)
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let struct = structs[index]
|
||||||
|
if (struct.id.clock < id.clock) {
|
||||||
|
struct = splitStruct(transaction, struct, id.clock - struct.id.clock)
|
||||||
|
structs.splice(index, 0, struct)
|
||||||
|
}
|
||||||
|
return struct
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {AbstractItem}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const getItemCleanEnd = (store, transaction, id) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<AbstractItem>}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
const structs = store.clients.get(id.client)
|
||||||
|
const index = findIndex(structs, id.clock)
|
||||||
|
const struct = structs[index]
|
||||||
|
if (id.clock !== struct.id.clock + struct.length - 1) {
|
||||||
|
structs.splice(index, 0, splitStruct(transaction, struct, id.clock - struct.id.clock + 1))
|
||||||
|
}
|
||||||
|
return struct
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {number} client
|
||||||
|
* @param {number} clock
|
||||||
|
* @param {number} len
|
||||||
|
* @return {Array<AbstractItem>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const getItemRange = (store, transaction, client, clock, len) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<AbstractItem>}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
const structs = store.clients.get(client)
|
||||||
|
let index = findIndex(structs, clock)
|
||||||
|
let struct = structs[index]
|
||||||
|
let range = []
|
||||||
|
if (struct.id.clock < clock) {
|
||||||
|
struct = splitStruct(transaction, struct, clock - struct.id.clock)
|
||||||
|
structs.splice(index, 0, struct)
|
||||||
|
}
|
||||||
|
while (struct.id.clock + struct.length <= clock + len) {
|
||||||
|
range.push(struct)
|
||||||
|
struct = structs[++index]
|
||||||
|
}
|
||||||
|
if (clock < struct.id.clock + struct.length) {
|
||||||
|
structs.splice(index, 0, splitStruct(transaction, struct, clock + len - struct.id.clock))
|
||||||
|
range.push(struct)
|
||||||
|
}
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace `item` with `newitem` in store
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {AbstractStruct} struct
|
||||||
|
* @param {AbstractStruct} newStruct
|
||||||
|
*/
|
||||||
|
export const replace = (store, struct, newStruct) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<AbstractStruct>}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
const structs = store.clients.get(struct.id.client)
|
||||||
|
structs[findIndex(structs, struct.id)] = newStruct
|
||||||
|
}
|
@ -3,6 +3,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as encoding from 'lib0/encoding.js'
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
|
||||||
|
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
|
||||||
|
import { Y } from './Y.js' // eslint-disable-line
|
||||||
|
import { YEvent } from './YEvent.js' // eslint-disable-line
|
||||||
|
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
|
||||||
|
import { getState } from './StructStore.js'
|
||||||
|
import { createID } from './ID.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A transaction is created for every change on the Yjs model. It is possible
|
* A transaction is created for every change on the Yjs model. It is possible
|
||||||
@ -28,42 +35,52 @@ import * as encoding from 'lib0/encoding.js'
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class Transaction {
|
export class Transaction {
|
||||||
|
/**
|
||||||
|
* @param {Y} y
|
||||||
|
*/
|
||||||
constructor (y) {
|
constructor (y) {
|
||||||
/**
|
/**
|
||||||
* @type {Y} The Yjs instance.
|
* @type {Y} The Yjs instance.
|
||||||
*/
|
*/
|
||||||
this.y = y
|
this.y = y
|
||||||
/**
|
/**
|
||||||
* All new types that are added during a transaction.
|
* All new items that are added during a transaction.
|
||||||
* @type {Set<Item>}
|
* @type {Set<AbstractItem>}
|
||||||
*/
|
*/
|
||||||
this.newTypes = new Set()
|
this.added = new Set()
|
||||||
/**
|
/**
|
||||||
* All types that were directly modified (property added or child
|
* Set of all deleted items
|
||||||
* inserted/deleted). New types are not included in this Set.
|
* @type {Set<AbstractItem>}
|
||||||
* Maps from type to parentSubs (`item._parentSub = null` for YArray)
|
|
||||||
* @type {Map<Type|Y,String>}
|
|
||||||
*/
|
*/
|
||||||
this.changedTypes = new Map()
|
this.deleted = new Set()
|
||||||
// TODO: rename deletedTypes
|
|
||||||
/**
|
|
||||||
* Set of all deleted Types and Structs.
|
|
||||||
* @type {Set<Item>}
|
|
||||||
*/
|
|
||||||
this.deletedStructs = new Set()
|
|
||||||
/**
|
/**
|
||||||
* Saves the old state set of the Yjs instance. If a state was modified,
|
* Saves the old state set of the Yjs instance. If a state was modified,
|
||||||
* the original value is saved here.
|
* the original value is saved here.
|
||||||
* @type {Map<Number,Number>}
|
* @type {Map<Number,Number>}
|
||||||
*/
|
*/
|
||||||
this.beforeState = new Map()
|
this.beforeState = new Map()
|
||||||
|
/**
|
||||||
|
* All types that were directly modified (property added or child
|
||||||
|
* inserted/deleted). New types are not included in this Set.
|
||||||
|
* Maps from type to parentSubs (`item._parentSub = null` for YArray)
|
||||||
|
* @type {Map<ItemType,Set<String|null>>}
|
||||||
|
*/
|
||||||
|
this.changed = new Map()
|
||||||
/**
|
/**
|
||||||
* Stores the events for the types that observe also child elements.
|
* Stores the events for the types that observe also child elements.
|
||||||
* It is mainly used by `observeDeep`.
|
* It is mainly used by `observeDeep`.
|
||||||
* @type {Map<Type,Array<YEvent>>}
|
* @type {Map<ItemType,Array<YEvent>>}
|
||||||
*/
|
*/
|
||||||
this.changedParentTypes = new Map()
|
this.changedParentTypes = new Map()
|
||||||
this.encodedStructsLen = 0
|
this.encodedStructsLen = 0
|
||||||
this.encodedStructs = encoding.createEncoder()
|
this.encodedStructs = encoding.createEncoder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
export const nextID = transaction => {
|
||||||
|
const y = transaction.y
|
||||||
|
return createID(y.clientID, getState(y.store, y.clientID))
|
||||||
|
}
|
||||||
|
@ -126,7 +126,7 @@ export class UndoManager {
|
|||||||
this._redoing === false &&
|
this._redoing === false &&
|
||||||
this._lastTransactionWasUndo === false &&
|
this._lastTransactionWasUndo === false &&
|
||||||
lastUndoOp !== null &&
|
lastUndoOp !== null &&
|
||||||
(options.captureTimeout < 0 || reverseOperation.created - lastUndoOp.created <= options.captureTimeout)
|
((options.captureTimeout < 0) || (reverseOperation.created.getTime() - lastUndoOp.created.getTime()) <= options.captureTimeout)
|
||||||
) {
|
) {
|
||||||
lastUndoOp.created = reverseOperation.created
|
lastUndoOp.created = reverseOperation.created
|
||||||
if (reverseOperation.toState !== null) {
|
if (reverseOperation.toState !== null) {
|
||||||
|
140
src/utils/Y.js
140
src/utils/Y.js
@ -1,10 +1,14 @@
|
|||||||
import { DeleteStore } from './DeleteStore.js'
|
import { DeleteStore } from './DeleteSet.js/index.js' // TODO: remove
|
||||||
import { OperationStore } from './OperationStore.js'
|
import { OperationStore } from './OperationStore.js'
|
||||||
import { StateStore } from './StateStore.js'
|
import { StateStore } from './StateStore.js'
|
||||||
|
import { StructStore } from './StructStore.js'
|
||||||
import * as random from 'lib0/random.js'
|
import * as random from 'lib0/random.js'
|
||||||
import { createRootID } from './ID.js'
|
import * as map from 'lib0/map.js'
|
||||||
import { Observable } from 'lib0/observable.js'
|
import { Observable } from 'lib0/observable.js'
|
||||||
import { Transaction } from './Transaction.js'
|
import { Transaction } from './Transaction.js'
|
||||||
|
import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' // eslint-disable-line
|
||||||
|
import { AbstractType } from '../types/AbstractType.js'
|
||||||
|
import { YArray } from '../types/YArray.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anything that can be encoded with `JSON.stringify` and can be decoded with
|
* Anything that can be encoded with `JSON.stringify` and can be decoded with
|
||||||
@ -28,25 +32,33 @@ export class Y extends Observable {
|
|||||||
constructor (conf = {}) {
|
constructor (conf = {}) {
|
||||||
super()
|
super()
|
||||||
this.gcEnabled = conf.gc || false
|
this.gcEnabled = conf.gc || false
|
||||||
this._contentReady = false
|
this.clientID = random.uint32()
|
||||||
this.userID = random.uint32()
|
this.share = new Map()
|
||||||
// TODO: This should be a Map so we can use encodables as keys
|
this.store = new StructStore()
|
||||||
this._map = new Map()
|
/**
|
||||||
this.ds = new DeleteStore()
|
* @type {Map<number, Map<number, AbstractRef>>}
|
||||||
this.os = new OperationStore(this)
|
*/
|
||||||
this.ss = new StateStore(this)
|
|
||||||
this._missingStructs = new Map()
|
this._missingStructs = new Map()
|
||||||
|
/**
|
||||||
|
* @type {Array<AbstractStruct>}
|
||||||
|
*/
|
||||||
this._readyToIntegrate = []
|
this._readyToIntegrate = []
|
||||||
|
/**
|
||||||
|
* @type {Transaction | null}
|
||||||
|
*/
|
||||||
this._transaction = null
|
this._transaction = null
|
||||||
this.connected = false
|
|
||||||
// for compatibility with isParentOf
|
|
||||||
this._parent = null
|
|
||||||
this._hasUndoManager = false
|
this._hasUndoManager = false
|
||||||
this._deleted = false // for compatiblity of having this as a parent for types
|
|
||||||
this._id = null
|
|
||||||
}
|
}
|
||||||
_beforeChange () {}
|
/**
|
||||||
_callObserver (transaction, subs, remote) {}
|
* @type {Transaction}
|
||||||
|
*/
|
||||||
|
get transaction () {
|
||||||
|
const t = this._transaction
|
||||||
|
if (t === null) {
|
||||||
|
throw new Error('All changes must happen inside a transaction')
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Changes that happen inside of a transaction are bundled. This means that
|
* Changes that happen inside of a transaction are bundled. This means that
|
||||||
* the observer fires _after_ the transaction is finished and that all changes
|
* the observer fires _after_ the transaction is finished and that all changes
|
||||||
@ -59,8 +71,9 @@ export class Y extends Observable {
|
|||||||
* Defaults to false.
|
* Defaults to false.
|
||||||
*/
|
*/
|
||||||
transact (f, remote = false) {
|
transact (f, remote = false) {
|
||||||
let initialCall = this._transaction === null
|
let initialCall = false
|
||||||
if (initialCall) {
|
if (this._transaction === null) {
|
||||||
|
initialCall = true
|
||||||
this._transaction = new Transaction(this)
|
this._transaction = new Transaction(this)
|
||||||
this.emit('beforeTransaction', [this, this._transaction, remote])
|
this.emit('beforeTransaction', [this, this._transaction, remote])
|
||||||
}
|
}
|
||||||
@ -74,9 +87,9 @@ export class Y extends Observable {
|
|||||||
const transaction = this._transaction
|
const transaction = this._transaction
|
||||||
this._transaction = null
|
this._transaction = null
|
||||||
// emit change events on changed types
|
// emit change events on changed types
|
||||||
transaction.changedTypes.forEach((subs, type) => {
|
transaction.changed.forEach((subs, itemtype) => {
|
||||||
if (!type._deleted) {
|
if (!itemtype._deleted) {
|
||||||
type._callObserver(transaction, subs, remote)
|
itemtype.type._callObserver(transaction, subs, remote)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
transaction.changedParentTypes.forEach((events, type) => {
|
transaction.changedParentTypes.forEach((events, type) => {
|
||||||
@ -91,86 +104,57 @@ export class Y extends Observable {
|
|||||||
})
|
})
|
||||||
// we don't have to check for events.length
|
// we don't have to check for events.length
|
||||||
// because there is no way events is empty..
|
// because there is no way events is empty..
|
||||||
type._deepEventHandler.callEventListeners(transaction, events)
|
type.type._deepEventHandler.callEventListeners(transaction, events)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// when all changes & events are processed, emit afterTransaction event
|
// when all changes & events are processed, emit afterTransaction event
|
||||||
this.emit('afterTransaction', [this, transaction, remote])
|
this.emit('afterTransaction', [this, transaction, remote])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fake _start for root properties (y.set('name', type))
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
get _start () {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fake _start for root properties (y.set('name', type))
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
set _start (start) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a shared data type.
|
* Define a shared data type.
|
||||||
*
|
*
|
||||||
* Multiple calls of `y.define(name, TypeConstructor)` yield the same result
|
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
|
||||||
* and do not overwrite each other. I.e.
|
* and do not overwrite each other. I.e.
|
||||||
* `y.define(name, type) === y.define(name, type)`
|
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
|
||||||
*
|
*
|
||||||
* After this method is called, the type is also available on `y._map.get(name)`.
|
* After this method is called, the type is also available on `y.share.get(name)`.
|
||||||
*
|
*
|
||||||
* *Best Practices:*
|
* *Best Practices:*
|
||||||
* Either define all types right after the Yjs instance is created or always
|
* Define all types right after the Yjs instance is created and store them in a separate object.
|
||||||
* use `y.define(..)` when accessing a type.
|
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // Option 1
|
|
||||||
* const y = new Y(..)
|
* const y = new Y(..)
|
||||||
* y.define('myArray', YArray)
|
* const appState = {
|
||||||
* y.define('myMap', YMap)
|
* document: y.getText('document')
|
||||||
* // .. when accessing the type use y._map.get(name)
|
* comments: y.getArray('comments')
|
||||||
* y.share.myArray.insert(..)
|
* }
|
||||||
* y.share.myMap.set(..)
|
|
||||||
*
|
*
|
||||||
* // Option2
|
* @TODO: implement getText, getArray, ..
|
||||||
* const y = new Y(..)
|
|
||||||
* // .. when accessing the type use `y.define(..)`
|
|
||||||
* y.define('myArray', YArray).insert(..)
|
|
||||||
* y.define('myMap', YMap).set(..)
|
|
||||||
*
|
*
|
||||||
* @param {String} name
|
* @param {string} name
|
||||||
* @param {Function} TypeConstructor The constructor of the type definition
|
* @param {Function} TypeConstructor The constructor of the type definition
|
||||||
* @returns {any} The created type. Constructed with TypeConstructor
|
* @return {AbstractType} The created type. Constructed with TypeConstructor
|
||||||
*/
|
*/
|
||||||
define (name, TypeConstructor) {
|
get (name, TypeConstructor = AbstractType) {
|
||||||
let id = createRootID(name, TypeConstructor)
|
// @ts-ignore
|
||||||
let type = this.os.get(id)
|
const type = map.setTfUndefined(this.share, name, () => new TypeConstructor())
|
||||||
if (this._map.get(name) === undefined) {
|
const Constr = type.constructor
|
||||||
this._map.set(name, type)
|
if (Constr !== TypeConstructor) {
|
||||||
} else if (this._map.get(name) !== type) {
|
if (Constr === AbstractType) {
|
||||||
throw new Error('Type is already defined with a different constructor')
|
const t = new Constr()
|
||||||
|
t._map = type._map
|
||||||
|
t._start = type._start
|
||||||
|
t._length = type._length
|
||||||
|
this.share.set(name, t)
|
||||||
|
return t
|
||||||
|
} else {
|
||||||
|
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a defined type. The type must be defined locally. First define the
|
|
||||||
* type with {@link define}.
|
|
||||||
*
|
|
||||||
* This returns the same value as `y.share[name]`
|
|
||||||
*
|
|
||||||
* @param {String} name The typename
|
|
||||||
* @return {any}
|
|
||||||
*/
|
|
||||||
get (name) {
|
|
||||||
return this._map.get(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect from the room, and destroy all traces of this Yjs instance.
|
* Disconnect from the room, and destroy all traces of this Yjs instance.
|
||||||
*/
|
*/
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
|
||||||
|
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module utils
|
* @module utils
|
||||||
*/
|
*/
|
||||||
@ -7,17 +10,17 @@
|
|||||||
*/
|
*/
|
||||||
export class YEvent {
|
export class YEvent {
|
||||||
/**
|
/**
|
||||||
* @param {Type} target The changed type.
|
* @param {AbstractType} target The changed type.
|
||||||
*/
|
*/
|
||||||
constructor (target) {
|
constructor (target) {
|
||||||
/**
|
/**
|
||||||
* The type on which this event was created on.
|
* The type on which this event was created on.
|
||||||
* @type {Type}
|
* @type {AbstractType}
|
||||||
*/
|
*/
|
||||||
this.target = target
|
this.target = target
|
||||||
/**
|
/**
|
||||||
* The current target on which the observe callback is called.
|
* The current target on which the observe callback is called.
|
||||||
* @type {Type}
|
* @type {AbstractType}
|
||||||
*/
|
*/
|
||||||
this.currentTarget = target
|
this.currentTarget = target
|
||||||
}
|
}
|
||||||
@ -34,6 +37,48 @@ export class YEvent {
|
|||||||
* type === event.target // => true
|
* type === event.target // => true
|
||||||
*/
|
*/
|
||||||
get path () {
|
get path () {
|
||||||
return this.currentTarget.getPathTo(this.target)
|
// @ts-ignore _item is defined because target is integrated
|
||||||
|
return getPathTo(this.currentTarget, this.target._item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the path from this type to the specified target.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // `child` should be accessible via `type.get(path[0]).get(path[1])..`
|
||||||
|
* const path = type.getPathTo(child)
|
||||||
|
* // assuming `type instanceof YArray`
|
||||||
|
* console.log(path) // might look like => [2, 'key1']
|
||||||
|
* child === type.get(path[0]).get(path[1])
|
||||||
|
*
|
||||||
|
* @param {AbstractType} parent
|
||||||
|
* @param {AbstractItem} child target
|
||||||
|
* @return {Array<string|number>} Path to the target
|
||||||
|
*/
|
||||||
|
const getPathTo = (parent, child) => {
|
||||||
|
const path = []
|
||||||
|
while (true) {
|
||||||
|
const cparent = child.parent
|
||||||
|
if (child.parentSub !== null) {
|
||||||
|
// parent is map-ish
|
||||||
|
path.unshift(child.parentSub)
|
||||||
|
} else {
|
||||||
|
// parent is array-ish
|
||||||
|
let i = 0
|
||||||
|
let c = cparent._start
|
||||||
|
while (c !== child && c !== null) {
|
||||||
|
if (!c.deleted) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
c = c.right
|
||||||
|
}
|
||||||
|
path.unshift(i)
|
||||||
|
}
|
||||||
|
if (parent === cparent) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
// @ts-ignore parent._item cannot be null, because it is integrated
|
||||||
|
child = parent._item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* @module utils
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as ID from '../utils/ID.js'
|
|
||||||
import { ItemJSON } from '../structs/ItemJSON.js'
|
|
||||||
import { ItemString } from '../structs/ItemString.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to merge all items in os with their successors.
|
|
||||||
*
|
|
||||||
* Some transformations (like delete) fragment items.
|
|
||||||
* Item(c: 'ab') + Delete(1,1) + Delete(0, 1) -> Item(c: 'a',deleted);Item(c: 'b',deleted)
|
|
||||||
*
|
|
||||||
* This functions merges the fragmented nodes together:
|
|
||||||
* Item(c: 'a',deleted);Item(c: 'b',deleted) -> Item(c: 'ab', deleted)
|
|
||||||
*
|
|
||||||
* TODO: The Tree implementation does not support deletions in-spot.
|
|
||||||
* This is why all deletions must be performed after the traversal.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const defragmentItemContent = y => {
|
|
||||||
const os = y.os
|
|
||||||
if (os.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let deletes = []
|
|
||||||
let node = os.findSmallestNode()
|
|
||||||
let next = node.next()
|
|
||||||
while (next !== null) {
|
|
||||||
let a = node.val
|
|
||||||
let b = next.val
|
|
||||||
if (
|
|
||||||
(a instanceof ItemJSON || a instanceof ItemString) &&
|
|
||||||
a.constructor === b.constructor &&
|
|
||||||
a._deleted === b._deleted &&
|
|
||||||
a._right === b &&
|
|
||||||
(ID.createID(a._id.user, a._id.clock + a._length)).equals(b._id)
|
|
||||||
) {
|
|
||||||
a._right = b._right
|
|
||||||
if (a instanceof ItemJSON) {
|
|
||||||
a._content = a._content.concat(b._content)
|
|
||||||
} else if (a instanceof ItemString) {
|
|
||||||
a._content += b._content
|
|
||||||
}
|
|
||||||
// delete b later
|
|
||||||
deletes.push(b._id)
|
|
||||||
// do not iterate node!
|
|
||||||
// !(node = next)
|
|
||||||
} else {
|
|
||||||
// not able to merge node, get next node
|
|
||||||
node = next
|
|
||||||
}
|
|
||||||
// update next
|
|
||||||
next = next.next()
|
|
||||||
}
|
|
||||||
for (let i = deletes.length - 1; i >= 0; i--) {
|
|
||||||
os.delete(deletes[i])
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
import { getStruct } from '../utils/structReferences.js'
|
import { getStruct } from '../utils/structReferences.js'
|
||||||
import * as decoding from 'lib0/decoding.js'
|
import * as decoding from 'lib0/decoding.js'
|
||||||
import { GC } from '../structs/GC.js'
|
import { GC } from '../structs/GC.js/index.js.js'
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
import { Item } from '../structs/Item.js' // eslint-disable-line
|
import { Item } from '../structs/AbstractItem.js/index.js' // eslint-disable-line
|
||||||
|
|
||||||
class MissingEntry {
|
class MissingEntry {
|
||||||
constructor (decoder, missing, struct) {
|
constructor (decoder, missing, struct) {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
import { Type } from '../structs/Type.js' // eslint-disable-line
|
import { Type } from '../types/AbstractType.js/index.js.js.js.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if `parent` is a parent of `child`.
|
* Check if `parent` is a parent of `child`.
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ID from './ID.js'
|
import * as ID from './ID.js'
|
||||||
import { GC } from '../structs/GC.js'
|
import { GC } from '../structs/GC.js/index.js.js'
|
||||||
|
|
||||||
// TODO: Implement function to describe ranges
|
// TODO: Implement function to describe ranges
|
||||||
|
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
|
import { DeleteStore } from './DeleteSet'
|
||||||
|
|
||||||
|
export class HistorySnapshot {
|
||||||
|
/**
|
||||||
|
* @param {DeleteStore} ds delete store
|
||||||
|
* @param {Map<number,number>} sm state map
|
||||||
|
* @param {Map<number,string>} userMap
|
||||||
|
*/
|
||||||
|
constructor (ds, sm, userMap) {
|
||||||
|
this.ds = new DeleteStore()
|
||||||
|
this.sm = sm
|
||||||
|
this.userMap = userMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {Item} item
|
* @param {Item} item
|
||||||
* @param {import("../protocols/history").HistorySnapshot} [snapshot]
|
* @param {HistorySnapshot} [snapshot]
|
||||||
*/
|
*/
|
||||||
export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (snapshot.sm.has(item._id.user) && snapshot.sm.get(item._id.user) > item._id.clock && !snapshot.ds.isDeleted(item._id))
|
export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (
|
||||||
|
snapshot.sm.has(item._id.user) && (snapshot.sm.get(item._id.user) || 0) > item._id.clock && !snapshot.ds.isDeleted(item._id)
|
||||||
|
)
|
||||||
|
@ -1,5 +1,30 @@
|
|||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import { getStructReference } from './structReferences.js'
|
||||||
|
import { ID, createID, writeID, writeNullID } from './ID.js'
|
||||||
|
import * as binary from 'lib0/binary.js'
|
||||||
|
|
||||||
export const writeStructToTransaction = (transaction, struct) => {
|
export const writeStructToTransaction = (transaction, struct) => {
|
||||||
transaction.encodedStructsLen++
|
transaction.encodedStructsLen++
|
||||||
struct._toBinary(transaction.encodedStructs)
|
struct._toBinary(transaction.encodedStructs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const structRefs = [
|
||||||
|
ItemBinaryRef
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the next Item in a Decoder and fill this Item with the read data.
|
||||||
|
*
|
||||||
|
* This is called when data is received from a remote peer.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||||
|
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||||
|
* @return {AbstractRef}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const read = decoder => {
|
||||||
|
const info = decoding.readUint8(decoder)
|
||||||
|
return new structRefs[binary.BITS5 & info](decoder, info)
|
||||||
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
import * as ID from '../utils/ID.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Delete all items in an ID-range.
|
|
||||||
* Does not create delete operations!
|
|
||||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup).
|
|
||||||
*/
|
|
||||||
export const deleteItemRange = (y, user, clock, range, gcChildren) => {
|
|
||||||
let item = y.os.getItemCleanStart(ID.createID(user, clock))
|
|
||||||
if (item !== null) {
|
|
||||||
if (!item._deleted) {
|
|
||||||
item._splitAt(y, range)
|
|
||||||
item._delete(y, false, true)
|
|
||||||
}
|
|
||||||
let itemLen = item._length
|
|
||||||
range -= itemLen
|
|
||||||
clock += itemLen
|
|
||||||
if (range > 0) {
|
|
||||||
let node = y.os.findNode(ID.createID(user, clock))
|
|
||||||
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(ID.createID(user, clock))) {
|
|
||||||
const nodeVal = node.val
|
|
||||||
if (!nodeVal._deleted) {
|
|
||||||
nodeVal._splitAt(y, range)
|
|
||||||
nodeVal._delete(y, false, gcChildren)
|
|
||||||
}
|
|
||||||
const nodeLen = nodeVal._length
|
|
||||||
range -= nodeLen
|
|
||||||
clock += nodeLen
|
|
||||||
node = node.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* @module utils
|
|
||||||
*/
|
|
||||||
|
|
||||||
const structs = new Map()
|
|
||||||
const references = new Map()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a new Yjs types. The same type must be defined with the same
|
|
||||||
* reference on all clients!
|
|
||||||
*
|
|
||||||
* @param {Number} reference
|
|
||||||
* @param {Function} structConstructor
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const registerStruct = (reference, structConstructor) => {
|
|
||||||
structs.set(reference, structConstructor)
|
|
||||||
references.set(structConstructor, reference)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export const getStruct = structs.get.bind(structs) // reference => structs.get(reference)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export const getStructReference = references.get.bind(references) // typeConstructor => references.get(typeConstructor)
|
|
@ -7,8 +7,9 @@ import * as array from './y-array.tests.js'
|
|||||||
import * as map from './y-map.tests.js'
|
import * as map from './y-map.tests.js'
|
||||||
import * as text from './y-text.tests.js'
|
import * as text from './y-text.tests.js'
|
||||||
import * as xml from './y-xml.tests.js'
|
import * as xml from './y-xml.tests.js'
|
||||||
|
import * as perf from './perf.js'
|
||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
log.createVConsole(document.body)
|
log.createVConsole(document.body)
|
||||||
}
|
}
|
||||||
runTests({ deleteStore, map, array, text, xml })
|
runTests({ deleteStore, map, array, text, xml, perf })
|
||||||
|
99
tests/perf.js
Normal file
99
tests/perf.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
|
||||||
|
class Item {
|
||||||
|
constructor (c) {
|
||||||
|
this.c = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectsToCreate = 10000000
|
||||||
|
|
||||||
|
export const testItemHoldsAll = tc => {
|
||||||
|
const items = []
|
||||||
|
for (let i = 0; i < objectsToCreate; i++) {
|
||||||
|
switch (i % 3) {
|
||||||
|
case 0:
|
||||||
|
items.push(new Item(i))
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
items.push(new Item(i + ''))
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
items.push(new Item({ x: i }))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const call = []
|
||||||
|
items.forEach(item => {
|
||||||
|
switch (item.c.constructor) {
|
||||||
|
case Number:
|
||||||
|
call.push(item.c + '')
|
||||||
|
break
|
||||||
|
case String:
|
||||||
|
call.push(item.c)
|
||||||
|
break
|
||||||
|
case Object:
|
||||||
|
call.push(item.c.x + '')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
class CItem { }
|
||||||
|
|
||||||
|
class CItemNumber {
|
||||||
|
constructor (i) {
|
||||||
|
this.c = i
|
||||||
|
}
|
||||||
|
toString () {
|
||||||
|
return this.c + ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CItemString {
|
||||||
|
constructor (s) {
|
||||||
|
this.c = s
|
||||||
|
}
|
||||||
|
toString () {
|
||||||
|
return this.c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CItemObject {
|
||||||
|
constructor (o) {
|
||||||
|
this.c = o
|
||||||
|
}
|
||||||
|
toString () {
|
||||||
|
return this.c.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
export const testDifferentItems = tc => {
|
||||||
|
const items = []
|
||||||
|
for (let i = 0; i < objectsToCreate; i++) {
|
||||||
|
switch (i % 3) {
|
||||||
|
case 0:
|
||||||
|
items.push(new CItemNumber(i))
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
items.push(new CItemString(i + ''))
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
items.push(new CItemObject({ x: i }))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const call = []
|
||||||
|
items.forEach(item => {
|
||||||
|
call.push(item.toString())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
/* Strict Type-Checking Options */
|
/* Strict Type-Checking Options */
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
@ -54,7 +54,9 @@
|
|||||||
/* Experimental Options */
|
/* Experimental Options */
|
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
"maxNodeModuleJsDepth": 5
|
"maxNodeModuleJsDepth": 5,
|
||||||
|
"typeRoots": ["./src/utils/typedefs.js"],
|
||||||
|
// "types": ["./src/utils/typedefs.js"]
|
||||||
},
|
},
|
||||||
"files": ["./src/index.js"]
|
"files": ["./src/index.js"]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user