diff --git a/src/internals.js b/src/internals.js index 77f5a0ec..e84e7ad2 100644 --- a/src/internals.js +++ b/src/internals.js @@ -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' diff --git a/src/structs/Item.js b/src/structs/Item.js index 6cf77e9d..2fdff261 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -581,18 +581,17 @@ export class Item extends AbstractStruct { this.content.constructor === right.content.constructor && this.content.mergeWith(right.content) ) { - const searchMarker = /** @type {AbstractType} */ (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} */ (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 diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index de169271..6e059721 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -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} 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} 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} */ (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} searchMarker + * @param {Transaction} tr + * @param {Array} 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} + * @type {null | Array} */ this._searchMarker = null } @@ -397,135 +282,6 @@ export class AbstractType { toJSON () {} } -export class ListPosition { - /** - * @param {AbstractType} 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} type * @param {number} start @@ -725,31 +481,6 @@ export const typeListForEachSnapshot = (type, f, snapshot) => { } } -/** - * @param {AbstractType} 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} parent @@ -814,105 +545,6 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, packJsonContent() } -const lengthExceeded = error.create('Length exceeded!') - -/** - * @param {Transaction} transaction - * @param {AbstractType} parent - * @param {number} index - * @param {Array|Array|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} 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} parent diff --git a/src/types/YArray.js b/src/types/YArray.js index 52a931ff..cd1cad71 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -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} + * @type {Array} */ 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} */ (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} */ (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} */ 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):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} */ [Symbol.iterator] () { - return typeListCreateIterator(this) + return this.toArray().values() } /** diff --git a/src/types/YText.js b/src/types/YText.js index 328795e0..46beac05 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -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} */ (/** @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} + * @type {Array} */ this._searchMarker = [] } diff --git a/src/types/YXmlFragment.js b/src/types/YXmlFragment.js index c18b9ca0..9f957475 100644 --- a/src/types/YXmlFragment.js +++ b/src/types/YXmlFragment.js @@ -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] + ) + ) } /** diff --git a/src/utils/ListIterator.js b/src/utils/ListIterator.js new file mode 100644 index 00000000..fe6da753 --- /dev/null +++ b/src/utils/ListIterator.js @@ -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} 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|Array|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|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} + */ + 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} content + * @param {Array} added + */ +const concatArrayContent = (content, added) => { + content.push(added) + return content +} diff --git a/tests/y-array.tests.js b/tests/y-array.tests.js index 3a77e6d1..b19750ce 100644 --- a/tests/y-array.tests.js +++ b/tests/y-array.tests.js @@ -522,7 +522,7 @@ const arrayTransactions = [ * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests6 = tc => { - applyRandomTests(tc, arrayTransactions, 6) + applyRandomTests(tc, arrayTransactions, 10) } /**