mirror of
https://github.com/yjs/yjs.git
synced 2025-12-29 00:25:01 +01:00
large scale refactoring
This commit is contained in:
136
structs/Delete.js
Normal file
136
structs/Delete.js
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { getStructReference } from '../utils/structReferences.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { stringifyID } from '../protocols/syncProtocol.js'
|
||||
import { writeStructToTransaction } from '../utils/Transaction.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import { Item } from './Item.js' // eslint-disable-line
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}
|
||||
*/
|
||||
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: ${stringifyID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
}
|
||||
102
structs/GC.js
Normal file
102
structs/GC.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { getStructReference } from '../utils/structReferences.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { writeStructToTransaction } from '../utils/Transaction.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
// TODO should have the same base class as Item
|
||||
export class GC {
|
||||
constructor () {
|
||||
/**
|
||||
* @type {ID.ID}
|
||||
*/
|
||||
this._id = null
|
||||
this._length = 0
|
||||
}
|
||||
|
||||
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
|
||||
* 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))
|
||||
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
|
||||
}
|
||||
}
|
||||
549
structs/Item.js
Normal file
549
structs/Item.js
Normal file
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { getStructReference } from '../utils/structReferences.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { Delete } from './Delete.js'
|
||||
import { transactionTypeChanged, writeStructToTransaction } from '../utils/Transaction.js'
|
||||
import { GC } from './GC.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js'
|
||||
import { Type } from './Type.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @private
|
||||
* 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
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
this._id = null
|
||||
/**
|
||||
* The item that was originally to the left of this item.
|
||||
* @type {Item}
|
||||
*/
|
||||
this._origin = null
|
||||
/**
|
||||
* The item that is currently to the left of this item.
|
||||
* @type {Item}
|
||||
*/
|
||||
this._left = null
|
||||
/**
|
||||
* The item that is currently to the right of this item.
|
||||
* @type {Item}
|
||||
*/
|
||||
this._right = null
|
||||
/**
|
||||
* The item that was originally to the right of this item.
|
||||
* @type {Item}
|
||||
*/
|
||||
this._right_origin = null
|
||||
/**
|
||||
* The parent type.
|
||||
* @type {Y|Type}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
this._redone = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Item with the same effect as this Item (without position effect)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_copy () {
|
||||
const C = this.constructor
|
||||
return new C()
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the effect of this operation.
|
||||
*
|
||||
* @param {Y} y The Yjs instance.
|
||||
* @param {Array<Item>} redoitems
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_redo (y, redoitems) {
|
||||
if (this._redone !== null) {
|
||||
return this._redone
|
||||
}
|
||||
if (this._parent instanceof Y) {
|
||||
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) {
|
||||
this._deleted = true
|
||||
y.ds.mark(this._id, this._length, false)
|
||||
let del = new Delete()
|
||||
del._targetID = this._id
|
||||
del._length = this._length
|
||||
if (createDelete) {
|
||||
// broadcast and persists Delete
|
||||
del._integrate(y, true)
|
||||
}
|
||||
transactionTypeChanged(y, this._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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
49
structs/ItemEmbed.js
Normal file
49
structs/ItemEmbed.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item } from './Item.js'
|
||||
import { logItemHelper } from '../protocols/syncProtocol.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.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
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.embed = JSON.parse(decoding.readVarString(decoder))
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.embed))
|
||||
}
|
||||
/**
|
||||
* Transform this YXml 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)}`)
|
||||
}
|
||||
}
|
||||
56
structs/ItemFormat.js
Normal file
56
structs/ItemFormat.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item } from './Item.js'
|
||||
import { logItemHelper } from '../protocols/syncProtocol.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
export class ItemFormat extends Item {
|
||||
constructor () {
|
||||
super()
|
||||
this.key = null
|
||||
this.value = null
|
||||
}
|
||||
_copy (undeleteChildren, copyPosition) {
|
||||
let struct = super._copy()
|
||||
struct.key = this.key
|
||||
struct.value = this.value
|
||||
return struct
|
||||
}
|
||||
get _length () {
|
||||
return 1
|
||||
}
|
||||
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)
|
||||
encoding.writeVarString(encoder, this.key)
|
||||
encoding.writeVarString(encoder, JSON.stringify(this.value))
|
||||
}
|
||||
/**
|
||||
* Transform this YXml 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)}`)
|
||||
}
|
||||
}
|
||||
82
structs/ItemJSON.js
Normal file
82
structs/ItemJSON.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item, splitHelper } from './Item.js'
|
||||
import { logItemHelper } from '../protocols/syncProtocol.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.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 () {
|
||||
return this._content.length
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_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
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
let len = this._content.length
|
||||
encoding.writeVarUint(encoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
let encoded
|
||||
let 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.
|
||||
* Useful for logging as all Items and Delete implement this method.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_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
|
||||
}
|
||||
let item = new ItemJSON()
|
||||
item._content = this._content.splice(diff)
|
||||
splitHelper(y, this, item, diff)
|
||||
return item
|
||||
}
|
||||
}
|
||||
61
structs/ItemString.js
Normal file
61
structs/ItemString.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @module structs
|
||||
*/
|
||||
|
||||
import { Item, splitHelper } from './Item.js'
|
||||
import { logItemHelper } from '../protocols/syncProtocol.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.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
|
||||
}
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @param {decoding.Decoder} decoder
|
||||
*/
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
this._content = decoding.readVarString(decoder)
|
||||
return missing
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoding.writeVarString(encoder, this._content)
|
||||
}
|
||||
/**
|
||||
* Transform this YXml 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}"`)
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
return this
|
||||
} else if (diff >= this._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
|
||||
}
|
||||
}
|
||||
266
structs/Type.js
Normal file
266
structs/Type.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* @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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Creates YArray Event and calls observers.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
this._callEventHandler(transaction, new YEvent(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Call event listeners with an event. This will also add an event to all
|
||||
* parents (for `.observeDeep` handlers).
|
||||
*/
|
||||
_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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* 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.
|
||||
*/
|
||||
_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)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* 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
|
||||
*/
|
||||
_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 () {}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* 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.
|
||||
*/
|
||||
_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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user