prelim refactor commit

This commit is contained in:
Kevin Jahns
2019-03-26 01:14:15 +01:00
parent 293527e62b
commit d9ab593b07
44 changed files with 2263 additions and 1914 deletions

570
src/structs/AbstractItem.js Normal file
View 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
]
}
}

View 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') }
}

View File

@@ -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}`
}
}

View File

@@ -1,106 +1,69 @@
/**
* @module structs
*/
import { getStructReference } from '../utils/structReferences.js'
import * as ID from '../utils/ID.js'
import { writeStructToTransaction } from '../utils/structEncoding.js'
import { AbstractRef, AbstractStruct } from './AbstractStruct.js'
import { ID, readID, createID, writeID } from '../utils/ID.js' // eslint-disable-line
import * as decoding from 'lib0/decoding.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
export class GC {
constructor () {
export class GC extends AbstractStruct {
/**
* @param {ID} id
* @param {number} length
*/
constructor (id, length) {
super(id)
/**
* @type {ID.ID}
* @type {number}
*/
this._id = null
this._length = 0
}
get _redone () {
return null
}
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)
}
this.length = length
}
/**
* 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
* @param {encoding.Encoder} encoder
*/
_toBinary (encoder) {
encoding.writeUint8(encoder, getStructReference(this.constructor))
this._id.encode(encoder)
encoding.writeVarUint(encoder, this._length)
}
/**
* 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) {
/**
* @type {any}
*/
const id = ID.decode(decoder)
this._id = id
this._length = decoding.readVarUint(decoder)
const missing = []
if (y.ss.getState(id.user) < id.clock) {
missing.push(ID.createID(id.user, id.clock - 1))
}
return missing
}
_splitAt () {
return this
}
_clonePartial (diff) {
const gc = new GC()
gc._id = ID.createID(this._id.user, this._id.clock + diff)
gc._length = this._length - diff
return gc
write (encoder) {
encoding.writeUint8(encoder, structGCRefNumber)
writeID(encoder, this.id)
encoding.writeVarUint(encoder, this.length)
}
}
export class GCRef extends AbstractRef {
/**
* @param {decoding.Decoder} decoder
* @param {number} info
*/
constructor (decoder, info) {
super()
const id = readID(decoder)
if (id === null) {
throw new Error('expected id')
}
/**
* @type {ID}
*/
this.id = id
/**
* @type {number}
*/
this.length = decoding.readVarUint(decoder)
}
missing () {
return [
createID(this.id.client, this.id.clock - 1)
]
}
/**
* @return {GC}
*/
toStruct () {
return new GC(
this.id,
this.length
)
}
}

View File

@@ -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
}
}

View File

@@ -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
import { Item, logItemHelper } from './Item.js'
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
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
export class ItemBinary extends Item {
constructor () {
super()
this._content = null
}
_copy () {
let struct = super._copy()
struct._content = this._content
return struct
}
export const structBinaryRefNumber = 1
export class ItemBinary extends AbstractItem {
/**
* @param {Y} y
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {AbstractItem | null} left
* @param {AbstractItem | null} right
* @param {ItemType | null} parent
* @param {string | null} parentSub
* @param {ArrayBuffer} content
*/
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this._content = decoding.readPayload(decoder)
return missing
constructor (id, left, right, parent, parentSub, content) {
super(id, left, right, parent, parentSub)
this.content = content
}
/**
* @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) {
super._toBinary(encoder)
encoding.writePayload(encoder, this._content)
copy (id, left, right, parent, parentSub) {
return new ItemBinary(id, left, right, parent, parentSub, 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.
*
* @private
*/
_logString () {
logString () {
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
)
}
}

View 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
)
}
}

View File

@@ -2,47 +2,90 @@
* @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 decoding from 'lib0/decoding.js'
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 {
constructor () {
super()
this.embed = null
}
_copy (undeleteChildren, copyPosition) {
let struct = super._copy()
struct.embed = this.embed
return struct
}
get _length () {
return 1
}
export const structEmbedRefNumber = 3
export class ItemEmbed extends AbstractItem {
/**
* @param {Y} y
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {AbstractItem | null} left
* @param {AbstractItem | null} right
* @param {ItemType | null} parent
* @param {string | null} parentSub
* @param {Object} embed
*/
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this.embed = JSON.parse(decoding.readVarString(decoder))
return missing
constructor (id, left, right, parent, parentSub, embed) {
super(id, left, right, parent, parentSub)
this.embed = embed
}
/**
* @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) {
super._toBinary(encoder)
encoding.writeVarString(encoder, JSON.stringify(this.embed))
copy (id, left, right, parent, parentSub) {
return new ItemEmbed(id, left, right, parent, parentSub, 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.
*
* @private
*/
_logString () {
return logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`)
logString () {
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
)
}
}

View File

@@ -2,22 +2,50 @@
* @module structs
*/
import { Item, logItemHelper } from './Item.js'
import { AbstractItem, logItemHelper, AbstractItemRef } from './AbstractItem.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
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 {
constructor () {
super()
this.key = null
this.value = null
export const structFormatRefNumber = 4
export class ItemFormat extends AbstractItem {
/**
* @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()
struct.key = this.key
struct.value = this.value
return struct
/**
* @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 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 () {
return 1
@@ -25,31 +53,43 @@ export class ItemFormat extends Item {
get _countable () {
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
*/
_toBinary (encoder) {
super._toBinary(encoder)
write (encoder) {
super.write(encoder, structFormatRefNumber)
encoding.writeVarString(encoder, this.key)
encoding.writeVarString(encoder, JSON.stringify(this.value))
}
}
export class ItemFormatRef extends AbstractItemRef {
/**
* Transform this YXml Type to a readable format.
* Useful for logging as all Items and Delete implement this method.
*
* @private
* @param {decoding.Decoder} decoder
* @param {number} info
*/
_logString () {
return stringify.logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`)
constructor (decoder, info) {
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
)
}
}

View File

@@ -2,81 +2,109 @@
* @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 decoding from 'lib0/decoding.js'
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 {
constructor () {
super()
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
}
export const structJSONRefNumber = 5
export class ItemJSON extends AbstractItem {
/**
* @param {Y} y
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {AbstractItem | null} left
* @param {AbstractItem | null} right
* @param {ItemType | null} parent
* @param {string | null} parentSub
* @param {Array<any>} content
*/
_fromBinary (y, decoder) {
let missing = super._fromBinary(y, decoder)
let len = decoding.readVarUint(decoder)
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
constructor (id, left, right, parent, parentSub, content) {
super(id, left, right, parent, parentSub)
this.content = content
}
/**
* @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) {
super._toBinary(encoder)
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)
}
copy (id, left, right, parent, parentSub) {
return new ItemJSON(id, left, right, parent, parentSub, 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.
*
* @private
*/
_logString () {
return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
logString () {
return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this.content)}`)
}
_splitAt (y, diff) {
if (diff === 0) {
return this
} else if (diff >= this._length) {
return this._right
get length () {
return this.content.length
}
/**
* @param {number} diff
*/
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))
}
let item = new ItemJSON()
item._content = this._content.splice(diff)
splitHelper(y, this, item, diff)
return item
}
}
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
)
}
}

View File

@@ -2,59 +2,100 @@
* @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 decoding from 'lib0/decoding.js'
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 {
constructor () {
super()
this._content = null
}
_copy () {
let struct = super._copy()
struct._content = this._content
return struct
}
get _length () {
return this._content.length
}
export const structStringRefNumber = 6
export class ItemString extends AbstractItem {
/**
* @param {Y} y
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {AbstractItem | null} left
* @param {AbstractItem | null} right
* @param {ItemType | null} parent
* @param {string | null} parentSub
* @param {string} string
*/
_fromBinary (y, decoder) {
let missing = super._fromBinary(y, decoder)
this._content = decoding.readVarString(decoder)
return missing
constructor (id, left, right, parent, parentSub, string) {
super(id, left, right, parent, parentSub)
this.string = string
}
/**
* @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) {
super._toBinary(encoder)
encoding.writeVarString(encoder, this._content)
copy (id, left, right, parent, parentSub) {
return new ItemString(id, left, right, parent, parentSub, this.string)
}
/**
* 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.
*
* @private
*/
_logString () {
return logItemHelper('ItemString', this, `content:"${this._content}"`)
logString () {
return logItemHelper('ItemString', this, `content:"${this.string}"`)
}
_splitAt (y, diff) {
get length () {
return this.string.length
}
splitAt (y, diff) {
if (diff === 0) {
return this
} else if (diff >= this._length) {
return this._right
} else if (diff >= this.string.length) {
return this.right
}
let item = new ItemString()
item._content = this._content.slice(diff)
this._content = this._content.slice(0, diff)
splitHelper(y, this, item, diff)
return item
/**
* @type {ItemString}
*/
const right = splitItem(this, y, diff)
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
View 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
)
}
}

View File

@@ -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)
}
}
}