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

@ -6,7 +6,7 @@
"module": "./dist/yjs.mjs'",
"sideEffects": false,
"scripts": {
"test": "npm run dist && node ./dist/tests.js --repitition-time 50 --production",
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production",
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000",
"dist": "rm -rf dist examples/build && rollup -c",
"watch": "rollup -wc",

View File

@ -64,7 +64,7 @@ export default [{
}, {
name: 'Y',
file: 'dist/yjs.mjs',
format: 'esm',
format: 'es',
sourcemap: true
}],
external: id => /^lib0\//.test(id)

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

View File

@ -6,9 +6,8 @@ import * as array from './y-array.tests.js'
import * as map from './y-map.tests.js'
import * as text from './y-text.tests.js'
import * as xml from './y-xml.tests.js'
import * as perf from './perf.js'
if (isBrowser) {
log.createVConsole(document.body)
}
runTests({ map, array, text, xml, perf })
runTests({ map, array, text, xml })

View File

@ -1,99 +0,0 @@
import * as t from 'lib0/testing.js'
class Item {
constructor (c) {
this.c = c
}
}
const objectsToCreate = 10000000
export const testItemHoldsAll = tc => {
const items = []
for (let i = 0; i < objectsToCreate; i++) {
switch (i % 3) {
case 0:
items.push(new Item(i))
break
case 1:
items.push(new Item(i + ''))
break
case 2:
items.push(new Item({ x: i }))
break
default:
throw new Error()
}
}
const call = []
items.forEach(item => {
switch (item.c.constructor) {
case Number:
call.push(item.c + '')
break
case String:
call.push(item.c)
break
case Object:
call.push(item.c.x + '')
break
default:
throw new Error()
}
})
}
class CItem { }
class CItemNumber {
constructor (i) {
this.c = i
}
toString () {
return this.c + ''
}
}
class CItemString {
constructor (s) {
this.c = s
}
toString () {
return this.c
}
}
class CItemObject {
constructor (o) {
this.c = o
}
toString () {
return this.c.x
}
}
/*
export const testDifferentItems = tc => {
const items = []
for (let i = 0; i < objectsToCreate; i++) {
switch (i % 3) {
case 0:
items.push(new CItemNumber(i))
break
case 1:
items.push(new CItemString(i + ''))
break
case 2:
items.push(new CItemObject({ x: i }))
break
default:
throw new Error()
}
}
const call = []
items.forEach(item => {
call.push(item.toString())
})
}
*/

View File

@ -5,7 +5,6 @@ import { createMutex } from 'lib0/mutex.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as syncProtocol from 'y-protocols/sync.js'
import { defragmentItemContent } from '../src/utils/defragmentItemContent.js'
/**
* @param {TestYInstance} y
@ -13,11 +12,9 @@ import { defragmentItemContent } from '../src/utils/defragmentItemContent.js'
*/
const afterTransaction = (y, transaction) => {
y.mMux(() => {
if (transaction.encodedStructsLen > 0) {
const encoder = encoding.createEncoder()
syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
broadcastMessage(y, encoding.toBuffer(encoder))
}
const encoder = encoding.createEncoder()
syncProtocol.writeUpdate(encoder, transaction.updateMessage)
broadcastMessage(y, encoding.toBuffer(encoder))
})
}
@ -217,6 +214,7 @@ export class TestConnector {
/**
* @param {t.TestCase} tc
* @param {{users?:number}} conf
* @return {{testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map,map1:Y.Map,map2:Y.Map,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:YXmlFragment,xml1:YXmlFragment,xml2:YXmlFragment}}
*/
export const init = (tc, { users = 5 } = {}) => {
/**
@ -231,50 +229,27 @@ export const init = (tc, { users = 5 } = {}) => {
for (let i = 0; i < users; i++) {
const y = testConnector.createY(i)
result.users.push(y)
result['array' + i] = y.define('array', Y.Array)
result['map' + i] = y.define('map', Y.Map)
result['xml' + i] = y.define('xml', Y.XmlElement)
result['text' + i] = y.define('text', Y.Text)
result['array' + i] = y.get('array', Y.Array)
result['map' + i] = y.get('map', Y.Map)
result['xml' + i] = y.get('xml', Y.XmlElement)
result['text' + i] = y.get('text', Y.Text)
}
testConnector.syncAll()
// @ts-ignore
return result
}
/**
* Convert DS to a proper DeleteSet of Map.
*
* @param {Y.Y} y
* @return {Object<number, Array<[number, number, boolean]>>}
* @param {any} constructor
* @param {ID} a
* @param {ID} b
* @param {string} path
* @param {any} next
*/
const getDeleteSet = y => {
/**
* @type {Object<number, Array<[number, number, boolean]>>}
*/
var ds = {}
y.ds.iterate(null, null, n => {
var user = n._id.user
var counter = n._id.clock
var len = n.len
var gc = n.gc
var dv = ds[user]
if (dv === void 0) {
dv = []
ds[user] = dv
}
dv.push([counter, len, gc])
})
return ds
}
const customOSCompare = (constructor, a, b, path, next) => {
switch (constructor) {
case Y.ID:
case Y.RootID:
if (a.equals(b)) {
return true
} else {
return false
}
return compareIDs(a, b)
}
return next(constructor, a, b, path, next)
}

View File

@ -3,6 +3,9 @@ import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js'
/**
* @param {t.TestCase} tc
*/
export const testDeleteInsert = tc => {
const { users, array0 } = init(tc, { users: 2 })
array0.delete(0, 0)
@ -16,6 +19,9 @@ export const testDeleteInsert = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testInsertThreeElementsTryRegetProperty = tc => {
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, [1, 2, 3])
@ -25,6 +31,9 @@ export const testInsertThreeElementsTryRegetProperty = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testConcurrentInsertWithThreeConflicts = tc => {
var { users, array0, array1, array2 } = init(tc, { users: 3 })
array0.insert(0, [0])
@ -33,6 +42,9 @@ export const testConcurrentInsertWithThreeConflicts = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testConcurrentInsertDeleteWithThreeConflicts = tc => {
const { testConnector, users, array0, array1, array2 } = init(tc, { users: 3 })
array0.insert(0, ['x', 'y', 'z'])
@ -44,6 +56,9 @@ export const testConcurrentInsertDeleteWithThreeConflicts = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testInsertionsInLateSync = tc => {
const { testConnector, users, array0, array1, array2 } = init(tc, { users: 3 })
array0.insert(0, ['x', 'y'])
@ -59,6 +74,9 @@ export const testInsertionsInLateSync = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testDisconnectReallyPreventsSendingMessages = tc => {
var { testConnector, users, array0, array1 } = init(tc, { users: 3 })
array0.insert(0, ['x', 'y'])
@ -74,6 +92,9 @@ export const testDisconnectReallyPreventsSendingMessages = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testDeletionsInLateSync = tc => {
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, ['x', 'y'])
@ -85,6 +106,9 @@ export const testDeletionsInLateSync = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testInsertThenMergeDeleteOnSync = tc => {
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, ['x', 'y', 'z'])
@ -105,6 +129,9 @@ const compareEvent = (is, should) => {
}
}
/**
* @param {t.TestCase} tc
*/
export const testInsertAndDeleteEvents = tc => {
const { array0, users } = init(tc, { users: 2 })
let event
@ -126,6 +153,9 @@ export const testInsertAndDeleteEvents = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testInsertAndDeleteEventsForTypes = tc => {
const { array0, users } = init(tc, { users: 2 })
let event
@ -143,6 +173,9 @@ export const testInsertAndDeleteEventsForTypes = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testInsertAndDeleteEventsForTypes2 = tc => {
const { array0, users } = init(tc, { users: 2 })
let events = []
@ -162,6 +195,9 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testGarbageCollector = tc => {
const { testConnector, users, array0 } = init(tc, { users: 3 })
array0.insert(0, ['x', 'y', 'z'])
@ -173,6 +209,9 @@ export const testGarbageCollector = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testEventTargetIsSetCorrectlyOnLocal = tc => {
const { array0, users } = init(tc, { users: 3 })
/**
@ -187,6 +226,9 @@ export const testEventTargetIsSetCorrectlyOnLocal = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testEventTargetIsSetCorrectlyOnRemote = tc => {
const { testConnector, array0, array1, users } = init(tc, { users: 3 })
/**
@ -205,6 +247,9 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testIteratingArrayContainingTypes = tc => {
const y = new Y.Y()
const arr = y.define('arr', Y.Array)
@ -276,61 +321,106 @@ const arrayTransactions = [
}
]
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests20 = tc => {
applyRandomTests(tc, arrayTransactions, 20)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests40 = tc => {
applyRandomTests(tc, arrayTransactions, 40)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests42 = tc => {
applyRandomTests(tc, arrayTransactions, 42)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests43 = tc => {
applyRandomTests(tc, arrayTransactions, 43)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests44 = tc => {
applyRandomTests(tc, arrayTransactions, 44)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests45 = tc => {
applyRandomTests(tc, arrayTransactions, 45)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests46 = tc => {
applyRandomTests(tc, arrayTransactions, 46)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests300 = tc => {
applyRandomTests(tc, arrayTransactions, 300)
}
/* TODO: implement something like difficutly in lib0
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests400 = tc => {
t.skip(!t.production)
applyRandomTests(tc, arrayTransactions, 400)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests500 = tc => {
t.skip(!t.production)
applyRandomTests(tc, arrayTransactions, 500)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests600 = tc => {
t.skip(!t.production)
applyRandomTests(tc, arrayTransactions, 600)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests1000 = tc => {
t.skip(!t.production)
applyRandomTests(tc, arrayTransactions, 1000)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests1800 = tc => {
t.skip(!t.production)
applyRandomTests(tc, arrayTransactions, 1800)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests10000 = tc => {
t.skip(!t.production)
applyRandomTests(tc, arrayTransactions, 10000)
}
*/

View File

@ -3,6 +3,9 @@ import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js'
/**
* @param {t.TestCase} tc
*/
export const testBasicMapTests = tc => {
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
users[2].disconnect()
@ -38,6 +41,9 @@ export const testBasicMapTests = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testGetAndSetOfMapProperty = tc => {
const { testConnector, users, map0 } = init(tc, { users: 2 })
map0.set('stuff', 'stuffy')
@ -56,6 +62,9 @@ export const testGetAndSetOfMapProperty = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testYmapSetsYmap = tc => {
const { users, map0 } = init(tc, { users: 2 })
const map = map0.set('Map', new Y.Map())
@ -65,6 +74,9 @@ export const testYmapSetsYmap = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testYmapSetsYarray = tc => {
const { users, map0 } = init(tc, { users: 2 })
const array = map0.set('Array', new Y.Array())
@ -74,6 +86,9 @@ export const testYmapSetsYarray = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testGetAndSetOfMapPropertySyncs = tc => {
const { testConnector, users, map0 } = init(tc, { users: 2 })
map0.set('stuff', 'stuffy')
@ -86,6 +101,9 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testGetAndSetOfMapPropertyWithConflict = tc => {
const { testConnector, users, map0, map1 } = init(tc, { users: 3 })
map0.set('stuff', 'c0')
@ -98,6 +116,9 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testGetAndSetAndDeleteOfMapProperty = tc => {
const { testConnector, users, map0, map1 } = init(tc, { users: 3 })
map0.set('stuff', 'c0')
@ -111,6 +132,9 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
map0.set('stuff', 'c0')
@ -125,6 +149,9 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
map0.set('stuff', 'c0')
@ -145,6 +172,9 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testObserveDeepProperties = tc => {
const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 })
const _map1 = map1.set('map', new Y.Map())
@ -176,6 +206,9 @@ export const testObserveDeepProperties = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testObserversUsingObservedeep = tc => {
const { users, map0 } = init(tc, { users: 2 })
const pathes = []
@ -201,6 +234,9 @@ const compareEvent = (t, is, should) => {
}
}
/**
* @param {t.TestCase} tc
*/
export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
const { users, map0 } = init(tc, { users: 2 })
let event
@ -229,6 +265,9 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => {
const { users, map0 } = init(tc, { users: 3 })
let event
@ -240,6 +279,9 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc => {
const { users, map0, map1, testConnector } = init(tc, { users: 3 })
let event
@ -274,61 +316,106 @@ const mapTransactions = [
}
]
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests20 = tc => {
applyRandomTests(tc, mapTransactions, 20)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests40 = tc => {
applyRandomTests(tc, mapTransactions, 40)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests42 = tc => {
applyRandomTests(tc, mapTransactions, 42)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests43 = tc => {
applyRandomTests(tc, mapTransactions, 43)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests44 = tc => {
applyRandomTests(tc, mapTransactions, 44)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests45 = tc => {
applyRandomTests(tc, mapTransactions, 45)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests46 = tc => {
applyRandomTests(tc, mapTransactions, 46)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests300 = tc => {
applyRandomTests(tc, mapTransactions, 300)
}
/* TODO: implement something like difficutly in lib0
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests400 = tc => {
t.skip(!t.production)
applyRandomTests(tc, mapTransactions, 400)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests500 = tc => {
t.skip(!t.production)
applyRandomTests(tc, mapTransactions, 500)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests600 = tc => {
t.skip(!t.production)
applyRandomTests(tc, mapTransactions, 600)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests1000 = tc => {
t.skip(!t.production)
applyRandomTests(tc, mapTransactions, 1000)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests1800 = tc => {
t.skip(!t.production)
applyRandomTests(tc, mapTransactions, 1800)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYmapTests10000 = tc => {
t.skip(!t.production)
applyRandomTests(tc, mapTransactions, 10000)
}
*/

View File

@ -1,6 +1,9 @@
import { init, compare } from './testHelper.js'
import * as t from 'lib0/testing.js'
/**
* @param {t.TestCase} tc
*/
export const testBasicInsertAndDelete = tc => {
const { users, text0 } = init(tc, { users: 2 })
let delta
@ -26,6 +29,9 @@ export const testBasicInsertAndDelete = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testBasicFormat = tc => {
const { users, text0 } = init(tc, { users: 2 })
let delta

View File

@ -2,6 +2,9 @@ import { init, compare } from './testHelper.js'
import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js'
/**
* @param {t.TestCase} tc
*/
export const testSetProperty = tc => {
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
xml0.setAttribute('height', '10')
@ -11,6 +14,9 @@ export const testSetProperty = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testEvents = tc => {
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
let event = { attributesChanged: new Set() }
@ -48,6 +54,9 @@ export const testEvents = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testTreewalker = tc => {
const { users, xml0 } = init(tc, { users: 3 })
let paragraph1 = new Y.XmlElement('p')

View File

@ -58,5 +58,6 @@
"typeRoots": ["./src/utils/typedefs.js"],
// "types": ["./src/utils/typedefs.js"]
},
"exclude": ["./dist/**"]
"include": ["./src/**/*", "./tests/**/*"],
"exclude": ["../lib0/**/*", "node_modules"]
}