all YArray.tests type fixes

This commit is contained in:
Kevin Jahns
2019-04-03 02:30:44 +02:00
parent e23582b1cd
commit 415de1cc4c
17 changed files with 383 additions and 180 deletions

View File

@@ -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'

View File

@@ -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
}
/**

View File

@@ -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}
*/

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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
}
}

View File

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

View File

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

View File

@@ -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
}
/**

View File

@@ -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.
*/

View File

@@ -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)
}
}
/**