more type fixes and rethinking writeStructs
This commit is contained in:
parent
73c28952c2
commit
e23582b1cd
@ -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",
|
||||
|
@ -64,7 +64,7 @@ export default [{
|
||||
}, {
|
||||
name: 'Y',
|
||||
file: 'dist/yjs.mjs',
|
||||
format: 'esm',
|
||||
format: 'es',
|
||||
sourcemap: true
|
||||
}],
|
||||
external: id => /^lib0\//.test(id)
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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++) {
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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] () {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
)
|
||||
)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 })
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
*/
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -58,5 +58,6 @@
|
||||
"typeRoots": ["./src/utils/typedefs.js"],
|
||||
// "types": ["./src/utils/typedefs.js"]
|
||||
},
|
||||
"exclude": ["./dist/**"]
|
||||
"include": ["./src/**/*", "./tests/**/*"],
|
||||
"exclude": ["../lib0/**/*", "node_modules"]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user