all YArray.tests type fixes
This commit is contained in:
parent
e23582b1cd
commit
415de1cc4c
@ -17,5 +17,5 @@ export { YXmlHook as XmlHook } from './types/YXmlHook.js'
|
||||
export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.js'
|
||||
|
||||
export { createRelativePosition, createRelativePositionByOffset, createAbsolutePosition, compareRelativePositions, writeRelativePosition, readRelativePosition, AbsolutePosition, RelativePosition } from './utils/relativePosition.js'
|
||||
export { ID, createID } from './utils/ID.js'
|
||||
export { ID, createID, compareIDs } from './utils/ID.js'
|
||||
export { isParentOf } from './utils/isParentOf.js'
|
||||
|
@ -17,6 +17,7 @@ import { AbstractRef, AbstractStruct } from './AbstractStruct.js' // eslint-disa
|
||||
import * as error from 'lib0/error.js'
|
||||
import { replaceStruct, addStruct } from '../utils/StructStore.js'
|
||||
import { addToDeleteSet } from '../utils/DeleteSet.js'
|
||||
import { ItemDeleted } from './ItemDeleted.js'
|
||||
|
||||
/**
|
||||
* Split leftItem into two items
|
||||
@ -408,9 +409,14 @@ export class AbstractItem extends AbstractStruct {
|
||||
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @return {GC|ItemDeleted}
|
||||
*/
|
||||
gc (y) {
|
||||
replaceStruct(y.store, this, new GC(this.id, this.length))
|
||||
const r = this.parent._item !== null && this.parent._item.deleted
|
||||
? new GC(this.id, this.length)
|
||||
: new ItemDeleted(this.id, this.left, this.right, this.parent, this.parentSub, this.length)
|
||||
replaceStruct(y.store, this, r)
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,16 @@ export class AbstractStruct {
|
||||
*/
|
||||
this.id = id
|
||||
}
|
||||
/**
|
||||
* Merge this struct with the item to the right.
|
||||
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
|
||||
* Also this method does *not* remove right from StructStore!
|
||||
* @param {AbstractStruct} right
|
||||
* @return {boolean} wether this merged with right
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
|
@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { AbstractRef, AbstractStruct } from './AbstractStruct.js'
|
||||
import { ID, readID, createID, writeID } from '../utils/ID.js' // eslint-disable-line
|
||||
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
|
||||
@ -26,6 +27,15 @@ export class GC extends AbstractStruct {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractStruct} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.length += right.length
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
|
@ -37,6 +37,17 @@ export class ItemDeleted extends AbstractItem {
|
||||
copy (id, left, right, parent, parentSub) {
|
||||
return new ItemDeleted(id, left, right, parent, parentSub, this.length)
|
||||
}
|
||||
/**
|
||||
* @param {ItemDeleted} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
if (right.origin === this && this.right === right) {
|
||||
this.length += right.length
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
|
@ -25,6 +25,9 @@ export class ItemJSON extends AbstractItem {
|
||||
*/
|
||||
constructor (id, left, right, parent, parentSub, content) {
|
||||
super(id, left, right, parent, parentSub)
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
this.content = content
|
||||
}
|
||||
/**
|
||||
@ -56,6 +59,17 @@ export class ItemJSON extends AbstractItem {
|
||||
right.content = this.content.splice(diff)
|
||||
return right
|
||||
}
|
||||
/**
|
||||
* @param {ItemJSON} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
if (right.origin === this && this.right === right) {
|
||||
this.content = this.content.concat(right.content)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
@ -63,8 +77,8 @@ export class ItemJSON extends AbstractItem {
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structJSONRefNumber)
|
||||
const len = this.content.length
|
||||
encoding.writeVarUint(encoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
encoding.writeVarUint(encoder, len - offset)
|
||||
for (let i = offset; i < len; i++) {
|
||||
const c = this.content[i]
|
||||
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
|
||||
}
|
||||
|
@ -60,13 +60,24 @@ export class ItemString extends AbstractItem {
|
||||
this.string = this.string.slice(0, diff)
|
||||
return right
|
||||
}
|
||||
/**
|
||||
* @param {ItemString} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
if (right.origin === this && this.right === right) {
|
||||
this.string += right.string
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @param {encoding.Encoder} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
super.write(encoder, offset, structStringRefNumber)
|
||||
encoding.writeVarString(encoder, this.string)
|
||||
encoding.writeVarString(encoder, offset === 0 ? this.string : this.string.slice(offset))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,8 @@ import { readYXmlHook } from '../types/YXmlHook.js'
|
||||
import { readYXmlText } from '../types/YXmlText.js'
|
||||
import { getItemCleanEnd, getItemCleanStart, getItemType } from '../utils/StructStore.js'
|
||||
import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||
import { GC } from './GC.js' // eslint-disable-line
|
||||
import { ItemDeleted } from './ItemDeleted.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @param {Y} y
|
||||
@ -130,10 +132,11 @@ export class ItemType extends AbstractItem {
|
||||
|
||||
/**
|
||||
* @param {Y} y
|
||||
* @return {ItemDeleted|GC}
|
||||
*/
|
||||
gc (y) {
|
||||
this.gcChildren(y)
|
||||
super.gc(y)
|
||||
return super.gc(y)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ export class AbstractType {
|
||||
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs) {
|
||||
this._callEventHandler(transaction, new YEvent(this))
|
||||
this._callEventHandler(transaction, new YEvent(this, transaction))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,7 +125,7 @@ export class AbstractType {
|
||||
/**
|
||||
* Observe all events that are created on this type.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
* @param {function(YEvent):void} f Observer function
|
||||
*/
|
||||
observe (f) {
|
||||
this._eventHandler.addEventListener(f)
|
||||
@ -223,6 +223,7 @@ export const typeArrayMap = (type, f) => {
|
||||
|
||||
/**
|
||||
* @param {AbstractType} type
|
||||
* @return {{next:function():{done:boolean,value:any|undefined}}}
|
||||
*/
|
||||
export const typeArrayCreateIterator = type => {
|
||||
let n = type._start
|
||||
@ -242,7 +243,8 @@ export const typeArrayCreateIterator = type => {
|
||||
// check if we reached the end, no need to check currentContent, because it does not exist
|
||||
if (n === null) {
|
||||
return {
|
||||
done: true
|
||||
done: true,
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
// currentContent could exist from the last iteration
|
||||
|
@ -18,50 +18,8 @@ export class YArrayEvent extends YEvent {
|
||||
* @param {Transaction} transaction The transaction object
|
||||
*/
|
||||
constructor (yarray, transaction) {
|
||||
super(yarray)
|
||||
super(yarray, transaction)
|
||||
this._transaction = transaction
|
||||
this._addedElements = null
|
||||
this._removedElements = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Child elements that were added in this transaction.
|
||||
*
|
||||
* @return {Set<AbstractItem>}
|
||||
*/
|
||||
get addedElements () {
|
||||
if (this._addedElements === null) {
|
||||
const target = this.target
|
||||
const transaction = this._transaction
|
||||
const addedElements = new Set()
|
||||
transaction.added.forEach(type => {
|
||||
if (type.parent === target && !transaction.deleted.has(type)) {
|
||||
addedElements.add(type)
|
||||
}
|
||||
})
|
||||
this._addedElements = addedElements
|
||||
}
|
||||
return this._addedElements
|
||||
}
|
||||
|
||||
/**
|
||||
* Child elements that were removed in this transaction.
|
||||
*
|
||||
* @return {Set<AbstractItem>}
|
||||
*/
|
||||
get removedElements () {
|
||||
if (this._removedElements === null) {
|
||||
const target = this.target
|
||||
const transaction = this._transaction
|
||||
const removedElements = new Set()
|
||||
transaction.deleted.forEach(struct => {
|
||||
if (struct.parent === target && !transaction.added.has(struct)) {
|
||||
removedElements.add(struct)
|
||||
}
|
||||
})
|
||||
this._removedElements = removedElements
|
||||
}
|
||||
return this._removedElements
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
* @module types
|
||||
*/
|
||||
|
||||
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'
|
||||
@ -11,20 +10,23 @@ import { Transaction } from '../utils/Transaction.js' // eslint-disable-line
|
||||
import * as iterator from 'lib0/iterator.js'
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* Event that describes the changes on a YMap.
|
||||
*/
|
||||
export class YMapEvent extends YEvent {
|
||||
/**
|
||||
* @param {YMap} ymap The YArray that changed.
|
||||
* @param {YMap<T>} ymap The YArray that changed.
|
||||
* @param {Transaction} transaction
|
||||
* @param {Set<any>} subs The keys that changed.
|
||||
*/
|
||||
constructor (ymap, subs) {
|
||||
super(ymap)
|
||||
constructor (ymap, transaction, subs) {
|
||||
super(ymap, transaction)
|
||||
this.keysChanged = subs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T number|string|Object|Array|ArrayBuffer
|
||||
* A shared Map implementation.
|
||||
*/
|
||||
export class YMap extends AbstractType {
|
||||
@ -62,17 +64,17 @@ export class YMap extends AbstractType {
|
||||
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs) {
|
||||
this._callEventHandler(transaction, new YMapEvent(this, parentSubs))
|
||||
this._callEventHandler(transaction, new YMapEvent(this, transaction, parentSubs))
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Object<string,number|string|Object|Array|ArrayBuffer>}
|
||||
* @return {Object<string,T>}
|
||||
*/
|
||||
toJSON () {
|
||||
/**
|
||||
* @type {Object<string,number|string|Object|Array|ArrayBuffer>}
|
||||
* @type {Object<string,T>}
|
||||
*/
|
||||
const map = {}
|
||||
for (let [key, item] of this._map) {
|
||||
@ -94,7 +96,7 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Returns the value for each element in the YMap Type.
|
||||
*
|
||||
* @return {Iterator<string|number|ArrayBuffer|Object<string,any>|Array<any>>}
|
||||
* @return {Iterator<T>}
|
||||
*/
|
||||
entries () {
|
||||
return iterator.iteratorMap(createMapIterator(this._map), v => v[1].getContent()[0])
|
||||
@ -124,7 +126,7 @@ export class YMap extends AbstractType {
|
||||
* Adds or updates an element with a specified key and value.
|
||||
*
|
||||
* @param {string} key The key of the element to add to this YMap
|
||||
* @param {Object | string | number | AbstractType | ArrayBuffer } value The value of the element to add
|
||||
* @param {T} value The value of the element to add
|
||||
*/
|
||||
set (key, value) {
|
||||
if (this._y !== null) {
|
||||
@ -142,9 +144,10 @@ export class YMap extends AbstractType {
|
||||
* Returns a specified element from this YMap.
|
||||
*
|
||||
* @param {string} key
|
||||
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType|undefined}
|
||||
* @return {T|undefined}
|
||||
*/
|
||||
get (key) {
|
||||
// @ts-ignore
|
||||
return typeMapGet(this, key)
|
||||
}
|
||||
|
||||
|
@ -355,8 +355,6 @@ class YTextEvent extends YArrayEvent {
|
||||
* @type {Array<{delete:number|undefined,retain:number|undefined,insert:string|undefined,attributes:Object<string,any>}>}
|
||||
*/
|
||||
const delta = []
|
||||
const added = this.addedElements
|
||||
const removed = this.removedElements
|
||||
const currentAttributes = new Map() // saves all current attributes for insert
|
||||
const oldAttributes = new Map()
|
||||
let item = this.target._start
|
||||
@ -413,19 +411,19 @@ class YTextEvent extends YArrayEvent {
|
||||
while (item !== null) {
|
||||
switch (item.constructor) {
|
||||
case ItemEmbed:
|
||||
if (added.has(item)) {
|
||||
if (this.adds(item)) {
|
||||
addOp()
|
||||
action = 'insert'
|
||||
// @ts-ignore item is ItemFormat
|
||||
insert = item.embed
|
||||
addOp()
|
||||
} else if (removed.has(item)) {
|
||||
} else if (this.deletes(item)) {
|
||||
if (action !== 'delete') {
|
||||
addOp()
|
||||
action = 'delete'
|
||||
}
|
||||
deleteLen += 1
|
||||
} else if (item.deleted === false) {
|
||||
} else if (!item.deleted) {
|
||||
if (action !== 'retain') {
|
||||
addOp()
|
||||
action = 'retain'
|
||||
@ -434,20 +432,20 @@ class YTextEvent extends YArrayEvent {
|
||||
}
|
||||
break
|
||||
case ItemString:
|
||||
if (added.has(item)) {
|
||||
if (this.adds(item)) {
|
||||
if (action !== 'insert') {
|
||||
addOp()
|
||||
action = 'insert'
|
||||
}
|
||||
// @ts-ignore
|
||||
insert += item.string
|
||||
} else if (removed.has(item)) {
|
||||
} else if (this.deletes(item)) {
|
||||
if (action !== 'delete') {
|
||||
addOp()
|
||||
action = 'delete'
|
||||
}
|
||||
deleteLen += item.length
|
||||
} else if (item.deleted === false) {
|
||||
} else if (!item.deleted) {
|
||||
if (action !== 'retain') {
|
||||
addOp()
|
||||
action = 'retain'
|
||||
@ -456,7 +454,7 @@ class YTextEvent extends YArrayEvent {
|
||||
}
|
||||
break
|
||||
case ItemFormat:
|
||||
if (added.has(item)) {
|
||||
if (this.adds(item)) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
const curVal = currentAttributes.get(item.key) || null
|
||||
// @ts-ignore item is ItemFormat
|
||||
@ -475,7 +473,7 @@ class YTextEvent extends YArrayEvent {
|
||||
} else {
|
||||
item.delete(transaction)
|
||||
}
|
||||
} else if (removed.has(item)) {
|
||||
} else if (this.deletes(item)) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
oldAttributes.set(item.key, item.value)
|
||||
// @ts-ignore item is ItemFormat
|
||||
@ -488,7 +486,7 @@ class YTextEvent extends YArrayEvent {
|
||||
// @ts-ignore item is ItemFormat
|
||||
attributes[item.key] = curVal
|
||||
}
|
||||
} else if (item.deleted === false) {
|
||||
} else if (!item.deleted) {
|
||||
// @ts-ignore item is ItemFormat
|
||||
oldAttributes.set(item.key, item.value)
|
||||
// @ts-ignore item is ItemFormat
|
||||
@ -512,7 +510,7 @@ class YTextEvent extends YArrayEvent {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.deleted === false) {
|
||||
if (!item.deleted) {
|
||||
if (action === 'insert') {
|
||||
addOp()
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ export class DeleteSet {
|
||||
* @param {number} clock
|
||||
* @return {number|null}
|
||||
*/
|
||||
export const findIndexSS = (dis, clock) => {
|
||||
export const findIndexDS = (dis, clock) => {
|
||||
let left = 0
|
||||
let right = dis.length
|
||||
while (left <= right) {
|
||||
@ -71,7 +71,7 @@ export const findIndexSS = (dis, clock) => {
|
||||
*/
|
||||
export const isDeleted = (ds, id) => {
|
||||
const dis = ds.clients.get(id.client)
|
||||
return dis !== undefined && findIndexSS(dis, id.clock) !== null
|
||||
return dis !== undefined && findIndexDS(dis, id.clock) !== null
|
||||
}
|
||||
|
||||
/**
|
||||
|
130
src/utils/Y.js
130
src/utils/Y.js
@ -1,10 +1,19 @@
|
||||
import { StructStore } from './StructStore.js'
|
||||
import { StructStore, findIndexSS } from './StructStore.js'
|
||||
import * as random from 'lib0/random.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
import { Transaction } from './Transaction.js'
|
||||
import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' // eslint-disable-line
|
||||
import { AbstractType } from '../types/AbstractType.js'
|
||||
import { AbstractItem } from '../structs/AbstractItem.js'
|
||||
import { sortAndMergeDeleteSet } from './DeleteSet.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import { GC } from '../structs/GC.js' // eslint-disable-line
|
||||
import { ItemDeleted } from '../structs/ItemDeleted.js' // eslint-disable-line
|
||||
import { YArray } from '../types/YArray.js'
|
||||
import { YText } from '../types/YText.js'
|
||||
import { YMap } from '../types/YMap.js'
|
||||
import { YXmlFragment } from '../types/YXmlElement.js'
|
||||
|
||||
/**
|
||||
* A Yjs instance handles the state of shared data.
|
||||
@ -54,16 +63,13 @@ export class Y extends Observable {
|
||||
* other peers.
|
||||
*
|
||||
* @param {function(Transaction):void} f The function that should be executed as a transaction
|
||||
* @param {?Boolean} remote Optional. Whether this transaction is initiated by
|
||||
* a remote peer. This should not be set manually!
|
||||
* Defaults to false.
|
||||
*/
|
||||
transact (f, remote = false) {
|
||||
transact (f) {
|
||||
let initialCall = false
|
||||
if (this._transaction === null) {
|
||||
initialCall = true
|
||||
this._transaction = new Transaction(this)
|
||||
this.emit('beforeTransaction', [this, this._transaction, remote])
|
||||
this.emit('beforeTransaction', [this, this._transaction])
|
||||
}
|
||||
try {
|
||||
f(this._transaction)
|
||||
@ -76,7 +82,7 @@ export class Y extends Observable {
|
||||
// only call event listeners / observers if anything changed
|
||||
const transactionChangedContent = transaction.changedParentTypes.size !== 0
|
||||
if (transactionChangedContent) {
|
||||
this.emit('beforeObserverCalls', [this, this._transaction, remote])
|
||||
this.emit('beforeObserverCalls', [this, this._transaction])
|
||||
// emit change events on changed types
|
||||
transaction.changed.forEach((subs, itemtype) => {
|
||||
itemtype._callObserver(transaction, subs)
|
||||
@ -95,11 +101,80 @@ export class Y extends Observable {
|
||||
type._deepEventHandler.callEventListeners(transaction, events)
|
||||
})
|
||||
// when all changes & events are processed, emit afterTransaction event
|
||||
this.emit('afterTransaction', [this, transaction, remote])
|
||||
this.emit('afterTransaction', [this, transaction])
|
||||
// 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
|
||||
const store = transaction.y.store
|
||||
const ds = transaction.deleteSet
|
||||
// replace deleted items with ItemDeleted / GC
|
||||
sortAndMergeDeleteSet(ds)
|
||||
/**
|
||||
* @type {Set<ItemDeleted|GC>}
|
||||
*/
|
||||
const replacedItems = new Set()
|
||||
for (const [client, deleteItems] of ds.clients) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
for (let di = 0; di < deleteItems.length; di++) {
|
||||
const deleteItem = deleteItems[di]
|
||||
for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) {
|
||||
const struct = structs[si]
|
||||
if (deleteItem.clock + deleteItem.len < struct.id.clock) {
|
||||
break
|
||||
}
|
||||
if (struct.deleted && struct instanceof AbstractItem) {
|
||||
// check if we can GC
|
||||
replacedItems.add(struct.gc(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Array<AbstractStruct>} structs
|
||||
* @param {number} pos
|
||||
*/
|
||||
const tryToMergeWithLeft = (structs, pos) => {
|
||||
const left = structs[pos - 1]
|
||||
const right = structs[pos]
|
||||
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||
if (left.mergeWith(right)) {
|
||||
structs.splice(pos, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// on all affected store.clients props, try to merge
|
||||
for (const [client, clock] of transaction.stateUpdates) {
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
// we iterate from right to left so we can safely remove entries
|
||||
for (let i = structs.length - 1; i >= math.max(findIndexSS(structs, clock), 1); i--) {
|
||||
tryToMergeWithLeft(structs, i)
|
||||
}
|
||||
}
|
||||
// try to merge replacedItems
|
||||
for (const replacedItem of replacedItems) {
|
||||
const id = replacedItem.id
|
||||
const client = id.client
|
||||
const clock = id.clock
|
||||
/**
|
||||
* @type {Array<AbstractStruct>}
|
||||
*/
|
||||
// @ts-ignore
|
||||
const structs = store.clients.get(client)
|
||||
const replacedStructPos = findIndexSS(structs, clock)
|
||||
if (replacedStructPos + 1 < structs.length) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos + 1)
|
||||
}
|
||||
if (replacedStructPos > 0) {
|
||||
tryToMergeWithLeft(structs, replacedStructPos)
|
||||
}
|
||||
}
|
||||
this.emit('afterTransactionCleanup', [this, transaction])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -148,6 +223,39 @@ export class Y extends Observable {
|
||||
}
|
||||
return type
|
||||
}
|
||||
/**
|
||||
* @template T
|
||||
* @param {string} name
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
getArray (name) {
|
||||
// @ts-ignore
|
||||
return this.get(name, YArray)
|
||||
}
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {YText}
|
||||
*/
|
||||
getText (name) {
|
||||
// @ts-ignore
|
||||
return this.get(name, YText)
|
||||
}
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {YMap}
|
||||
*/
|
||||
getMap (name) {
|
||||
// @ts-ignore
|
||||
return this.get(name, YMap)
|
||||
}
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {YXmlFragment}
|
||||
*/
|
||||
getXmlFragment (name) {
|
||||
// @ts-ignore
|
||||
return this.get(name, YXmlFragment)
|
||||
}
|
||||
/**
|
||||
* Disconnect from the room, and destroy all traces of this Yjs instance.
|
||||
*/
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
|
||||
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
|
||||
import { Transaction } from './Transaction.js' // eslint-disable-line
|
||||
import { AbstractStruct } from '../structs/AbstractStruct.js' // eslint-disable-line
|
||||
import { isDeleted } from './DeleteSet.js'
|
||||
|
||||
/**
|
||||
* @module utils
|
||||
@ -11,8 +14,9 @@ import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
|
||||
export class YEvent {
|
||||
/**
|
||||
* @param {AbstractType} target The changed type.
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
constructor (target) {
|
||||
constructor (target, transaction) {
|
||||
/**
|
||||
* The type on which this event was created on.
|
||||
* @type {AbstractType}
|
||||
@ -23,6 +27,11 @@ export class YEvent {
|
||||
* @type {AbstractType}
|
||||
*/
|
||||
this.currentTarget = target
|
||||
/**
|
||||
* The transaction that triggered this event.
|
||||
* @type {Transaction}
|
||||
*/
|
||||
this.transaction = transaction
|
||||
}
|
||||
|
||||
/**
|
||||
@ -40,6 +49,26 @@ export class YEvent {
|
||||
// @ts-ignore _item is defined because target is integrated
|
||||
return getPathTo(this.currentTarget, this.target._item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a struct is deleted by this event.
|
||||
*
|
||||
* @param {AbstractStruct} struct
|
||||
* @return {boolean}
|
||||
*/
|
||||
deletes (struct) {
|
||||
return isDeleted(this.transaction.deleteSet, struct.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a struct is added by this event.
|
||||
*
|
||||
* @param {AbstractStruct} struct
|
||||
* @return {boolean}
|
||||
*/
|
||||
adds (struct) {
|
||||
return struct.id.clock > (this.transaction.stateUpdates.get(struct.id.client) || 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,6 +5,9 @@ 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 { createDeleteSetFromStructStore, DeleteSet } from '../src/utils/DeleteSet.js' // eslint-disable-line
|
||||
import { getStates, StructStore } from '../src/utils/StructStore.js' // eslint-disable-line
|
||||
import { AbstractItem } from '../src/structs/AbstractItem.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @param {TestYInstance} y
|
||||
@ -35,6 +38,7 @@ const broadcastMessage = (y, m) => {
|
||||
export class TestYInstance extends Y.Y {
|
||||
/**
|
||||
* @param {TestConnector} testConnector
|
||||
* @param {number} clientID
|
||||
*/
|
||||
constructor (testConnector, clientID) {
|
||||
super()
|
||||
@ -109,6 +113,9 @@ export class TestYInstance extends Y.Y {
|
||||
* I think it makes sense. Deal with it.
|
||||
*/
|
||||
export class TestConnector {
|
||||
/**
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
constructor (gen) {
|
||||
/**
|
||||
* @type {Set<TestYInstance>}
|
||||
@ -197,6 +204,9 @@ export class TestConnector {
|
||||
* @return {boolean} Whether it was possible to reconnect a random connection.
|
||||
*/
|
||||
reconnectRandom () {
|
||||
/**
|
||||
* @type {Array<TestYInstance>}
|
||||
*/
|
||||
const reconnectable = []
|
||||
this.allConns.forEach(conn => {
|
||||
if (!this.onlineConns.has(conn)) {
|
||||
@ -214,7 +224,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}}
|
||||
* @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:Y.XmlFragment,xml1:Y.XmlFragment,xml2:Y.XmlFragment}}
|
||||
*/
|
||||
export const init = (tc, { users = 5 } = {}) => {
|
||||
/**
|
||||
@ -239,21 +249,6 @@ export const init = (tc, { users = 5 } = {}) => {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} constructor
|
||||
* @param {ID} a
|
||||
* @param {ID} b
|
||||
* @param {string} path
|
||||
* @param {any} next
|
||||
*/
|
||||
const customOSCompare = (constructor, a, b, path, next) => {
|
||||
switch (constructor) {
|
||||
case Y.ID:
|
||||
return compareIDs(a, b)
|
||||
}
|
||||
return next(constructor, a, b, path, next)
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. reconnect and flush all
|
||||
* 2. user 0 gc
|
||||
@ -266,59 +261,22 @@ const customOSCompare = (constructor, a, b, path, next) => {
|
||||
export const compare = users => {
|
||||
users.forEach(u => u.connect())
|
||||
while (users[0].tc.flushAllMessages()) {}
|
||||
var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
|
||||
var userMapValues = users.map(u => u.define('map', Y.Map).toJSON())
|
||||
var userXmlValues = users.map(u => u.define('xml', Y.XmlElement).toString())
|
||||
var userTextValues = users.map(u => u.define('text', Y.Text).toDelta())
|
||||
var data = users.map(u => {
|
||||
defragmentItemContent(u)
|
||||
var data = {}
|
||||
let ops = []
|
||||
u.os.iterate(null, null, op => {
|
||||
let json
|
||||
if (op.constructor === Y.GC) {
|
||||
json = {
|
||||
type: 'GC',
|
||||
id: op._id,
|
||||
length: op._length,
|
||||
content: null
|
||||
}
|
||||
} else {
|
||||
json = {
|
||||
id: op._id,
|
||||
left: op._left === null ? null : op._left._lastId,
|
||||
right: op._right === null ? null : op._right._id,
|
||||
length: op._length,
|
||||
deleted: op._deleted,
|
||||
parent: op._parent._id,
|
||||
content: null
|
||||
}
|
||||
}
|
||||
if (op instanceof Y.ItemJSON || op instanceof Y.ItemString) {
|
||||
json.content = op._content
|
||||
}
|
||||
ops.push(json)
|
||||
})
|
||||
data.os = ops
|
||||
data.ds = getDeleteSet(u)
|
||||
const ss = {}
|
||||
u.ss.state.forEach((clock, user) => {
|
||||
ss[user] = clock
|
||||
})
|
||||
data.ss = ss
|
||||
return data
|
||||
})
|
||||
for (var i = 0; i < data.length - 1; i++) {
|
||||
// t.describe(`Comparing user${i} with user${i + 1}`)
|
||||
t.compare(userArrayValues[i].length, users[i].get('array').length)
|
||||
const userArrayValues = users.map(u => u.getArray('array').toJSON().map(val => JSON.stringify(val)))
|
||||
const userMapValues = users.map(u => u.getMap('map').toJSON())
|
||||
const userXmlValues = users.map(u => u.getXmlFragment('xml').toString())
|
||||
const userTextValues = users.map(u => u.getText('text').toDelta())
|
||||
for (var i = 0; i < users.length - 1; i++) {
|
||||
t.describe(`Comparing user${i} with user${i + 1}`)
|
||||
t.compare(userArrayValues[i].length, users[i].getArray('array').length)
|
||||
t.compare(userArrayValues[i], userArrayValues[i + 1])
|
||||
t.compare(userMapValues[i], userMapValues[i + 1])
|
||||
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
||||
t.compare(userTextValues[i].map(a => a.insert).join('').length, users[i].get('text').length)
|
||||
// @ts-ignore
|
||||
t.compare(userTextValues[i].map(a => a.insert).join('').length, users[i].getText('text').length)
|
||||
t.compare(userTextValues[i], userTextValues[i + 1])
|
||||
t.compare(data[i].os, data[i + 1].os, null, customOSCompare)
|
||||
t.compare(data[i].ds, data[i + 1].ds, null, customOSCompare)
|
||||
t.compare(data[i].ss, data[i + 1].ss, null, customOSCompare)
|
||||
t.compare(getStates(users[i].store), getStates(users[i + 1].store))
|
||||
compareDS(createDeleteSetFromStructStore(users[i].store), createDeleteSetFromStructStore(users[i + 1].store))
|
||||
compareStructStores(users[i].store, users[i + 1].store)
|
||||
}
|
||||
users.forEach(user =>
|
||||
t.assert(user._missingStructs.size === 0)
|
||||
@ -326,6 +284,76 @@ export const compare = users => {
|
||||
users.map(u => u.destroy())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractItem?} a
|
||||
* @param {AbstractItem?} b
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
||||
|
||||
/**
|
||||
* @param {StructStore} ss1
|
||||
* @param {StructStore} ss2
|
||||
*/
|
||||
export const compareStructStores = (ss1, ss2) => {
|
||||
t.assert(ss1.clients.size === ss2.clients.size)
|
||||
for (const [client, structs1] of ss1.clients) {
|
||||
const structs2 = ss2.clients.get(client)
|
||||
t.assert(structs2 !== undefined && structs1.length === structs2.length)
|
||||
for (let i = 0; i < structs1.length; i++) {
|
||||
const s1 = structs1[i]
|
||||
// @ts-ignore
|
||||
const s2 = structs2[i]
|
||||
// checks for abstract struct
|
||||
if (
|
||||
s1.constructor !== s2.constructor ||
|
||||
!Y.compareIDs(s1.id, s2.id) ||
|
||||
s1.deleted !== s2.deleted ||
|
||||
s1.length !== s2.length
|
||||
) {
|
||||
t.fail('Structs dont match')
|
||||
}
|
||||
if (s1 instanceof AbstractItem) {
|
||||
if (
|
||||
!(s2 instanceof AbstractItem) ||
|
||||
!compareItemIDs(s1.left, s2.left) ||
|
||||
!compareItemIDs(s1.right, s2.right) ||
|
||||
!compareItemIDs(s1.origin, s2.origin) ||
|
||||
!compareItemIDs(s1.rightOrigin, s2.rightOrigin) ||
|
||||
s1.parentSub !== s2.parentSub
|
||||
) {
|
||||
t.fail('Items dont match')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds1
|
||||
* @param {DeleteSet} ds2
|
||||
*/
|
||||
export const compareDS = (ds1, ds2) => {
|
||||
t.assert(ds1.clients.size === ds2.clients.size)
|
||||
for (const [client, deleteItems1] of ds1.clients) {
|
||||
const deleteItems2 = ds2.clients.get(client)
|
||||
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
|
||||
for (let i = 0; i < deleteItems1.length; i++) {
|
||||
const di1 = deleteItems1[i]
|
||||
// @ts-ignore
|
||||
const di2 = deleteItems2[i]
|
||||
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||
t.fail('DeleteSets dont match')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
* @param {Array<function(TestYInstance,prng.PRNG):void>} mods
|
||||
* @param {number} iterations
|
||||
*/
|
||||
export const applyRandomTests = (tc, mods, iterations) => {
|
||||
const gen = tc.prng
|
||||
const result = init(tc, { users: 5 })
|
||||
@ -350,7 +378,7 @@ export const applyRandomTests = (tc, mods, iterations) => {
|
||||
}
|
||||
let user = prng.oneOf(gen, users)
|
||||
var test = prng.oneOf(gen, mods)
|
||||
test(t, user, gen)
|
||||
test(user, gen)
|
||||
}
|
||||
compare(users)
|
||||
return result
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { init, compare, applyRandomTests } from './testHelper.js'
|
||||
import { init, compare, applyRandomTests, TestYInstance } from './testHelper.js' // eslint-disable-line
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as prng from 'lib0/prng.js'
|
||||
@ -119,6 +119,10 @@ export const testInsertThenMergeDeleteOnSync = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object<string,any>} is
|
||||
* @param {Object<string,any>} should
|
||||
*/
|
||||
const compareEvent = (is, should) => {
|
||||
for (var key in should) {
|
||||
t.assert(
|
||||
@ -134,7 +138,10 @@ const compareEvent = (is, should) => {
|
||||
*/
|
||||
export const testInsertAndDeleteEvents = tc => {
|
||||
const { array0, users } = init(tc, { users: 2 })
|
||||
let event
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
let event = {}
|
||||
array0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
@ -158,11 +165,14 @@ export const testInsertAndDeleteEvents = tc => {
|
||||
*/
|
||||
export const testInsertAndDeleteEventsForTypes = tc => {
|
||||
const { array0, users } = init(tc, { users: 2 })
|
||||
let event
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
let event = {}
|
||||
array0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
array0.insert(0, [Y.Array])
|
||||
array0.insert(0, [new Y.Array()])
|
||||
compareEvent(event, {
|
||||
remote: false
|
||||
})
|
||||
@ -178,11 +188,14 @@ export const testInsertAndDeleteEventsForTypes = tc => {
|
||||
*/
|
||||
export const testInsertAndDeleteEventsForTypes2 = tc => {
|
||||
const { array0, users } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {Array<Object<string,any>>}
|
||||
*/
|
||||
let events = []
|
||||
array0.observe(e => {
|
||||
events.push(e)
|
||||
})
|
||||
array0.insert(0, ['hi', Y.Map])
|
||||
array0.insert(0, ['hi', new Y.Map()])
|
||||
compareEvent(events[0], {
|
||||
remote: false
|
||||
})
|
||||
@ -252,7 +265,7 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
|
||||
*/
|
||||
export const testIteratingArrayContainingTypes = tc => {
|
||||
const y = new Y.Y()
|
||||
const arr = y.define('arr', Y.Array)
|
||||
const arr = y.getArray('arr')
|
||||
const numItems = 10
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
const map = new Y.Map()
|
||||
@ -269,9 +282,12 @@ export const testIteratingArrayContainingTypes = tc => {
|
||||
let _uniqueNumber = 0
|
||||
const getUniqueNumber = () => _uniqueNumber++
|
||||
|
||||
/**
|
||||
* @type {Array<function(TestYInstance,prng.PRNG):void>}
|
||||
*/
|
||||
const arrayTransactions = [
|
||||
function insert (tc, user, gen) {
|
||||
const yarray = user.define('array', Y.Array)
|
||||
function insert (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
var uniqueNumber = getUniqueNumber()
|
||||
var content = []
|
||||
var len = prng.int31(gen, 1, 4)
|
||||
@ -281,38 +297,34 @@ const arrayTransactions = [
|
||||
var pos = prng.int31(gen, 0, yarray.length)
|
||||
yarray.insert(pos, content)
|
||||
},
|
||||
function insertTypeArray (tc, user, gen) {
|
||||
const yarray = user.define('array', Y.Array)
|
||||
function insertTypeArray (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
var pos = prng.int31(gen, 0, yarray.length)
|
||||
yarray.insert(pos, [Y.Array])
|
||||
yarray.insert(pos, [new Y.Array()])
|
||||
var array2 = yarray.get(pos)
|
||||
array2.insert(0, [1, 2, 3, 4])
|
||||
},
|
||||
function insertTypeMap (tc, user, gen) {
|
||||
const yarray = user.define('array', Y.Array)
|
||||
function insertTypeMap (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
var pos = prng.int31(gen, 0, yarray.length)
|
||||
yarray.insert(pos, [Y.Map])
|
||||
yarray.insert(pos, [new Y.Map()])
|
||||
var map = yarray.get(pos)
|
||||
map.set('someprop', 42)
|
||||
map.set('someprop', 43)
|
||||
map.set('someprop', 44)
|
||||
},
|
||||
function _delete (tc, user, gen) {
|
||||
const yarray = user.define('array', Y.Array)
|
||||
function _delete (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
var length = yarray.length
|
||||
if (length > 0) {
|
||||
var somePos = prng.int31(gen, 0, length - 1)
|
||||
var delLength = prng.int31(gen, 1, Math.min(2, length - somePos))
|
||||
if (yarray instanceof Y.Array) {
|
||||
if (prng.bool(gen)) {
|
||||
var type = yarray.get(somePos)
|
||||
if (type.length > 0) {
|
||||
somePos = prng.int31(gen, 0, type.length - 1)
|
||||
delLength = prng.int31(gen, 0, Math.min(2, type.length - somePos))
|
||||
type.delete(somePos, delLength)
|
||||
}
|
||||
} else {
|
||||
yarray.delete(somePos, delLength)
|
||||
if (prng.bool(gen)) {
|
||||
var type = yarray.get(somePos)
|
||||
if (type.length > 0) {
|
||||
somePos = prng.int31(gen, 0, type.length - 1)
|
||||
delLength = prng.int31(gen, 0, Math.min(2, type.length - somePos))
|
||||
type.delete(somePos, delLength)
|
||||
}
|
||||
} else {
|
||||
yarray.delete(somePos, delLength)
|
||||
|
Loading…
x
Reference in New Issue
Block a user