created new abstraction for search marker

This commit is contained in:
Kevin Jahns 2021-11-19 16:07:03 +01:00
parent a723c32557
commit b9ccbb2dc7
8 changed files with 544 additions and 462 deletions

View File

@ -8,6 +8,7 @@ export * from './utils/encoding.js'
export * from './utils/EventHandler.js'
export * from './utils/ID.js'
export * from './utils/isParentOf.js'
export * from './utils/ListIterator.js'
export * from './utils/logging.js'
export * from './utils/PermanentUserData.js'
export * from './utils/RelativePosition.js'

View File

@ -581,18 +581,17 @@ export class Item extends AbstractStruct {
this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content)
) {
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker
if (searchMarker) {
searchMarker.forEach(marker => {
if (marker.p === right) {
// right is going to be "forgotten" so we need to update the marker
marker.p = this
// adjust marker index
if (!this.deleted && this.countable) {
marker.index -= this.length
if (right.marker) {
// Right will be "forgotten", so we delete all
// search markers that reference right.
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker
if (searchMarker) {
for (let i = searchMarker.length - 1; i >= 0; i--) {
if (searchMarker[i].nextItem === right) {
searchMarker.splice(i, 1)
}
}
})
}
}
if (right.keep) {
this.keep = true

View File

@ -10,9 +10,7 @@ import {
createID,
ContentAny,
ContentBinary,
getItemCleanStart,
ContentMove,
getMovedCoords,
ListIterator,
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
} from '../internals.js'
@ -23,67 +21,6 @@ import * as math from 'lib0/math'
const maxSearchMarker = 80
/**
* A unique timestamp that identifies each marker.
*
* Time is relative,.. this is more like an ever-increasing clock.
*
* @type {number}
*/
let globalSearchMarkerTimestamp = 0
export class ArraySearchMarker {
/**
* @param {Item} p
* @param {number} index
*/
constructor (p, index) {
p.marker = true
this.p = p
this.index = index
this.timestamp = globalSearchMarkerTimestamp++
}
}
/**
* @param {ArraySearchMarker} marker
*/
const refreshMarkerTimestamp = marker => { marker.timestamp = globalSearchMarkerTimestamp++ }
/**
* This is rather complex so this function is the only thing that should overwrite a marker
*
* @param {ArraySearchMarker} marker
* @param {Item} p
* @param {number} index
*/
const overwriteMarker = (marker, p, index) => {
marker.p.marker = false
marker.p = p
p.marker = true
marker.index = index
marker.timestamp = globalSearchMarkerTimestamp++
}
/**
* @param {Array<ArraySearchMarker>} searchMarker
* @param {Item} p
* @param {number} index
*/
const markPosition = (searchMarker, p, index) => {
if (searchMarker.length >= maxSearchMarker) {
// override oldest marker (we don't want to create more objects)
const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b)
overwriteMarker(marker, p, index)
return marker
} else {
// create new marker
const pm = new ArraySearchMarker(p, index)
searchMarker.push(pm)
return pm
}
}
/**
* Search marker help us to find positions in the associative array faster.
*
@ -91,82 +28,47 @@ const markPosition = (searchMarker, p, index) => {
*
* A maximum of `maxSearchMarker` objects are created.
*
* This function always returns a refreshed marker (updated timestamp)
*
* @template T
* @param {Transaction} tr
* @param {AbstractType<any>} yarray
* @param {number} index
* @param {function(ListIterator):T} f
*/
export const findMarker = (yarray, index) => {
if (yarray._start === null || index === 0 || yarray._searchMarker === null) {
return null
export const useSearchMarker = (tr, yarray, index, f) => {
const searchMarker = yarray._searchMarker
if (searchMarker === null || yarray._start === null || index < 30) {
return f(new ListIterator(yarray).forward(tr, index))
}
const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b)
let p = yarray._start
let pindex = 0
if (marker !== null) {
p = marker.p
pindex = marker.index
refreshMarkerTimestamp(marker) // we used it, we might need to use it again
if (searchMarker.length === 0) {
const sm = new ListIterator(yarray).forward(tr, index)
searchMarker.push(sm)
if (sm.nextItem) sm.nextItem.marker = true
return f(sm)
}
// iterate to right if possible
while (p.right !== null && pindex < index) {
if (!p.deleted && p.countable) {
if (index < pindex + p.length) {
break
}
pindex += p.length
}
p = p.right
const sm = searchMarker.reduce(
(a, b, arrayIndex) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b
)
const createFreshMarker = searchMarker.length < maxSearchMarker && math.abs(sm.index - index) > 30
const fsm = createFreshMarker ? sm.clone() : sm
const prevItem = /** @type {Item} */ (sm.nextItem)
if (createFreshMarker) {
searchMarker.push(fsm)
}
// iterate to left if necessary (might be that pindex > index)
while (p.left !== null && pindex > index) {
p = p.left
if (!p.deleted && p.countable) {
pindex -= p.length
const result = f(fsm)
if (!createFreshMarker && fsm.nextItem !== prevItem) {
// reused old marker and we moved to a different position
prevItem.marker = false
}
const fsmItem = fsm.nextItem
if (fsmItem) {
if (fsmItem.marker) {
// already marked, forget current iterator
searchMarker.splice(searchMarker.findIndex(m => m === fsm), 1)
} else {
fsmItem.marker = true
}
}
// we want to make sure that p can't be merged with left, because that would screw up everything
// in that cas just return what we have (it is most likely the best marker anyway)
// iterate to left until p can't be merged with left
while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) {
p = p.left
if (!p.deleted && p.countable) {
pindex -= p.length
}
}
// @todo remove!
// assure position
// {
// let start = yarray._start
// let pos = 0
// while (start !== p) {
// if (!start.deleted && start.countable) {
// pos += start.length
// }
// start = /** @type {Item} */ (start.right)
// }
// if (pos !== pindex) {
// debugger
// throw new Error('Gotcha position fail!')
// }
// }
// if (marker) {
// if (window.lengthes == null) {
// window.lengthes = []
// window.getLengthes = () => window.lengthes.sort((a, b) => a - b)
// }
// window.lengthes.push(marker.index - pindex)
// console.log('distance', marker.index - pindex, 'len', p && p.parent.length)
// }
if (marker !== null && math.abs(marker.index - pindex) < /** @type {YText|YArray<any>} */ (p.parent).length / maxSearchMarker) {
// adjust existing marker
overwriteMarker(marker, p, pindex)
return marker
} else {
// create new marker
return markPosition(yarray._searchMarker, p, pindex)
}
return result
}
/**
@ -174,39 +76,22 @@ export const findMarker = (yarray, index) => {
*
* This should be called before doing a deletion!
*
* @param {Array<ArraySearchMarker>} searchMarker
* @param {Transaction} tr
* @param {Array<ListIterator>} searchMarker
* @param {number} index
* @param {number} len If insertion, len is positive. If deletion, len is negative.
*/
export const updateMarkerChanges = (searchMarker, index, len) => {
export const updateMarkerChanges = (tr, searchMarker, index, len) => {
for (let i = searchMarker.length - 1; i >= 0; i--) {
const m = searchMarker[i]
if (len > 0) {
/**
* @type {Item|null}
*/
let p = m.p
p.marker = false
// Ideally we just want to do a simple position comparison, but this will only work if
// search markers don't point to deleted items for formats.
// Iterate marker to prev undeleted countable position so we know what to do when updating a position
while (p && (p.deleted || !p.countable)) {
p = p.left
if (p && !p.deleted && p.countable) {
// adjust position. the loop should break now
m.index -= p.length
}
}
if (p === null || p.marker === true) {
// remove search marker if updated position is null or if position is already marked
searchMarker.splice(i, 1)
continue
}
m.p = p
p.marker = true
const marker = searchMarker[i]
if (len > 0 && index === marker.index) {
// inserting at a marked position deletes the marked position because we can't do a simple transformation
// (we don't know whether to insert directly before or directly after the position)
searchMarker.splice(i, 1)
continue
}
if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice
m.index = math.max(index, m.index + len)
if (index < marker.index) { // a simple index <= m.index check would actually suffice
marker.index = math.max(index, marker.index + len)
}
}
}
@ -284,7 +169,7 @@ export class AbstractType {
*/
this._dEH = createEventHandler()
/**
* @type {null | Array<ArraySearchMarker>}
* @type {null | Array<ListIterator>}
*/
this._searchMarker = null
}
@ -397,135 +282,6 @@ export class AbstractType {
toJSON () {}
}
export class ListPosition {
/**
* @param {AbstractType<any>} type
* @param {Transaction} tr
*/
constructor (type, tr) {
this.type = type
/**
* Current index-position
*/
this.index = 0
/**
* Relative position to the current item (if item.content.length > 1)
*/
this.rel = 0
/**
* This refers to the current right item, unless reachedEnd is true. Then it refers to the left item.
*/
this.item = type._start
this.reachedEnd = type._start === null
/**
* @type {Item | null}
*/
this.currMove = null
/**
* @type {Item | null}
*/
this.currMoveEnd = null
/**
* @type {Array<{ end: Item | null, move: Item }>}
*/
this.movedStack = []
this.tr = tr
}
/**
* @param {number} len
*/
forward (len) {
let item = this.item
this.index += len
if (this.rel) {
len += this.rel
this.rel = 0
}
while (item && !this.reachedEnd && (len > 0 || (len === 0 && (!item.countable || item.deleted)))) {
if (item.countable && !item.deleted && item.moved === this.currMove) {
len -= item.length
if (len < 0) {
this.rel = item.length + len
break
}
} else if (item.content.constructor === ContentMove) {
if (this.currMove) {
this.movedStack.push({ end: this.currMoveEnd, move: this.currMove })
}
const { start, end } = getMovedCoords(item.content, this.tr)
this.currMove = item
this.currMoveEnd = end
this.item = start
continue
}
if (item === this.currMoveEnd) {
this.item = this.currMove // we iterate to the right after the current condition
const { end, move } = this.movedStack.pop() || { end: null, move: null }
this.currMove = move
this.currMoveEnd = end
}
if (item.right) {
item = item.right
} else {
this.reachedEnd = true
}
}
this.index -= len
this.item = item
if (len > 0) {
throw lengthExceeded
}
return this
}
/**
* @param {number} len
*/
slice (len) {
const result = []
while (len > 0 && !this.reachedEnd) {
while (this.item && this.item.countable && !this.reachedEnd && len > 0) {
if (!this.item.deleted) {
const content = this.item.content.getContent()
const slicedContent = content.length <= len && this.rel === 0 ? content : content.slice(this.rel, this.rel + len)
len -= slicedContent.length
result.push(...slicedContent)
if (content.length !== slicedContent.length) {
if (this.rel + slicedContent.length === content.length) {
this.rel = 0
} else {
this.rel += slicedContent.length
continue // do not iterate to item.right
}
}
}
if (this.item.right) {
this.item = this.item.right
} else {
this.reachedEnd = true
}
}
if (this.item && !this.reachedEnd && len > 0) {
this.forward(0)
}
}
return result
}
[Symbol.iterator] () {
return this
}
next () {
const [value] = this.slice(1)
return {
done: value == null,
value: value
}
}
}
/**
* @param {AbstractType<any>} type
* @param {number} start
@ -725,31 +481,6 @@ export const typeListForEachSnapshot = (type, f, snapshot) => {
}
}
/**
* @param {AbstractType<any>} type
* @param {number} index
* @return {any}
*
* @private
* @function
*/
export const typeListGet = (type, index) => {
const marker = findMarker(type, index)
let n = type._start
if (marker !== null) {
n = marker.p
index -= marker.index
}
for (; n !== null; n = n.right) {
if (!n.deleted && n.countable) {
if (index < n.length) {
return n.content.getContent()[index]
}
index -= n.length
}
}
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
@ -814,105 +545,6 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
packJsonContent()
}
const lengthExceeded = error.create('Length exceeded!')
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {number} index
* @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
*
* @private
* @function
*/
export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index > parent._length) {
throw lengthExceeded
}
if (index === 0) {
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, index, content.length)
}
return typeListInsertGenericsAfter(transaction, parent, null, content)
}
const startIndex = index
const marker = findMarker(parent, index)
let n = parent._start
if (marker !== null) {
n = marker.p
index -= marker.index
// we need to iterate one to the left so that the algorithm works
if (index === 0) {
// @todo refactor this as it actually doesn't consider formats
n = n.prev // important! get the left undeleted item so that we can actually decrease index
index += (n && n.countable && !n.deleted) ? n.length : 0
}
}
for (; n !== null; n = n.right) {
if (!n.deleted && n.countable) {
if (index <= n.length) {
if (index < n.length) {
// insert in-between
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
}
break
}
index -= n.length
}
}
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, content.length)
}
return typeListInsertGenericsAfter(transaction, parent, n, content)
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {number} index
* @param {number} length
*
* @private
* @function
*/
export const typeListDelete = (transaction, parent, index, length) => {
if (length === 0) { return }
const startIndex = index
const startLength = length
const marker = findMarker(parent, index)
let n = parent._start
if (marker !== null) {
n = marker.p
index -= marker.index
}
// compute the first item to be deleted
for (; n !== null && index > 0; n = n.right) {
if (!n.deleted && n.countable) {
if (index < n.length) {
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
}
index -= n.length
}
}
// delete all items until done
while (length > 0 && n !== null) {
if (!n.deleted) {
if (length < n.length) {
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
}
n.delete(transaction)
length -= n.length
}
n = n.right
}
if (length > 0) {
throw lengthExceeded
}
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
}
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent

View File

@ -2,19 +2,15 @@
* @module YArray
*/
import { useSearchMarker } from 'tests/testHelper.js'
import {
YEvent,
AbstractType,
typeListForEach,
typeListCreateIterator,
typeListInsertGenerics,
typeListDelete,
typeListMap,
YArrayRefID,
callTypeObservers,
transact,
ListPosition,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
ListIterator,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
} from '../internals.js'
/**
@ -47,7 +43,7 @@ export class YArray extends AbstractType {
*/
this._prelimContent = []
/**
* @type {Array<ArraySearchMarker>}
* @type {Array<ListIterator>}
*/
this._searchMarker = []
}
@ -129,7 +125,9 @@ export class YArray extends AbstractType {
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content)
useSearchMarker(transaction, this, index, walker =>
walker.insertArrayValue(transaction, content)
)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
@ -163,7 +161,9 @@ export class YArray extends AbstractType {
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
useSearchMarker(transaction, this, index, walker =>
walker.delete(transaction, length)
)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
@ -177,8 +177,10 @@ export class YArray extends AbstractType {
* @return {T}
*/
get (index) {
return transact(/** @type {Doc} */ (this.doc), tr =>
new ListPosition(this, tr).forward(index).slice(1)[0]
return transact(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, index, walker =>
walker.slice(transaction, 1)[0]
)
)
}
@ -189,7 +191,7 @@ export class YArray extends AbstractType {
*/
toArray () {
return transact(/** @type {Doc} */ (this.doc), tr =>
new ListPosition(this, tr).slice(this.length)
new ListIterator(this).slice(tr, this.length)
)
}
@ -201,8 +203,10 @@ export class YArray extends AbstractType {
* @return {Array<T>}
*/
slice (start = 0, end = this.length) {
return transact(/** @type {Doc} */ (this.doc), tr =>
new ListPosition(this, tr).forward(start).slice(end < 0 ? this.length + end - start : end - start)
return transact(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, start, walker =>
walker.slice(transaction, end < 0 ? this.length + end - start : end - start)
)
)
}
@ -225,7 +229,9 @@ export class YArray extends AbstractType {
* callback function
*/
map (f) {
return typeListMap(this, /** @type {any} */ (f))
return transact(/** @type {Doc} */ (this.doc), tr =>
new ListIterator(this).map(tr, f)
)
}
/**
@ -234,14 +240,16 @@ export class YArray extends AbstractType {
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f)
return transact(/** @type {Doc} */ (this.doc), tr =>
new ListIterator(this).forEach(tr, f)
)
}
/**
* @return {IterableIterator<T>}
*/
[Symbol.iterator] () {
return typeListCreateIterator(this)
return this.toArray().values()
}
/**

View File

@ -20,14 +20,14 @@ import {
splitSnapshotAffectedStructs,
iterateDeletedStructs,
iterateStructs,
findMarker,
typeMapDelete,
typeMapSet,
typeMapGet,
typeMapGetAll,
updateMarkerChanges,
ContentType,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
useSearchMarker,
ListIterator, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js'
import * as object from 'lib0/object'
@ -125,10 +125,11 @@ const findNextPosition = (transaction, pos, count) => {
*/
const findPosition = (transaction, parent, index) => {
const currentAttributes = new Map()
const marker = findMarker(parent, index)
if (marker) {
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
return findNextPosition(transaction, pos, index - marker.index)
if (parent._searchMarker) {
return useSearchMarker(transaction, parent, index, listIter => {
const pos = new ItemTextListPosition(listIter.left, listIter.right, listIter.index, currentAttributes)
return findNextPosition(transaction, pos, index - listIter.index)
})
} else {
const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes)
return findNextPosition(transaction, pos, index)
@ -264,7 +265,7 @@ const insertText = (transaction, parent, currPos, text, attributes) => {
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text))
let { left, right, index } = currPos
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
updateMarkerChanges(transaction, parent._searchMarker, currPos.index, content.getLength())
}
right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
right.integrate(transaction, 0)
@ -469,7 +470,7 @@ const deleteText = (transaction, currPos, length) => {
}
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length)
updateMarkerChanges(transaction, parent._searchMarker, currPos.index, -startLength + length)
}
return currPos
}
@ -764,7 +765,7 @@ export class YText extends AbstractType {
*/
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
/**
* @type {Array<ArraySearchMarker>}
* @type {Array<ListIterator>}
*/
this._searchMarker = []
}

View File

@ -8,15 +8,13 @@ import {
AbstractType,
typeListMap,
typeListForEach,
typeListInsertGenerics,
typeListInsertGenericsAfter,
typeListDelete,
typeListToArray,
YXmlFragmentRefID,
callTypeObservers,
transact,
typeListGet,
typeListSlice,
useSearchMarker,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js'
@ -304,9 +302,11 @@ export class YXmlFragment extends AbstractType {
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content)
})
return transact(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, index, walker =>
walker.insertArrayValue(transaction, content)
)
)
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
@ -347,9 +347,11 @@ export class YXmlFragment extends AbstractType {
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
})
transact(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, index, walker =>
walker.delete(transaction, length)
)
)
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
@ -390,7 +392,11 @@ export class YXmlFragment extends AbstractType {
* @return {YXmlElement|YXmlText}
*/
get (index) {
return typeListGet(this, index)
return transact(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, index, walker =>
walker.slice(transaction, 1)[0]
)
)
}
/**

435
src/utils/ListIterator.js Normal file
View File

@ -0,0 +1,435 @@
import * as error from 'lib0/error'
import {
getItemCleanStart,
createID,
getMovedCoords,
updateMarkerChanges,
getState,
ContentAny,
ContentBinary,
ContentType,
ContentDoc,
Doc,
ID, AbstractContent, ContentMove, Transaction, Item, AbstractType // eslint-disable-line
} from '../internals.js'
const lengthExceeded = error.create('Length exceeded!')
/**
* @todo rename to walker?
*/
export class ListIterator {
/**
* @param {AbstractType<any>} type
*/
constructor (type) {
this.type = type
/**
* Current index-position
*/
this.index = 0
/**
* Relative position to the current item (if item.content.length > 1)
*/
this.rel = 0
/**
* This refers to the current right item, unless reachedEnd is true. Then it refers to the left item.
*
* @public
* @type {Item | null}
*/
this.nextItem = type._start
this.reachedEnd = type._start === null
/**
* @type {Item | null}
*/
this.currMove = null
/**
* @type {Item | null}
*/
this.currMoveStart = null
/**
* @type {Item | null}
*/
this.currMoveEnd = null
/**
* @type {Array<{ start: Item | null, end: Item | null, move: Item }>}
*/
this.movedStack = []
}
clone () {
const iter = new ListIterator(this.type)
iter.index = this.index
iter.rel = this.rel
iter.nextItem = this.nextItem
iter.reachedEnd = this.reachedEnd
iter.currMove = this.currMove
iter.currMoveStart = this.currMoveStart
iter.currMoveEnd = this.currMoveEnd
iter.movedStack = this.movedStack.slice()
return iter
}
/**
* @type {Item | null}
*/
get left () {
if (this.reachedEnd) {
return this.nextItem
} else {
return this.nextItem && this.nextItem.left
}
}
/**
* @type {Item | null}
*/
get right () {
if (this.reachedEnd) {
return null
} else {
return this.nextItem
}
}
/**
* @param {Transaction} tr
* @param {number} index
*/
moveTo (tr, index) {
const diff = index - this.index
if (diff > 0) {
this.forward(tr, diff)
} else if (diff < 0) {
this.backward(tr, -diff)
}
}
/**
* @param {Transaction} tr
* @param {number} len
*/
forward (tr, len) {
if (this.index + len > this.type._length) {
throw lengthExceeded
}
let item = this.nextItem
this.index += len
if (this.rel) {
len += this.rel
this.rel = 0
}
while (item && !this.reachedEnd && (len > 0 || (len === 0 && (!item.countable || item.deleted)))) {
if (item.countable && !item.deleted && item.moved === this.currMove) {
len -= item.length
if (len < 0) {
this.rel = item.length + len
break
}
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
if (this.currMove) {
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
}
const { start, end } = getMovedCoords(item.content, tr)
this.currMove = item
this.currMoveStart = start
this.currMoveEnd = end
item = start
continue
}
if (item === this.currMoveEnd) {
item = /** @type {Item} */ (this.currMove) // we iterate to the right after the current condition
const { start, end, move } = this.movedStack.pop() || { start: null, end: null, move: null }
this.currMove = move
this.currMoveStart = start
this.currMoveEnd = end
}
if (item.right) {
item = item.right
} else {
this.reachedEnd = true
}
}
this.index -= len
this.nextItem = item
return this
}
/**
* @param {Transaction} tr
* @param {number} len
*/
backward (tr, len) {
if (this.index - len < 0) {
throw lengthExceeded
}
let item = this.nextItem && this.nextItem.left
this.index -= len
if (this.rel) {
len -= this.rel
this.rel = 0
}
while (item && len > 0) {
if (item.countable && !item.deleted && item.moved === this.currMove) {
len -= item.length
if (len < 0) {
this.rel = item.length + len
break
}
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
if (this.currMove) {
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
}
const { start, end } = getMovedCoords(item.content, tr)
this.currMove = item
this.currMoveStart = start
this.currMoveEnd = end
item = start
continue
}
if (item === this.currMoveStart) {
item = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
const { start, end, move } = this.movedStack.pop() || { start: null, end: null, move: null }
this.currMove = move
this.currMoveStart = start
this.currMoveEnd = end
}
item = item.left
}
this.index -= len
this.nextItem = item
return this
}
/**
* @template {{length: number}} T
* @param {Transaction} tr
* @param {number} len
* @param {T} value the initial content
* @param {function(AbstractContent, number, number):T} slice
* @param {function(T, T): T} concat
*/
_slice (tr, len, value, slice, concat) {
while (len > 0 && !this.reachedEnd) {
while (this.nextItem && this.nextItem.countable && !this.reachedEnd && len > 0) {
if (!this.nextItem.deleted) {
const item = this.nextItem
const slicedContent = slice(this.nextItem.content, this.rel, len)
len -= slicedContent.length
value = concat(value, slicedContent)
if (item.length !== slicedContent.length) {
if (this.rel + slicedContent.length === item.length) {
this.rel = 0
} else {
this.rel += slicedContent.length
continue // do not iterate to item.right
}
}
}
if (this.nextItem.right) {
this.nextItem = this.nextItem.right
} else {
this.reachedEnd = true
}
}
if (this.nextItem && !this.reachedEnd && len > 0) {
this.forward(tr, 0)
}
}
return value
}
/**
* @param {Transaction} tr
* @param {number} len
*/
delete (tr, len) {
const startLength = len
const sm = this.type._searchMarker
let item = this.nextItem
while (len > 0 && !this.reachedEnd) {
while (item && item.countable && !this.reachedEnd && len > 0) {
if (!item.deleted) {
if (this.rel > 0) {
item = getItemCleanStart(tr, createID(item.id.client, item.id.clock + this.rel))
this.rel = 0
}
if (len < item.length) {
getItemCleanStart(tr, createID(item.id.client, item.id.clock + len))
}
len -= item.length
item.delete(tr)
}
if (item.right) {
item = item.right
} else {
this.reachedEnd = true
}
}
if (item && !this.reachedEnd && len > 0) {
this.nextItem = item
this.forward(tr, 0)
}
}
this.nextItem = item
if (sm) {
updateMarkerChanges(tr, sm, this.index, -startLength + len)
}
}
/**
* @param {Transaction} tr
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} content
*/
insertArrayValue (tr, content) {
/**
* @type {Item | null}
*/
let item = this.nextItem
if (this.rel > 0) {
/**
* @type {ID}
*/
const itemid = /** @type {Item} */ (item).id
item = getItemCleanStart(tr, createID(itemid.client, itemid.clock + this.rel))
this.rel = 0
}
const parent = this.type
const store = tr.doc.store
const ownClientId = tr.doc.clientID
/**
* @type {Item | null}
*/
const right = this.right
/**
* @type {Item | null}
*/
let left = this.left
/**
* @type {Array<Object|Array<any>|number|null>}
*/
let jsonContent = []
const packJsonContent = () => {
if (jsonContent.length > 0) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
left.integrate(tr, 0)
jsonContent = []
}
}
content.forEach(c => {
if (c === null) {
jsonContent.push(c)
} else {
switch (c.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
jsonContent.push(c)
break
default:
packJsonContent()
switch (c.constructor) {
case Uint8Array:
case ArrayBuffer:
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
left.integrate(tr, 0)
break
case Doc:
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)))
left.integrate(tr, 0)
break
default:
if (c instanceof AbstractType) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
left.integrate(tr, 0)
} else {
throw new Error('Unexpected content type in insert operation')
}
}
}
}
})
packJsonContent()
if (right === null && left !== null) {
item = left
this.reachedEnd = true
} else {
item = right
}
this.nextItem = item
}
/**
* @param {Transaction} tr
* @param {number} len
*/
slice (tr, len) {
return this._slice(tr, len, [], sliceArrayContent, concatArrayContent)
}
/**
* @param {Transaction} tr
* @param {function(any, number, any):void} f
*/
forEach (tr, f) {
for (const val of this.values(tr)) {
f(val, this.index, this.type)
}
}
/**
* @template T
* @param {Transaction} tr
* @param {function(any, number, any):T} f
* @return {Array<T>}
*/
map (tr, f) {
const arr = new Array(this.type._length - this.index)
let i = 0
for (const val of this.values(tr)) {
arr[i++] = f(val, this.index, this.type)
}
return arr
}
/**
* @param {Transaction} tr
*/
values (tr) {
return {
[Symbol.iterator] () {
return this
},
next: () => {
const [value] = this.slice(tr, 1)
return {
done: value == null,
value: value
}
}
}
}
}
/**
* @param {AbstractContent} itemcontent
* @param {number} start
* @param {number} len
*/
const sliceArrayContent = (itemcontent, start, len) => {
const content = itemcontent.getContent()
return content.length <= len && start === 0 ? content : content.slice(start, start + len)
}
/**
* @param {Array<any>} content
* @param {Array<any>} added
*/
const concatArrayContent = (content, added) => {
content.push(added)
return content
}

View File

@ -522,7 +522,7 @@ const arrayTransactions = [
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests6 = tc => {
applyRandomTests(tc, arrayTransactions, 6)
applyRandomTests(tc, arrayTransactions, 10)
}
/**