more type fixes and rethinking writeStructs

This commit is contained in:
Kevin Jahns
2019-04-02 23:08:58 +02:00
parent 73c28952c2
commit e23582b1cd
35 changed files with 952 additions and 695 deletions

View File

@@ -1,6 +1,6 @@
export { Y } from './utils/Y.js'
export { UndoManager } from './utils/UndoManager.js'
// export { UndoManager } from './utils/UndoManager.js'
export { Transaction } from './utils/Transaction.js'
export { ItemJSON } from './structs/ItemJSON.js'
export { ItemString } from './structs/ItemString.js'
@@ -16,8 +16,6 @@ export { YXmlText as XmlText } from './types/YXmlText.js'
export { YXmlHook as XmlHook } from './types/YXmlHook.js'
export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.js'
export { getRelativePosition, fromRelativePosition, equal as equalRelativePosition } from './utils/relativePosition.js'
export { createRelativePosition, createRelativePositionByOffset, createAbsolutePosition, compareRelativePositions, writeRelativePosition, readRelativePosition, AbsolutePosition, RelativePosition } from './utils/relativePosition.js'
export { ID, createID } from './utils/ID.js'
export { integrateRemoteStructs } from './utils/integrateRemoteStructs.js'
export { isParentOf } from './utils/isParentOf.js'

View File

@@ -2,7 +2,7 @@
* @module structs
*/
import { readID, createID, writeID, writeNullID, ID } from '../utils/ID.js' // eslint-disable-line
import { readID, createID, writeID, ID } from '../utils/ID.js' // eslint-disable-line
import { GC } from './GC.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
@@ -16,6 +16,7 @@ import * as binary from 'lib0/binary.js'
import { AbstractRef, AbstractStruct } from './AbstractStruct.js' // eslint-disable-line
import * as error from 'lib0/error.js'
import { replaceStruct, addStruct } from '../utils/StructStore.js'
import { addToDeleteSet } from '../utils/DeleteSet.js'
/**
* Split leftItem into two items
@@ -51,13 +52,7 @@ export const splitItem = (transaction, leftItem, diff) => {
foundOrigins.add(o)
o = o.right
}
const right = leftItem.splitAt(transaction, diff)
if (transaction.added.has(leftItem)) {
transaction.added.add(right)
} else if (transaction.deleted.has(leftItem)) {
transaction.deleted.add(right)
}
return rightItem
return leftItem.splitAt(transaction, diff)
}
/**
@@ -230,7 +225,6 @@ export class AbstractItem extends AbstractStruct {
if (parent !== null) {
maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub)
}
transaction.added.add(this)
// @ts-ignore
if (parent._item.deleted || (left !== null && parentSub !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent
@@ -341,15 +335,11 @@ export class AbstractItem extends AbstractStruct {
/**
* Computes the last content address of this Item.
*
* TODO: do still need this?
* @private
*/
get lastId () {
/**
* @type {any}
*/
const id = this.id
return createID(id.user, id.clock + this.length - 1)
return createID(this.id.client, this.id.clock + this.length - 1)
}
/**
@@ -406,8 +396,8 @@ export class AbstractItem extends AbstractStruct {
parent._length -= this.length
}
this.deleted = true
addToDeleteSet(transaction.deleteSet, this.id, this.length)
maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
transaction.deleted.add(this)
}
}
@@ -437,18 +427,26 @@ export class AbstractItem extends AbstractStruct {
* This is called when this Item is sent to a remote peer.
*
* @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset
* @param {number} encodingRef
* @private
*/
write (encoder, encodingRef) {
write (encoder, offset, 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 (offset === 0) {
writeID(encoder, this.id)
if (this.origin !== null) {
writeID(encoder, this.origin.lastId)
}
} else {
writeID(encoder, createID(this.id.client, this.id.clock + offset))
if (this.origin !== null) {
writeID(encoder, createID(this.id.client, this.id.clock + offset - 1))
}
}
if (this.rightOrigin !== null) {
writeID(encoder, this.rightOrigin.id)
@@ -470,10 +468,10 @@ export class AbstractItem extends AbstractStruct {
if (ykey === null) {
throw error.unexpectedCase()
}
writeNullID(encoder)
encoding.writeVarUint(encoder, 1) // write parentYKey
encoding.writeVarString(encoder, ykey)
} else {
// neither origin nor right is defined
encoding.writeVarUint(encoder, 0) // write parent id
// @ts-ignore _item is defined because parent is integrated
writeID(encoder, parent._item.id)
}
@@ -487,19 +485,11 @@ export class AbstractItem extends AbstractStruct {
export class AbstractItemRef extends AbstractRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super()
const id = readID(decoder)
if (id === null) {
throw error.unexpectedCase()
}
/**
* The uniqe identifier of this type.
* @type {ID}
*/
this.id = id
constructor (decoder, id, info) {
super(id)
/**
* The item that was originally to the left of this item.
* @type {ID | null}
@@ -511,18 +501,19 @@ export class AbstractItemRef extends AbstractRef {
*/
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
const hasParentYKey = decoding.readVarUint(decoder) === 1
/**
* If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
* and we read the next string as parentYKey.
* It indicates how we store/retrieve parent from `y.share`
* @type {string|null}
*/
this.parentYKey = canCopyParentInfo && this.parent === null ? decoding.readVarString(decoder) : null
this.parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
/**
* The parent type.
* @type {ID | null}
*/
this.parent = canCopyParentInfo && !hasParentYKey ? 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
@@ -531,11 +522,22 @@ export class AbstractItemRef extends AbstractRef {
* @type {String | null}
*/
this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
const missing = this._missing
if (this.left !== null) {
missing.push(this.left)
}
if (this.right !== null) {
missing.push(this.right)
}
if (this.parent !== null) {
missing.push(this.parent)
}
}
/**
* @param {Transaction} transaction
* @return {Array<ID|null>}
*/
getMissing () {
getMissing (transaction) {
return [
createID(this.id.client, this.id.clock - 1),
this.left,

View File

@@ -23,22 +23,50 @@ export class AbstractStruct {
get length () {
throw error.methodUnimplemented()
}
/**
* @type {boolean}
*/
get deleted () {
throw error.methodUnimplemented()
}
/**
* @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset
* @param {number} encodingRef
* @private
*/
write (encoder, encodingRef) {
write (encoder, offset, encodingRef) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
*/
integrate (transaction) {
throw error.methodUnimplemented()
}
}
export class AbstractRef {
/**
* @param {ID} id
*/
constructor (id) {
/**
* @type {Array<ID>}
*/
this._missing = []
/**
* The uniqe identifier of this type.
* @type {ID}
*/
this.id = id
}
/**
* @param {Transaction} transaction
* @return {Array<ID|null>}
*/
getMissing () {
return []
getMissing (transaction) {
return this._missing
}
/**
* @param {Transaction} transaction

View File

@@ -28,10 +28,15 @@ export class GC extends AbstractStruct {
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder) {
write (encoder, offset) {
encoding.writeUint8(encoder, structGCRefNumber)
writeID(encoder, this.id)
if (offset === 0) {
writeID(encoder, this.id)
} else {
writeID(encoder, createID(this.id.client, this.id.clock + offset))
}
encoding.writeVarUint(encoder, this.length)
}
}
@@ -39,14 +44,11 @@ export class GC extends AbstractStruct {
export class GCRef extends AbstractRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super()
const id = readID(decoder)
if (id === null) {
throw new Error('expected id')
}
constructor (decoder, id, info) {
super(id)
/**
* @type {ID}
*/

View File

@@ -43,9 +43,10 @@ export class ItemBinary extends AbstractItem {
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder) {
super.write(encoder, structBinaryRefNumber)
write (encoder, offset) {
super.write(encoder, offset, structBinaryRefNumber)
encoding.writePayload(encoder, this.content)
}
}
@@ -53,10 +54,11 @@ export class ItemBinary extends AbstractItem {
export class ItemBinaryRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super(decoder, info)
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {ArrayBuffer}
*/

View File

@@ -8,9 +8,9 @@ import { AbstractItem, 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 { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
export const structDeletedRefNumber = 2
@@ -39,20 +39,22 @@ export class ItemDeleted extends AbstractItem {
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder) {
super.write(encoder, structDeletedRefNumber)
encoding.writeVarUint(encoder, this.length)
write (encoder, offset) {
super.write(encoder, offset, structDeletedRefNumber)
encoding.writeVarUint(encoder, this.length - offset)
}
}
export class ItemDeletedRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super(decoder, info)
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {number}
*/

View File

@@ -39,9 +39,10 @@ export class ItemEmbed extends AbstractItem {
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder) {
super.write(encoder, structEmbedRefNumber)
write (encoder, offset) {
super.write(encoder, offset, structEmbedRefNumber)
encoding.writeVarString(encoder, JSON.stringify(this.embed))
}
}
@@ -49,10 +50,11 @@ export class ItemEmbed extends AbstractItem {
export class ItemEmbedRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super(decoder, info)
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {ArrayBuffer}
*/

View File

@@ -44,9 +44,10 @@ export class ItemFormat extends AbstractItem {
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder) {
super.write(encoder, structFormatRefNumber)
write (encoder, offset) {
super.write(encoder, offset, structFormatRefNumber)
encoding.writeVarString(encoder, this.key)
encoding.writeVarString(encoder, JSON.stringify(this.value))
}
@@ -55,10 +56,11 @@ export class ItemFormat extends AbstractItem {
export class ItemFormatRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super(decoder, info)
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {string}
*/

View File

@@ -58,9 +58,10 @@ export class ItemJSON extends AbstractItem {
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder) {
super.write(encoder, structJSONRefNumber)
write (encoder, offset) {
super.write(encoder, offset, structJSONRefNumber)
const len = this.content.length
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
@@ -73,10 +74,11 @@ export class ItemJSON extends AbstractItem {
export class ItemJSONRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super(decoder, info)
constructor (decoder, id, info) {
super(decoder, id, info)
const len = decoding.readVarUint(decoder)
const cs = []
for (let i = 0; i < len; i++) {

View File

@@ -62,9 +62,10 @@ export class ItemString extends AbstractItem {
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder) {
super.write(encoder, structStringRefNumber)
write (encoder, offset) {
super.write(encoder, offset, structStringRefNumber)
encoding.writeVarString(encoder, this.string)
}
}
@@ -72,10 +73,11 @@ export class ItemString extends AbstractItem {
export class ItemStringRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super(decoder, info)
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {string}
*/

View File

@@ -73,37 +73,41 @@ export class ItemType extends AbstractItem {
return new ItemType(id, left, right, parent, parentSub, this.type._copy())
}
/**
* @param {encoding.Encoder} encoder
* @param {Transaction} transaction
*/
write (encoder) {
super.write(encoder, structTypeRefNumber)
integrate (transaction) {
this.type._integrate(transaction, this)
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
super.write(encoder, offset, 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) {
delete (transaction) {
const y = transaction.y
super.delete(transaction, createDelete, gcChildren)
super.delete(transaction)
transaction.changed.delete(this.type)
transaction.changedParentTypes.delete(this.type)
// delete map types
for (let value of this.type._map.values()) {
if (!value.deleted) {
value.delete(transaction, false, gcChildren)
value.delete(transaction)
}
}
// delete array types
let t = this.type._start
while (t !== null) {
if (!t.deleted) {
t.delete(transaction, false, gcChildren)
t.delete(transaction)
}
t = t.right
}
@@ -133,13 +137,14 @@ export class ItemType extends AbstractItem {
}
}
export class ItemBinaryRef extends AbstractItemRef {
export class ItemTypeRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, info) {
super(decoder, info)
constructor (decoder, id, info) {
super(decoder, id, info)
const typeRef = decoding.readVarUint(decoder)
/**
* @type {AbstractType}

View File

@@ -14,7 +14,8 @@ import { isVisible, Snapshot } from '../utils/Snapshot.js' // eslint-disable-lin
import { ItemJSON } from '../structs/ItemJSON.js'
import { ItemBinary } from '../structs/ItemBinary.js'
import { ID, createID } from '../utils/ID.js' // eslint-disable-line
import { getItemCleanStart } from '../utils/StructStore.js'
import { getItemCleanStart, getItemCleanEnd } from '../utils/StructStore.js'
import * as iterator from 'lib0/iterator.js'
/**
* Abstract Yjs Type class
@@ -203,6 +204,23 @@ export const typeArrayForEach = (type, f) => {
}
}
/**
* @template C,R
* @param {AbstractType} type
* @param {function(C,number,AbstractType):R} f
* @return {Array<R>}
*/
export const typeArrayMap = (type, f) => {
/**
* @type {Array<any>}
*/
const result = []
typeArrayForEach(type, (c, i) => {
result.push(f(c, i, type))
})
return result
}
/**
* @param {AbstractType} type
*/
@@ -351,6 +369,37 @@ export const typeArrayInsertGenerics = (transaction, parent, index, content) =>
throw new Error('Index exceeds array range')
}
/**
* @param {Transaction} transaction
* @param {AbstractType} parent
* @param {number} index
* @param {number} length
*/
export const typeArrayDelete = (transaction, parent, index, length) => {
let n = parent._start
for (; n !== null; n = n.right) {
if (!n.deleted && n.countable) {
if (index <= n.length) {
if (index < n.length) {
n = getItemCleanStart(transaction.y.store, transaction, createID(n.id.client, n.id.clock + index))
}
break
}
index -= n.length
}
}
while (length > 0 && n !== null) {
if (!n.deleted) {
if (length < n.length) {
getItemCleanEnd(transaction.y.store, transaction, createID(n.id.client, n.id.clock + length))
}
n.delete(transaction)
length -= n.length
}
n = n.right
}
}
/**
* @param {Transaction} transaction
* @param {AbstractType} parent
@@ -400,6 +449,23 @@ export const typeMapGet = (parent, key) => {
return val !== undefined && !val.deleted ? val.getContent()[0] : undefined
}
/**
* @param {AbstractType} parent
* @return {Object<string,Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType|undefined>}
*/
export const typeMapGetAll = (parent) => {
/**
* @type {Object<string,any>}
*/
let res = {}
for (const [key, value] of parent._map) {
if (!value.deleted) {
res[key] = value.getContent()[0]
}
}
return res
}
/**
* @param {AbstractType} parent
* @param {string} key
@@ -423,3 +489,9 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => {
}
return v !== null && isVisible(v, snapshot) ? v.getContent()[0] : undefined
}
/**
* @param {Map<string,AbstractItem>} map
* @return {Iterator<[string,AbstractItem]>}
*/
export const createMapIterator = map => iterator.iteratorFilter(map.entries(), entry => !entry[1].deleted)

View File

@@ -4,7 +4,7 @@
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
import { AbstractType, typeArrayGet, typeArrayToArray, typeArrayForEach, typeArrayCreateIterator, typeArrayInsertGenerics, typeArrayDelete } from './AbstractType.js'
import { AbstractType, typeArrayGet, typeArrayToArray, typeArrayForEach, typeArrayCreateIterator, typeArrayInsertGenerics, typeArrayDelete, typeArrayMap } from './AbstractType.js'
import { YEvent } from '../utils/YEvent.js'
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
@@ -140,20 +140,14 @@ export class YArray extends AbstractType {
* Returns an Array with the result of calling a provided function on every
* element of this YArray.
*
* @template M
* @template T,M
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
* @return {Array<M>} A new array with each element being the result of the
* callback function
*/
map (f) {
/**
* @type {Array<M>}
*/
const result = []
this.forEach((c, i) => {
result.push(f(c, i, this))
})
return result
// @ts-ignore
return typeArrayMap(this, f)
}
/**

View File

@@ -2,36 +2,13 @@
* @module types
*/
import { AbstractType, typeMapDelete, typeMapSet, typeMapGet, typeMapHas } from './AbstractType.js'
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
import { AbstractType, typeMapDelete, typeMapSet, typeMapGet, typeMapHas, createMapIterator } from './AbstractType.js'
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
import { YEvent } from '../utils/YEvent.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
class YMapIterator {
/**
* @param {Array<any>} vals
*/
constructor (vals) {
this.vals = vals
this.i = 0
}
[Symbol.iterator] () {
return this
}
next () {
let value
let done = true
if (this.i < this.vals.length) {
value = this.vals[this.i]
done = false
}
return {
value,
done
}
}
}
import * as iterator from 'lib0/iterator.js'
/**
* Event that describes the changes on a YMap.
@@ -109,26 +86,18 @@ export class YMap extends AbstractType {
/**
* Returns the keys for each element in the YMap Type.
*
* @return {YMapIterator}
* @return {Iterator<string>}
*/
keys () {
const keys = []
for (let [key, value] of this._map) {
if (value.deleted) {
keys.push(key)
}
}
return new YMapIterator(keys)
return iterator.iteratorMap(createMapIterator(this._map), v => v[0])
}
/**
* Returns the value for each element in the YMap Type.
*
* @return {Iterator<string|number|ArrayBuffer|Object<string,any>|Array<any>>}
*/
entries () {
const entries = []
for (let [key, value] of this._map) {
if (value.deleted) {
entries.push([key, value.getContent()[0]])
}
}
return new YMapIterator(entries)
return iterator.iteratorMap(createMapIterator(this._map), v => v[1].getContent()[0])
}
[Symbol.iterator] () {

View File

@@ -7,8 +7,12 @@ import { YMap } from './YMap.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 { YArray } from './YArray.js'
import { YXmlEvent } from './YXmlEvent.js'
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
import { YXmlText } from './YXmlText.js' // eslint-disable-line
import { YXmlHook } from './YXmlHook.js' // eslint-disable-line
import { AbstractType, typeArrayMap, typeArrayForEach, typeMapGet, typeMapGetAll } from './AbstractType.js'
import { Snapshot } from '../utils/Snapshot.js' // eslint-disable-line
/**
* Define the elements to which a set of CSS queries apply.
@@ -42,16 +46,16 @@ import { YXmlEvent } from './YXmlEvent.js'
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function} f
* @param {function(AbstractType):boolean} f
*/
constructor (root, f) {
this._filter = f || (() => true)
this._root = root
/**
* @type {YXmlFragment | YXmlElement}
* @type {ItemType | null}
*/
this._currentNode = root
this._firstCall = true
// @ts-ignore
this._currentNode = root._start
}
[Symbol.iterator] () {
return this
@@ -59,45 +63,40 @@ export class YXmlTreeWalker {
/**
* Get the next node.
*
* @return {YXmlElement} The next node.
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
let n = this._currentNode
if (this._firstCall) {
this._firstCall = false
if (!n._deleted && this._filter(n)) {
return { value: n, done: false }
}
if (n === null) {
// @ts-ignore return undefined if done=true (the expected result)
return { value: undefined, done: true }
}
const nextValue = n
do {
if (!n._deleted && (n.constructor === YXmlElement || n.constructor === YXmlFragment) && n._start !== null) {
if (!n.deleted && (n.constructor === YXmlElement || n.constructor === YXmlFragment) && n.type._start !== null) {
// walk down in the tree
n = n._start
// @ts-ignore
n = n.type._start
} else {
// walk right or up in the tree
while (n !== this._root) {
if (n._right !== null) {
n = n._right
while (n !== null) {
if (n.right !== null) {
// @ts-ignore
n = n.right
break
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
}
n = n._parent
}
if (n === this._root) {
n = null
}
}
if (n === this._root) {
break
}
} while (n !== null && (n._deleted || !this._filter(n)))
} while (n !== null && (n.deleted || !this._filter(n.type)))
this._currentNode = n
if (n === null) {
return { done: true }
} else {
return { value: n, done: false }
}
// @ts-ignore
return { value: nextValue.type, done: false }
}
}
@@ -109,7 +108,7 @@ export class YXmlTreeWalker {
*
* @public
*/
export class YXmlFragment extends YArray {
export class YXmlFragment extends AbstractType {
/**
* Create a subtree of childNodes.
*
@@ -120,7 +119,7 @@ export class YXmlFragment extends YArray {
* nop(node)
* }
*
* @param {Function} filter Function that is called on each child element and
* @param {function(AbstractType):boolean} filter Function that is called on each child element and
* returns a Boolean indicating whether the child
* is to be included in the subtree.
* @return {YXmlTreeWalker} A subtree and a position within it.
@@ -142,12 +141,13 @@ export class YXmlFragment extends YArray {
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement} The first element that matches the query or null.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
const next = iterator.next()
if (next.done) {
@@ -164,12 +164,13 @@ export class YXmlFragment extends YArray {
* TODO: Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement>} The elements that match this query.
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
}
@@ -194,7 +195,7 @@ export class YXmlFragment extends YArray {
* @return {string} The string representation of all children.
*/
toDomString () {
return this.map(xml => xml.toDomString()).join('')
return typeArrayMap(this, xml => xml.toDomString()).join('')
}
/**
@@ -205,10 +206,10 @@ export class YXmlFragment extends YArray {
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {DomBinding} [binding] You should not set this property. This is
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {DocumentFragment} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
@@ -217,20 +218,11 @@ export class YXmlFragment extends YArray {
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
this.forEach(xmlType => {
typeArrayForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null)
})
return fragment
}
/**
* Transform this YXml Type to a readable format.
* Useful for logging as all Items and Delete implement this method.
*
* @private
*/
_logString () {
return logItemHelper('YXml', this)
}
}
/**
@@ -249,27 +241,11 @@ export class YXmlElement extends YXmlFragment {
/**
* Creates an Item with the same effect as this Item (without position effect)
*
* @return {YXmlElement}
* @private
*/
_copy () {
let struct = super._copy()
struct.nodeName = this.nodeName
return struct
}
/**
* 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.
*
* @private
* @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) {
const missing = super._fromBinary(y, decoder)
this.nodeName = decoding.readVarString(decoder)
return missing
return new YXmlElement(this.nodeName)
}
/**
@@ -281,31 +257,10 @@ export class YXmlElement extends YXmlFragment {
* @private
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_toBinary (encoder) {
super._toBinary(encoder)
_write (encoder) {
encoding.writeVarString(encoder, this.nodeName)
}
/**
* 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.
*
* * Checks for nodeName
* * Sets domFilter
*
* @private
* @param {Transaction} transaction The Yjs instance
*/
_integrate (transaction) {
if (this.nodeName === null) {
throw new Error('nodeName must be defined!')
}
super._integrate(transaction)
}
toString () {
return this.toDomString()
}
@@ -365,37 +320,25 @@ export class YXmlElement extends YXmlFragment {
*
* @param {String} attributeName The attribute name that identifies the
* queried value.
* @param {HistorySnapshot} [snapshot]
* @return {String} The queried attribute value.
*
* @public
*/
getAttribute (attributeName, snapshot) {
return YMap.prototype.get.call(this, attributeName, snapshot)
getAttribute (attributeName) {
// @ts-ignore
return typeMapGet(this, attributeName)
}
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @param {HistorySnapshot} [snapshot]
* @param {Snapshot} [snapshot]
* @return {Object} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes (snapshot) {
const obj = {}
if (snapshot === undefined) {
for (let [key, value] of this._map) {
if (!value._deleted) {
obj[key] = value._content[0]
}
}
} else {
YMap.prototype.keys.call(this, snapshot).forEach(key => {
obj[key] = YMap.prototype.get.call(this, key, snapshot)
})
}
return obj
return typeMapGetAll(this)
}
// TODO: outsource the binding property.
/**
@@ -406,10 +349,10 @@ export class YXmlElement extends YXmlFragment {
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {DomBinding} [binding] You should not set this property. This is
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
@@ -419,7 +362,7 @@ export class YXmlElement extends YXmlFragment {
for (let key in attrs) {
dom.setAttribute(key, attrs[key])
}
this.forEach(yxml => {
typeArrayForEach(this, yxml => {
dom.appendChild(yxml.toDom(_document, hooks, binding))
})
if (binding !== undefined) {
@@ -429,5 +372,13 @@ export class YXmlElement extends YXmlFragment {
}
}
/**
* @param {decoding.Decoder} decoder
* @return {YXmlElement}
*/
export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder))
export const readYXmlFragment = decoder => new YXmlFragment()
/**
* @param {decoding.Decoder} decoder
* @return {YXmlFragment}
*/
export const readYXmlFragment = decoder => new YXmlFragment()

View File

@@ -17,11 +17,10 @@ export class YXmlEvent extends YEvent {
* @param {AbstractType} target The target on which the event is created.
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
* child list changed.
* @param {Boolean} remote Whether this change was created by a remote peer.
* @param {Transaction} transaction The transaction instance with wich the
* change was created.
*/
constructor (target, subs, remote, transaction) {
constructor (target, subs, transaction) {
super(target)
/**
* The transaction instance for the computed change.
@@ -38,11 +37,6 @@ export class YXmlEvent extends YEvent {
* @type {Set<string|null>}
*/
this.attributesChanged = new Set()
/**
* Whether this change was created by a remote peer.
* @type {Boolean}
*/
this.remote = remote
subs.forEach((sub) => {
if (sub === null) {
this.childListChanged = true

View File

@@ -1,6 +1,7 @@
import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as math from 'lib0/math.js'
import { StructStore, getItemRange } from './StructStore.js' // eslint-disable-line
import { Transaction } from './Transaction.js' // eslint-disable-line
import { ID } from './ID.js' // eslint-disable-line
@@ -39,13 +40,38 @@ export class DeleteSet {
}
}
/**
* @param {Array<DeleteItem>} dis
* @param {number} clock
* @return {number|null}
*/
export const findIndexSS = (dis, clock) => {
let left = 0
let right = dis.length
while (left <= right) {
const midindex = math.floor((left + right) / 2)
const mid = dis[midindex]
const midclock = mid.clock
if (midclock <= clock) {
if (clock < midclock + mid.len) {
return midindex
}
left = midindex
} else {
right = midindex
}
}
return null
}
/**
* @param {DeleteSet} ds
* @param {ID} id
* @return {boolean}
*/
export const isDeleted = (ds, id) => {
const dis = ds.clients.get(id.client)
return dis !== undefined && findIndexSS(dis, id.clock) !== null
}
/**
@@ -75,15 +101,12 @@ export const sortAndMergeDeleteSet = ds => {
}
/**
* @param {Transaction} transaction
* @param {DeleteSet} ds
* @param {ID} id
* @param {number} length
*/
export const createDeleteSetFromTransaction = transaction => {
const ds = new DeleteSet()
transaction.deleted.forEach(item => {
map.setIfUndefined(ds.clients, item.id.client, () => []).push(new DeleteItem(item.id.clock, item.length))
})
sortAndMergeDeleteSet(ds)
return ds
export const addToDeleteSet = (ds, id, length) => {
map.setIfUndefined(ds.clients, id.client, () => []).push(new DeleteItem(id.clock, length))
}
/**

View File

@@ -4,6 +4,8 @@
import * as decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js'
import * as error from 'lib0/error.js'
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
export class ID {
/**
@@ -46,14 +48,19 @@ export class ID {
}
}
/**
* @param {ID} a
* @param {ID} b
* @return {boolean}
*/
export const compareIDs = (a, b) => a === b || (a !== null && b !== null && a.client === b.client && a.clock === b.clock)
/**
* @param {number} client
* @param {number} clock
*/
export const createID = (client, clock) => new ID(client, clock)
const isNullID = 0xFFFFFF
/**
* @param {encoding.Encoder} encoder
* @param {ID} id
@@ -63,21 +70,31 @@ export const writeID = (encoder, id) => {
encoding.writeVarUint(encoder, id.clock)
}
/**
* @param {encoding.Encoder} encoder
*/
export const writeNullID = (encoder) =>
encoding.writeVarUint(encoder, isNullID)
/**
* Read ID.
* * If first varUint read is 0xFFFFFF a RootID is returned.
* * Otherwise an ID is returned
*
* @param {decoding.Decoder} decoder
* @return {ID | null}
* @return {ID}
*/
export const readID = decoder => {
const client = decoding.readVarUint(decoder)
return client === isNullID ? null : createID(client, decoding.readVarUint(decoder))
export const readID = decoder =>
createID(decoding.readVarUint(decoder), decoding.readVarUint(decoder))
/**
* The top types are mapped from y.share.get(keyname) => type.
* `type` does not store any information about the `keyname`.
* This function finds the correct `keyname` for `type` and throws otherwise.
*
* @param {AbstractType} type
* @return {string}
*/
export const findRootTypeKey = type => {
// @ts-ignore _y must be defined, otherwise unexpected case
for (let [key, value] of type._y.share) {
if (value === type) {
return key
}
}
throw error.unexpectedCase()
}

View File

@@ -18,6 +18,6 @@ export class Snapshot {
* @param {AbstractItem} item
* @param {Snapshot} [snapshot]
*/
export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (
snapshot.sm.has(item.id.client) && (snapshot.sm.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item._id)
export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : (
snapshot.sm.has(item.id.client) && (snapshot.sm.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
)

View File

@@ -5,6 +5,7 @@ import { ID } from './ID.js' // eslint-disable-line
import { Transaction } from './Transaction.js' // eslint-disable-line
import * as map from 'lib0/map.js'
import * as math from 'lib0/math.js'
import * as error from 'lib0/error.js'
export class StructStore {
constructor () {
@@ -68,13 +69,13 @@ export const addStruct = (store, struct) => {
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* @param {Array<AbstractStruct>} structs // ordered structs without holes
* Perform a binary search on a sorted array
* @param {Array<any>} structs
* @param {number} clock
* @return {number}
* @private
*/
export const findIndex = (structs, clock) => {
export const findIndexSS = (structs, clock) => {
let left = 0
let right = structs.length
while (left <= right) {
@@ -90,7 +91,7 @@ export const findIndex = (structs, clock) => {
right = midindex
}
}
throw new Error('ID does not exist')
throw error.unexpectedCase()
}
/**
@@ -101,13 +102,13 @@ export const findIndex = (structs, clock) => {
* @return {AbstractStruct}
* @private
*/
const find = (store, id) => {
export const find = (store, id) => {
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(id.client)
return structs[findIndex(structs, id.clock)]
return structs[findIndexSS(structs, id.clock)]
}
/**
@@ -135,14 +136,13 @@ export const getItemCleanStart = (store, transaction, id) => {
*/
// @ts-ignore
const structs = store.clients.get(id.client)
const index = findIndex(structs, id.clock)
const index = findIndexSS(structs, id.clock)
/**
* @type {AbstractItem}
*/
let struct = structs[index]
if (struct.id.clock < id.clock) {
struct.splitAt()
struct = splitStruct(transaction, struct, id.clock - struct.id.clock)
struct = struct.splitAt(transaction, id.clock - struct.id.clock)
structs.splice(index, 0, struct)
}
return struct
@@ -163,10 +163,10 @@ export const getItemCleanEnd = (store, transaction, id) => {
*/
// @ts-ignore
const structs = store.clients.get(id.client)
const index = findIndex(structs, id.clock)
const index = findIndexSS(structs, id.clock)
const struct = structs[index]
if (id.clock !== struct.id.clock + struct.length - 1) {
structs.splice(index, 0, splitStruct(transaction, struct, id.clock - struct.id.clock + 1))
structs.splice(index, 0, struct.splitAt(transaction, id.clock - struct.id.clock + 1))
}
return struct
}
@@ -188,11 +188,11 @@ export const getItemRange = (store, transaction, client, clock, len) => {
*/
// @ts-ignore
const structs = store.clients.get(client)
let index = findIndex(structs, clock)
let index = findIndexSS(structs, clock)
let struct = structs[index]
let range = []
if (struct.id.clock < clock) {
struct = splitStruct(transaction, struct, clock - struct.id.clock)
struct = struct.splitAt(transaction, clock - struct.id.clock)
structs.splice(index, 0, struct)
}
while (struct.id.clock + struct.length <= clock + len) {
@@ -200,7 +200,7 @@ export const getItemRange = (store, transaction, client, clock, len) => {
struct = structs[++index]
}
if (clock < struct.id.clock + struct.length) {
structs.splice(index, 0, splitStruct(transaction, struct, clock + len - struct.id.clock))
structs.splice(index, 0, struct.splitAt(transaction, clock + len - struct.id.clock))
range.push(struct)
}
return range
@@ -218,5 +218,12 @@ export const replaceStruct = (store, struct, newStruct) => {
*/
// @ts-ignore
const structs = store.clients.get(struct.id.client)
structs[findIndex(structs, struct.id.clock)] = newStruct
structs[findIndexSS(structs, struct.id.clock)] = newStruct
}
/**
* @param {StructStore} store
* @param {ID} id
* @return {boolean}
*/
export const exists = (store, id) => id.clock < getState(store, id.client)

View File

@@ -10,7 +10,7 @@ import { YEvent } from './YEvent.js' // eslint-disable-line
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
import { writeStructsFromTransaction } from './structEncoding.js'
import { createID } from './ID.js' // eslint-disable-line
import { createDeleteSetFromTransaction, writeDeleteSet } from './DeleteSet.js'
import { writeDeleteSet, DeleteSet, sortAndMergeDeleteSet } from './DeleteSet.js'
import { getState } from './StructStore.js'
/**
@@ -46,15 +46,10 @@ export class Transaction {
*/
this.y = y
/**
* All new items that are added during a transaction.
* @type {Set<AbstractItem>}
* Describes the set of deleted items by ids
* @type {DeleteSet}
*/
this.added = new Set()
/**
* Set of all deleted items
* @type {Set<AbstractItem>}
*/
this.deleted = new Set()
this.deleteSet = new DeleteSet()
/**
* If a state was modified, the original value is saved here.
* Use `stateUpdates` to compute the original state before the transaction,
@@ -87,7 +82,8 @@ export class Transaction {
if (this._updateMessage === null) {
const encoder = encoding.createEncoder()
writeStructsFromTransaction(encoder, this)
writeDeleteSet(encoder, createDeleteSetFromTransaction(this))
sortAndMergeDeleteSet(this.deleteSet)
writeDeleteSet(encoder, this.deleteSet)
this._updateMessage = encoder
}
return this._updateMessage

View File

@@ -71,20 +71,20 @@ export class Y extends Observable {
console.error(e)
}
if (initialCall) {
this.emit('beforeObserverCalls', [this, this._transaction, remote])
const transaction = this._transaction
this._transaction = null
// emit change events on changed types
transaction.changed.forEach((subs, itemtype) => {
if (!itemtype._item.deleted) {
itemtype.type._callObserver(transaction, subs, remote)
}
})
transaction.changedParentTypes.forEach((events, type) => {
if (!type._deleted) {
// only call event listeners / observers if anything changed
const transactionChangedContent = transaction.changedParentTypes.size !== 0
if (transactionChangedContent) {
this.emit('beforeObserverCalls', [this, this._transaction, remote])
// emit change events on changed types
transaction.changed.forEach((subs, itemtype) => {
itemtype._callObserver(transaction, subs)
})
transaction.changedParentTypes.forEach((events, type) => {
events = events
.filter(event =>
!event.target._deleted
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
@@ -92,11 +92,15 @@ export class Y extends Observable {
})
// we don't have to check for events.length
// because there is no way events is empty..
type.type._deepEventHandler.callEventListeners(transaction, events)
}
})
// when all changes & events are processed, emit afterTransaction event
this.emit('afterTransaction', [this, transaction, remote])
type._deepEventHandler.callEventListeners(transaction, events)
})
// when all changes & events are processed, emit afterTransaction event
this.emit('afterTransaction', [this, transaction, remote])
// transaction cleanup
// todo: replace deleted items with ItemDeleted
// todo: replace items with deleted parent with ItemGC
// todo: on all affected store.clients props, try to merge
}
}
}
/**
@@ -120,6 +124,7 @@ export class Y extends Observable {
* }
*
* @TODO: implement getText, getArray, ..
* @TODO: Decide wether to use define() or get() and then use it consistently
*
* @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition
@@ -127,7 +132,7 @@ export class Y extends Observable {
*/
get (name, TypeConstructor = AbstractType) {
// @ts-ignore
const type = map.setTfUndefined(this.share, name, () => new TypeConstructor())
const type = map.setIfUndefined(this.share, name, () => new TypeConstructor())
const Constr = type.constructor
if (Constr !== TypeConstructor) {
if (Constr === AbstractType) {

View File

@@ -1,112 +0,0 @@
/**
* @module utils
*/
import * as decoding from 'lib0/decoding.js'
import { GC } from '../structs/GC.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
class MissingEntry {
constructor (decoder, missing, struct) {
this.decoder = decoder
this.missing = missing.length
this.struct = struct
}
}
/**
* @private
* Integrate remote struct
* When a remote struct is integrated, other structs might be ready to ready to
* integrate.
* @param {Y} y
* @param {Item} struct
*/
function _integrateRemoteStructHelper (y, struct) {
const id = struct._id
if (id === undefined) {
struct._integrate(y)
} else {
if (y.ss.getState(id.user) > id.clock) {
return
}
if (!y.gcEnabled || struct.constructor === GC || (struct._parent.constructor !== GC && struct._parent._deleted === false)) {
// Is either a GC or Item with an undeleted parent
// save to integrate
struct._integrate(y)
} else {
// Is an Item. parent was deleted.
struct._gc(y)
}
let msu = y._missingStructs.get(id.user)
if (msu != null) {
let clock = id.clock
const finalClock = clock + struct._length
for (;clock < finalClock; clock++) {
const missingStructs = msu.get(clock)
if (missingStructs !== undefined) {
missingStructs.forEach(missingDef => {
missingDef.missing--
if (missingDef.missing === 0) {
y._readyToIntegrate.push(missingDef)
}
})
msu.delete(clock)
}
}
if (msu.size === 0) {
y._missingStructs.delete(id.user)
}
}
}
}
/**
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const integrateRemoteStructs = (decoder, y) => {
const len = decoding.readUint32(decoder)
for (let i = 0; i < len; i++) {
let reference = decoding.readVarUint(decoder)
let Constr = getStruct(reference)
let struct = new Constr()
let decoderPos = decoder.pos
let missing = struct._fromBinary(y, decoder)
if (missing.length === 0) {
while (struct !== null) {
_integrateRemoteStructHelper(y, struct)
struct = null
if (y._readyToIntegrate.length > 0) {
const missingDef = y._readyToIntegrate.shift()
const decoder = missingDef.decoder
let oldPos = decoder.pos
let missing = missingDef.struct._fromBinary(y, decoder)
decoder.pos = oldPos
if (missing.length === 0) {
struct = missingDef.struct
} else {
throw new Error('Missing should be empty')
}
}
}
} else {
let _decoder = decoding.createDecoder(decoder.arr.buffer)
_decoder.pos = decoderPos
let missingEntry = new MissingEntry(_decoder, missing, struct)
let missingStructs = y._missingStructs
for (let i = missing.length - 1; i >= 0; i--) {
let m = missing[i]
if (!missingStructs.has(m.user)) {
missingStructs.set(m.user, new Map())
}
let msu = missingStructs.get(m.user)
if (!msu.has(m.clock)) {
msu.set(m.clock, [])
}
let mArray = msu = msu.get(m.clock)
mArray.push(missingEntry)
}
}
}
}

View File

@@ -3,9 +3,13 @@
*/
import * as ID from './ID.js'
import { GC } from '../structs/GC.js'
// TODO: Implement function to describe ranges
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as error from 'lib0/error.js'
import { find, exists, getItemType, StructStore } from './StructStore.js' // eslint-disable-line
import { Y } from './Y.js' // eslint-disable-line
/**
* A relative position that is based on the Yjs model. In contrast to an
@@ -18,9 +22,7 @@ import { GC } from '../structs/GC.js'
* {@link getRelativePosition} and it can be transformed to an absolute position
* with {@link fromRelativePosition}.
*
* Pro tip: Use this to implement shared cursor locations in YText or YXml!
* The relative position is {@link encodable}, so you can send it to other
* clients.
* One of the properties must be defined.
*
* @example
* // Current cursor position is at position 10
@@ -33,98 +35,220 @@ import { GC } from '../structs/GC.js'
* absolutePosition.type // => yText
* console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3
*
* @typedef {encodable} RelativePosition
*/
export class RelativePosition {
/**
* @param {ID.ID|null} type
* @param {string|null} tname
* @param {ID.ID|null} item
*/
constructor (type, tname, item) {
/**
* @type {ID.ID|null}
*/
this.type = type
/**
* @type {string|null}
*/
this.tname = tname
/**
* @type {ID.ID | null}
*/
this.item = item
}
}
export class AbsolutePosition {
/**
* @param {AbstractType} type
* @param {number} offset
*/
constructor (type, offset) {
/**
* @type {AbstractType}
*/
this.type = type
/**
* @type {number}
*/
this.offset = offset
}
}
/**
* @param {AbstractType} type
* @param {number} offset
*/
export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset)
/**
* @param {AbstractType} type
* @param {ID.ID|null} item
*/
export const createRelativePosition = (type, item) => {
let typeid = null
let tname = null
if (type._item === null) {
tname = ID.findRootTypeKey(type)
} else {
typeid = type._item.id
}
return new RelativePosition(typeid, tname, item)
}
/**
* Create a relativePosition based on a absolute position.
*
* @param {YType} type The base type (e.g. YText or YArray).
* @param {Integer} offset The absolute position.
* @param {AbstractType} type The base type (e.g. YText or YArray).
* @param {number} offset The absolute position.
* @return {RelativePosition}
*/
export const getRelativePosition = (type, offset) => {
// TODO: rename to createRelativePosition
export const createRelativePositionByOffset = (type, offset) => {
let t = type._start
while (t !== null) {
if (!t._deleted && t._countable) {
if (t._length > offset) {
return [t._id.user, t._id.clock + offset]
if (!t.deleted && t.countable) {
if (t.length > offset) {
// case 1: found position somewhere in the linked list
return createRelativePosition(type, ID.createID(t.id.client, t.id.clock + offset))
}
offset -= t._length
offset -= t.length
}
t = t._right
t = t.right
}
return ['endof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null]
return createRelativePosition(type, null)
}
/**
* @typedef {Object} AbsolutePosition The result of {@link fromRelativePosition}
* @property {YType} type The type on which to apply the absolute position.
* @property {number} offset The absolute offset.r
* @param {encoding.Encoder} encoder
* @param {RelativePosition} rpos
*/
export const writeRelativePosition = (encoder, rpos) => {
const { type, tname, item } = rpos
if (item !== null) {
encoding.writeVarUint(encoder, 0)
ID.writeID(encoder, item)
} else if (tname !== null) {
// case 2: found position at the end of the list and type is stored in y.share
encoding.writeUint8(encoder, 1)
encoding.writeVarString(encoder, tname)
} else if (type !== null) {
// case 3: found position at the end of the list and type is attached to an item
encoding.writeUint8(encoder, 2)
ID.writeID(encoder, type)
} else {
throw error.unexpectedCase()
}
return encoder
}
/**
* Transforms a relative position back to a relative position.
*
* @param {Y} y The Yjs instance in which to query for the absolute position.
* @param {RelativePosition} rpos The relative position.
* @return {AbsolutePosition} The absolute position in the Yjs model
* (type + offset).
* @param {decoding.Decoder} decoder
* @param {Y} y
* @param {StructStore} store
* @return {RelativePosition|null}
*/
export const fromRelativePosition = (y, rpos) => {
if (rpos === null) {
export const readRelativePosition = (decoder, y, store) => {
let type = null
let tname = null
let itemID = null
switch (decoding.readVarUint(decoder)) {
case 0:
// case 1: found position somewhere in the linked list
itemID = ID.readID(decoder)
break
case 1:
// case 2: found position at the end of the list and type is stored in y.share
tname = decoding.readVarString(decoder)
break
case 2: {
// case 3: found position at the end of the list and type is attached to an item
type = ID.readID(decoder)
}
}
return new RelativePosition(type, tname, itemID)
}
/**
* @param {RelativePosition} rpos
* @param {StructStore} store
* @param {Y} y
* @return {AbsolutePosition|null}
*/
export const toAbsolutePosition = (rpos, store, y) => {
const rightID = rpos.item
const typeID = rpos.type
const tname = rpos.tname
let type = null
let offset = 0
if (rightID !== null) {
if (!exists(store, rightID)) {
return null
}
const right = find(store, rightID)
if (!(right instanceof AbstractItem)) {
return null
}
offset = right.deleted ? 0 : rightID.clock - right.id.clock
let n = right.left
while (n !== null) {
if (!n.deleted && n.countable) {
offset += n.length
}
n = n.left
}
type = right.parent
} else {
if (tname !== null) {
type = y.get(tname)
} else if (typeID !== null) {
type = getItemType(store, typeID).type
} else {
throw error.unexpectedCase()
}
offset = type._length
}
if (type._item !== null && type._item.deleted) {
return null
}
if (rpos[0] === 'endof') {
let id
if (rpos[3] === null) {
id = ID.createID(rpos[1], rpos[2])
} else {
id = ID.createRootID(rpos[3], rpos[4])
}
let type = y.os.get(id)
if (type === null) {
return null
}
while (type._redone !== null) {
type = type._redone
}
if (type === null || type.constructor === GC) {
return null
}
return {
type,
offset: type.length
}
} else {
let offset = 0
let struct = y.os.findNodeWithUpperBound(ID.createID(rpos[0], rpos[1])).val
if (struct === null || struct._id.user === ID.RootFakeUserID) {
return null // TODO: support fake ids?
}
const diff = rpos[1] - struct._id.clock
while (struct._redone !== null) {
struct = struct._redone
}
const parent = struct._parent
if (struct.constructor === GC || parent._deleted) {
return null
}
if (!struct._deleted && struct._countable) {
offset = diff
}
struct = struct._left
while (struct !== null) {
if (!struct._deleted && struct._countable) {
offset += struct._length
}
struct = struct._left
}
return {
type: parent,
offset: offset
}
}
return createAbsolutePosition(type, offset)
}
export const equal = (posa, posb) => posa === posb || (posa !== null && posb !== null && posa.length === posb.length && posa.every((v, i) => v === posb[i]))
/**
* Transforms an absolute to a relative position.
*
* @param {AbsolutePosition} apos The absolute position.
* @param {Y} y The Yjs instance in which to query for the absolute position.
* @return {RelativePosition} The absolute position in the Yjs model
* (type + offset).
*/
export const toRelativePosition = (apos, y) => {
const type = apos.type
if (type._length === apos.offset) {
return createRelativePosition(type, null)
} else {
let offset = apos.offset
let n = type._start
while (n !== null) {
if (!n.deleted && n.countable) {
if (n.length > offset) {
return createRelativePosition(type, ID.createID(n.id.client, n.id.clock + offset))
}
offset -= n.length
}
n = n.right
}
}
throw error.unexpectedCase()
}
/**
* @param {RelativePosition|null} a
* @param {RelativePosition|null} b
*/
export const compareRelativePositions = (a, b) => a === b || (
a !== null && b !== null && (
(a.item !== null && b.item !== null && ID.compareIDs(a.item, b.item)) ||
(a.tname !== null && a.tname === b.tname) ||
(a.type !== null && b.type !== null && ID.compareIDs(a.type, b.type))
)
)

View File

@@ -1,45 +1,147 @@
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js'
import * as map from 'lib0/map.js'
import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' // eslint-disable-line
import * as binary from 'lib0/binary.js'
import { Transaction } from './Transaction.js'
import { findIndex } from './StructStore.js'
import { Transaction } from './Transaction.js' // eslint-disable-line
import { findIndexSS, exists, StructStore } from './StructStore.js' // eslint-disable-line
import { writeID, createID, readID, ID } from './ID.js' // eslint-disable-line
import * as iterator from 'lib0/iterator.js'
import { ItemBinaryRef } from '../structs/ItemBinary.js'
import { GCRef } from '../structs/GC.js'
import { ItemDeletedRef } from '../structs/ItemDeleted.js'
import { ItemEmbedRef } from '../structs/ItemEmbed.js'
import { ItemFormatRef } from '../structs/ItemFormat.js'
import { ItemJSONRef } from '../structs/ItemJSON.js'
import { ItemStringRef } from '../structs/ItemString.js'
import { ItemTypeRef } from '../structs/ItemType.js'
/**
* @typedef {Map<number, number>} StateMap
*/
const structRefs = [
ItemBinaryRef
ItemBinaryRef,
GCRef,
ItemDeletedRef,
ItemEmbedRef,
ItemFormatRef,
ItemJSONRef,
ItemStringRef,
ItemTypeRef
]
/**
* @param {decoding.Decoder} decoder
* @param {number} structsLen
* @param {ID} nextID
* @return {Iterator<AbstractRef>}
*/
const createStructReaderIterator = (decoder, structsLen, nextID) => iterator.createIterator(() => {
let done = false
let value
if (structsLen === 0) {
done = true
} else {
const info = decoding.readUint8(decoder)
value = new structRefs[binary.BITS5 & info](decoder, nextID, info)
nextID = createID(nextID.client, nextID.clock)
}
return { done, value }
})
/**
* @param {encoding.Encoder} encoder
* @param {Transaction} transaction
*/
export const writeStructsFromTransaction = (encoder, transaction) => writeStructs(encoder, transaction.y.store, transaction.stateUpdates)
/**
* @param {encoding.Encoder} encoder
* @param {StructStore} store
* @param {StateMap} sm
*/
export const writeStructs = (encoder, store, sm) => {
const encoderUserPosMap = map.create()
// write # states that were updated
encoding.writeVarUint(encoder, sm.size)
sm.forEach((client, clock) => {
// write first id
writeID(encoder, createID(client, clock))
encoderUserPosMap.set(client, encoding.length(encoder))
// write diff to pos where structs are written
// We will fill out this value later *)
encoding.writeUint32(encoder, 0)
})
sm.forEach((client, clock) => {
const decPos = encoderUserPosMap.get(client)
encoding.setUint32(encoder, decPos, encoding.length(encoder) - decPos)
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
const startNewStructs = findIndexSS(structs, clock)
// write # encoded structs
encoding.writeVarUint(encoder, structs.length - startNewStructs)
const firstStruct = structs[startNewStructs]
// write first struct with an offset (may be 0)
firstStruct.write(encoder, clock - firstStruct.id.clock, 0)
for (let i = startNewStructs + 1; i < structs.length; i++) {
structs[i].write(encoder, 0, 0)
}
})
}
/**
* 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 {decoding.Decoder} decoder The decoder object to read data from.
* @return {AbstractRef}
* @param {Transaction} transaction
* @param {StructStore} store
*
* @private
*/
export const read = decoder => {
const info = decoding.readUint8(decoder)
return new structRefs[binary.BITS5 & info](decoder, info)
}
/**
* @param {encoding.Encoder} encoder
* @param {Transaction} transaction
*/
export const writeStructsFromTransaction = (encoder, transaction) => {
const stateUpdates = transaction.stateUpdates
const y = transaction.y
encoding.writeVarUint(encoder, stateUpdates.size)
stateUpdates.forEach((clock, client) => {
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = y.store.clients.get(client)
for (let i = findIndex(structs, clock); i < structs.length; i++) {
structs[i].write(encoder, 0)
export const readStructs = (decoder, transaction, store) => {
/**
* @type {Map<number,Iterator<AbstractRef>>}
*/
const structReaders = new Map()
const clientStateUpdates = decoding.readVarUint(decoder)
for (let i = 0; i < clientStateUpdates; i++) {
const nextID = readID(decoder)
const decoderPos = decoder.pos + decoding.readUint32(decoder)
const structReaderDecoder = decoding.clone(decoder, decoderPos)
const numberOfStructs = decoding.readVarUint(structReaderDecoder)
structReaders.set(nextID.client, createStructReaderIterator(structReaderDecoder, numberOfStructs, nextID))
}
/**
* @type {Array<AbstractRef>}
*/
const stack = []
for (const it of structReaders.values()) {
// todo try for in of it
for (let res = it.next(); !res.done; res = it.next()) {
stack.push(res.value)
while (stack.length > 0) {
const ref = stack[stack.length - 1]
const m = ref._missing
while (m.length > 0) {
const nextMissing = m[m.length - 1]
if (!exists(store, nextMissing)) {
// @ts-ignore must not be undefined, otherwise unexpected case
stack.push(structReaders.get(nextMissing.client).next().value)
break
}
ref._missing.pop()
}
if (m.length === 0) {
ref.toStruct(transaction).integrate(transaction)
stack.pop()
}
}
}
})
}
}