Improve memory usage by omitting the ItemRef step and directly applying the Item
This commit is contained in:
		
							parent
							
								
									0a0098fdfb
								
							
						
					
					
						commit
						5293ab4df1
					
				@ -39,39 +39,9 @@ export class AbstractStruct {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   */
 | 
			
		||||
  integrate (transaction) {
 | 
			
		||||
    throw error.methodUnimplemented()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AbstractStructRef {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {ID} id
 | 
			
		||||
   */
 | 
			
		||||
  constructor (id) {
 | 
			
		||||
    this.id = id
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Array<ID>}
 | 
			
		||||
     */
 | 
			
		||||
    this._missing = []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @return {Array<ID|null>}
 | 
			
		||||
   */
 | 
			
		||||
  getMissing (transaction) {
 | 
			
		||||
    return this._missing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @param {StructStore} store
 | 
			
		||||
   * @param {number} offset
 | 
			
		||||
   * @return {AbstractStruct}
 | 
			
		||||
   */
 | 
			
		||||
  toStruct (transaction, store, offset) {
 | 
			
		||||
  integrate (transaction, offset) {
 | 
			
		||||
    throw error.methodUnimplemented()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,10 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  AbstractStructRef,
 | 
			
		||||
  AbstractStruct,
 | 
			
		||||
  addStruct,
 | 
			
		||||
  StructStore, Transaction, ID // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as decoding from 'lib0/decoding.js'
 | 
			
		||||
import * as encoding from 'lib0/encoding.js'
 | 
			
		||||
 | 
			
		||||
export const structGCRefNumber = 0
 | 
			
		||||
@ -37,8 +35,13 @@ export class GC extends AbstractStruct {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @param {number} offset
 | 
			
		||||
   */
 | 
			
		||||
  integrate (transaction) {
 | 
			
		||||
  integrate (transaction, offset) {
 | 
			
		||||
    if (offset > 0) {
 | 
			
		||||
      this.id.clock += offset
 | 
			
		||||
      this.length -= offset
 | 
			
		||||
    }
 | 
			
		||||
    addStruct(transaction.doc.store, this)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -50,39 +53,13 @@ export class GC extends AbstractStruct {
 | 
			
		||||
    encoding.writeUint8(encoder, structGCRefNumber)
 | 
			
		||||
    encoding.writeVarUint(encoder, this.length - offset)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export class GCRef extends AbstractStructRef {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {decoding.Decoder} decoder
 | 
			
		||||
   * @param {ID} id
 | 
			
		||||
   * @param {number} info
 | 
			
		||||
   */
 | 
			
		||||
  constructor (decoder, id, info) {
 | 
			
		||||
    super(id)
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {number}
 | 
			
		||||
     */
 | 
			
		||||
    this.length = decoding.readVarUint(decoder)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @param {StructStore} store
 | 
			
		||||
   * @param {number} offset
 | 
			
		||||
   * @return {GC}
 | 
			
		||||
   * @return {null | ID}
 | 
			
		||||
   */
 | 
			
		||||
  toStruct (transaction, store, offset) {
 | 
			
		||||
    if (offset > 0) {
 | 
			
		||||
      this.id.clock += offset
 | 
			
		||||
      this.length -= offset
 | 
			
		||||
    }
 | 
			
		||||
    return new GC(
 | 
			
		||||
      this.id,
 | 
			
		||||
      this.length
 | 
			
		||||
    )
 | 
			
		||||
  getMissing (transaction, store) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ import {
 | 
			
		||||
  writeID,
 | 
			
		||||
  GC,
 | 
			
		||||
  getState,
 | 
			
		||||
  AbstractStructRef,
 | 
			
		||||
  AbstractStruct,
 | 
			
		||||
  replaceStruct,
 | 
			
		||||
  addStruct,
 | 
			
		||||
@ -24,7 +23,7 @@ import {
 | 
			
		||||
  readContentFormat,
 | 
			
		||||
  readContentType,
 | 
			
		||||
  addChangedTypeToTransaction,
 | 
			
		||||
  ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
 | 
			
		||||
  Doc, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as error from 'lib0/error.js'
 | 
			
		||||
@ -73,7 +72,7 @@ export const followRedone = (store, id) => {
 | 
			
		||||
export const keepItem = (item, keep) => {
 | 
			
		||||
  while (item !== null && item.keep !== keep) {
 | 
			
		||||
    item.keep = keep
 | 
			
		||||
    item = item.parent._item
 | 
			
		||||
    item = /** @type {AbstractType<any>} */ (item.parent)._item
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -119,7 +118,7 @@ export const splitItem = (transaction, leftItem, diff) => {
 | 
			
		||||
  transaction._mergeStructs.push(rightItem)
 | 
			
		||||
  // update parent._map
 | 
			
		||||
  if (rightItem.parentSub !== null && rightItem.right === null) {
 | 
			
		||||
    rightItem.parent._map.set(rightItem.parentSub, rightItem)
 | 
			
		||||
    /** @type {AbstractType<any>} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
 | 
			
		||||
  }
 | 
			
		||||
  leftItem.length = diff
 | 
			
		||||
  return rightItem
 | 
			
		||||
@ -144,7 +143,7 @@ export const redoItem = (transaction, item, redoitems) => {
 | 
			
		||||
  if (redone !== null) {
 | 
			
		||||
    return getItemCleanStart(transaction, redone)
 | 
			
		||||
  }
 | 
			
		||||
  let parentItem = item.parent._item
 | 
			
		||||
  let parentItem = /** @type {AbstractType<any>} */ (item.parent)._item
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Item|null}
 | 
			
		||||
   */
 | 
			
		||||
@ -169,7 +168,7 @@ export const redoItem = (transaction, item, redoitems) => {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (left.right !== null) {
 | 
			
		||||
      left = /** @type {Item} */ (item.parent._map.get(item.parentSub))
 | 
			
		||||
      left = /** @type {Item} */ (/** @type {AbstractType<any>} */ (item.parent)._map.get(item.parentSub))
 | 
			
		||||
    }
 | 
			
		||||
    right = null
 | 
			
		||||
  }
 | 
			
		||||
@ -191,10 +190,10 @@ export const redoItem = (transaction, item, redoitems) => {
 | 
			
		||||
       */
 | 
			
		||||
      let leftTrace = left
 | 
			
		||||
      // trace redone until parent matches
 | 
			
		||||
      while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
 | 
			
		||||
      while (leftTrace !== null && /** @type {AbstractType<any>} */ (leftTrace.parent)._item !== parentItem) {
 | 
			
		||||
        leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
 | 
			
		||||
      }
 | 
			
		||||
      if (leftTrace !== null && leftTrace.parent._item === parentItem) {
 | 
			
		||||
      if (leftTrace !== null && /** @type {AbstractType<any>} */ (leftTrace.parent)._item === parentItem) {
 | 
			
		||||
        left = leftTrace
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
@ -206,10 +205,10 @@ export const redoItem = (transaction, item, redoitems) => {
 | 
			
		||||
       */
 | 
			
		||||
      let rightTrace = right
 | 
			
		||||
      // trace redone until parent matches
 | 
			
		||||
      while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
 | 
			
		||||
      while (rightTrace !== null && /** @type {AbstractType<any>} */ (rightTrace.parent)._item !== parentItem) {
 | 
			
		||||
        rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
 | 
			
		||||
      }
 | 
			
		||||
      if (rightTrace !== null && rightTrace.parent._item === parentItem) {
 | 
			
		||||
      if (rightTrace !== null && /** @type {AbstractType<any>} */ (rightTrace.parent)._item === parentItem) {
 | 
			
		||||
        right = rightTrace
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
@ -228,7 +227,7 @@ export const redoItem = (transaction, item, redoitems) => {
 | 
			
		||||
  )
 | 
			
		||||
  item.redone = nextId
 | 
			
		||||
  keepItem(redoneItem, true)
 | 
			
		||||
  redoneItem.integrate(transaction)
 | 
			
		||||
  redoneItem.integrate(transaction, 0)
 | 
			
		||||
  return redoneItem
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -242,7 +241,7 @@ export class Item extends AbstractStruct {
 | 
			
		||||
   * @param {ID | null} origin
 | 
			
		||||
   * @param {Item | null} right
 | 
			
		||||
   * @param {ID | null} rightOrigin
 | 
			
		||||
   * @param {AbstractType<any>} parent
 | 
			
		||||
   * @param {AbstractType<any>|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
 | 
			
		||||
   * @param {string | null} parentSub
 | 
			
		||||
   * @param {AbstractContent} content
 | 
			
		||||
   */
 | 
			
		||||
@ -251,7 +250,6 @@ export class Item extends AbstractStruct {
 | 
			
		||||
    /**
 | 
			
		||||
     * The item that was originally to the left of this item.
 | 
			
		||||
     * @type {ID | null}
 | 
			
		||||
     * @readonly
 | 
			
		||||
     */
 | 
			
		||||
    this.origin = origin
 | 
			
		||||
    /**
 | 
			
		||||
@ -266,14 +264,11 @@ export class Item extends AbstractStruct {
 | 
			
		||||
    this.right = right
 | 
			
		||||
    /**
 | 
			
		||||
     * The item that was originally to the right of this item.
 | 
			
		||||
     * @readonly
 | 
			
		||||
     * @type {ID | null}
 | 
			
		||||
     */
 | 
			
		||||
    this.rightOrigin = rightOrigin
 | 
			
		||||
    /**
 | 
			
		||||
     * The parent type.
 | 
			
		||||
     * @type {AbstractType<any>}
 | 
			
		||||
     * @readonly
 | 
			
		||||
     * @type {AbstractType<any>|ID|null}
 | 
			
		||||
     */
 | 
			
		||||
    this.parent = parent
 | 
			
		||||
    /**
 | 
			
		||||
@ -282,7 +277,6 @@ export class Item extends AbstractStruct {
 | 
			
		||||
     * to insert this item. If `parentSub = null` type._start is the list in
 | 
			
		||||
     * which to insert to. Otherwise it is `parent._map`.
 | 
			
		||||
     * @type {String | null}
 | 
			
		||||
     * @readonly
 | 
			
		||||
     */
 | 
			
		||||
    this.parentSub = parentSub
 | 
			
		||||
    /**
 | 
			
		||||
@ -311,104 +305,178 @@ export class Item extends AbstractStruct {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Return missing ids, or define missing items and return null.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @param {StructStore} store
 | 
			
		||||
   * @return {null | ID}
 | 
			
		||||
   */
 | 
			
		||||
  integrate (transaction) {
 | 
			
		||||
  getMissing (transaction, store) {
 | 
			
		||||
    const origin = this.origin
 | 
			
		||||
    const rightOrigin = this.rightOrigin
 | 
			
		||||
    const parent = /** @type {ID} */ (this.parent)
 | 
			
		||||
 | 
			
		||||
    if (origin && origin.clock >= getState(store, origin.client)) {
 | 
			
		||||
      return this.origin
 | 
			
		||||
    }
 | 
			
		||||
    if (rightOrigin && rightOrigin.clock >= getState(store, rightOrigin.client)) {
 | 
			
		||||
      return this.rightOrigin
 | 
			
		||||
    }
 | 
			
		||||
    if (parent && parent.constructor === ID && parent.clock >= getState(store, parent.client)) {
 | 
			
		||||
      return parent
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We have all missing ids, now find the items
 | 
			
		||||
 | 
			
		||||
    if (origin) {
 | 
			
		||||
      this.left = getItemCleanEnd(transaction, store, origin)
 | 
			
		||||
      this.origin = this.left.lastId
 | 
			
		||||
    }
 | 
			
		||||
    if (rightOrigin) {
 | 
			
		||||
      this.right = getItemCleanStart(transaction, rightOrigin)
 | 
			
		||||
      this.rightOrigin = this.right.id
 | 
			
		||||
    }
 | 
			
		||||
    if (parent && parent.constructor === ID) {
 | 
			
		||||
      if (parent.clock < getState(store, parent.client)) {
 | 
			
		||||
        const parentItem = getItem(store, parent)
 | 
			
		||||
        if (parentItem.constructor === GC) {
 | 
			
		||||
          this.parent = null
 | 
			
		||||
        } else {
 | 
			
		||||
          this.parent = /** @type {ContentType} */ (parentItem.content).type
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        return parent
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // only set item if this shouldn't be garbage collected
 | 
			
		||||
    if (!this.parent) {
 | 
			
		||||
      if (this.left && this.left.constructor === Item) {
 | 
			
		||||
        this.parent = this.left.parent
 | 
			
		||||
        this.parentSub = this.left.parentSub
 | 
			
		||||
      }
 | 
			
		||||
      if (this.right && this.right.constructor === Item) {
 | 
			
		||||
        this.parent = this.right.parent
 | 
			
		||||
        this.parentSub = this.right.parentSub
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @param {number} offset
 | 
			
		||||
   */
 | 
			
		||||
  integrate (transaction, offset) {
 | 
			
		||||
    const store = transaction.doc.store
 | 
			
		||||
    const parent = this.parent
 | 
			
		||||
    if (offset > 0) {
 | 
			
		||||
      this.id.clock += offset
 | 
			
		||||
      this.left = getItemCleanEnd(transaction, store, createID(this.id.client, this.id.clock - 1))
 | 
			
		||||
      this.origin = this.left.lastId
 | 
			
		||||
      this.content = this.content.splice(offset)
 | 
			
		||||
      this.length -= offset
 | 
			
		||||
    }
 | 
			
		||||
    const parentSub = this.parentSub
 | 
			
		||||
    const length = this.length
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Item|null}
 | 
			
		||||
     */
 | 
			
		||||
    let left = this.left
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Item|null}
 | 
			
		||||
     */
 | 
			
		||||
    let o
 | 
			
		||||
    // set o to the first conflicting item
 | 
			
		||||
    if (left !== null) {
 | 
			
		||||
      o = left.right
 | 
			
		||||
    } else if (parentSub !== null) {
 | 
			
		||||
      o = parent._map.get(parentSub) || null
 | 
			
		||||
      while (o !== null && o.left !== null) {
 | 
			
		||||
        o = o.left
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      o = parent._start
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: use something like DeleteSet here (a tree implementation would be best)
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Set<Item>}
 | 
			
		||||
     */
 | 
			
		||||
    const conflictingItems = new Set()
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Set<Item>}
 | 
			
		||||
     */
 | 
			
		||||
    const itemsBeforeOrigin = new Set()
 | 
			
		||||
    // Let c in conflictingItems, b in itemsBeforeOrigin
 | 
			
		||||
    // ***{origin}bbbb{this}{c,b}{c,b}{o}***
 | 
			
		||||
    // Note that conflictingItems is a subset of itemsBeforeOrigin
 | 
			
		||||
    while (o !== null && o !== this.right) {
 | 
			
		||||
      itemsBeforeOrigin.add(o)
 | 
			
		||||
      conflictingItems.add(o)
 | 
			
		||||
      if (compareIDs(this.origin, o.origin)) {
 | 
			
		||||
        // case 1
 | 
			
		||||
        if (o.id.client < this.id.client) {
 | 
			
		||||
          left = o
 | 
			
		||||
          conflictingItems.clear()
 | 
			
		||||
        }
 | 
			
		||||
      } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
 | 
			
		||||
        // case 2
 | 
			
		||||
        if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
 | 
			
		||||
          left = o
 | 
			
		||||
          conflictingItems.clear()
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
      o = o.right
 | 
			
		||||
    }
 | 
			
		||||
    this.left = left
 | 
			
		||||
    // reconnect left/right + update parent map/start if necessary
 | 
			
		||||
    if (left !== null) {
 | 
			
		||||
      const right = left.right
 | 
			
		||||
      this.right = right
 | 
			
		||||
      left.right = this
 | 
			
		||||
    } else {
 | 
			
		||||
      let r
 | 
			
		||||
      if (parentSub !== null) {
 | 
			
		||||
        r = parent._map.get(parentSub) || null
 | 
			
		||||
        while (r !== null && r.left !== null) {
 | 
			
		||||
          r = r.left
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        r = parent._start
 | 
			
		||||
        parent._start = this
 | 
			
		||||
      }
 | 
			
		||||
      this.right = r
 | 
			
		||||
    }
 | 
			
		||||
    if (this.right !== null) {
 | 
			
		||||
      this.right.left = this
 | 
			
		||||
    } else if (parentSub !== null) {
 | 
			
		||||
      // set as current parent value if right === null and this is parentSub
 | 
			
		||||
      parent._map.set(parentSub, this)
 | 
			
		||||
    const parent = /** @type {AbstractType<any>|null} */ (this.parent)
 | 
			
		||||
 | 
			
		||||
    if (parent) {
 | 
			
		||||
      /**
 | 
			
		||||
       * @type {Item|null}
 | 
			
		||||
       */
 | 
			
		||||
      let left = this.left
 | 
			
		||||
 | 
			
		||||
      /**
 | 
			
		||||
       * @type {Item|null}
 | 
			
		||||
       */
 | 
			
		||||
      let o
 | 
			
		||||
      // set o to the first conflicting item
 | 
			
		||||
      if (left !== null) {
 | 
			
		||||
        // this is the current attribute value of parent. delete right
 | 
			
		||||
        left.delete(transaction)
 | 
			
		||||
        o = left.right
 | 
			
		||||
      } else if (parentSub !== null) {
 | 
			
		||||
        o = parent._map.get(parentSub) || null
 | 
			
		||||
        while (o !== null && o.left !== null) {
 | 
			
		||||
          o = o.left
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        o = parent._start
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // adjust length of parent
 | 
			
		||||
    if (parentSub === null && this.countable && !this.deleted) {
 | 
			
		||||
      parent._length += length
 | 
			
		||||
    }
 | 
			
		||||
    addStruct(store, this)
 | 
			
		||||
    this.content.integrate(transaction, this)
 | 
			
		||||
    // add parent to transaction.changed
 | 
			
		||||
    addChangedTypeToTransaction(transaction, parent, parentSub)
 | 
			
		||||
    if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
 | 
			
		||||
      // delete if parent is deleted or if this is not the current attribute value of parent
 | 
			
		||||
      this.delete(transaction)
 | 
			
		||||
      // TODO: use something like DeleteSet here (a tree implementation would be best)
 | 
			
		||||
      // @todo use global set definitions
 | 
			
		||||
      /**
 | 
			
		||||
       * @type {Set<Item>}
 | 
			
		||||
       */
 | 
			
		||||
      const conflictingItems = new Set()
 | 
			
		||||
      /**
 | 
			
		||||
       * @type {Set<Item>}
 | 
			
		||||
       */
 | 
			
		||||
      const itemsBeforeOrigin = new Set()
 | 
			
		||||
      // Let c in conflictingItems, b in itemsBeforeOrigin
 | 
			
		||||
      // ***{origin}bbbb{this}{c,b}{c,b}{o}***
 | 
			
		||||
      // Note that conflictingItems is a subset of itemsBeforeOrigin
 | 
			
		||||
      while (o !== null && o !== this.right) {
 | 
			
		||||
        itemsBeforeOrigin.add(o)
 | 
			
		||||
        conflictingItems.add(o)
 | 
			
		||||
        if (compareIDs(this.origin, o.origin)) {
 | 
			
		||||
          // case 1
 | 
			
		||||
          if (o.id.client < this.id.client) {
 | 
			
		||||
            left = o
 | 
			
		||||
            conflictingItems.clear()
 | 
			
		||||
          }
 | 
			
		||||
        } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
 | 
			
		||||
          // case 2
 | 
			
		||||
          if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
 | 
			
		||||
            left = o
 | 
			
		||||
            conflictingItems.clear()
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        o = o.right
 | 
			
		||||
      }
 | 
			
		||||
      this.left = left
 | 
			
		||||
      // reconnect left/right + update parent map/start if necessary
 | 
			
		||||
      if (left !== null) {
 | 
			
		||||
        const right = left.right
 | 
			
		||||
        this.right = right
 | 
			
		||||
        left.right = this
 | 
			
		||||
      } else {
 | 
			
		||||
        let r
 | 
			
		||||
        if (parentSub !== null) {
 | 
			
		||||
          r = parent._map.get(parentSub) || null
 | 
			
		||||
          while (r !== null && r.left !== null) {
 | 
			
		||||
            r = r.left
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          r = parent._start
 | 
			
		||||
          parent._start = this
 | 
			
		||||
        }
 | 
			
		||||
        this.right = r
 | 
			
		||||
      }
 | 
			
		||||
      if (this.right !== null) {
 | 
			
		||||
        this.right.left = this
 | 
			
		||||
      } else if (parentSub !== null) {
 | 
			
		||||
        // set as current parent value if right === null and this is parentSub
 | 
			
		||||
        parent._map.set(parentSub, this)
 | 
			
		||||
        if (left !== null) {
 | 
			
		||||
          // this is the current attribute value of parent. delete right
 | 
			
		||||
          left.delete(transaction)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // adjust length of parent
 | 
			
		||||
      if (parentSub === null && this.countable && !this.deleted) {
 | 
			
		||||
        parent._length += length
 | 
			
		||||
      }
 | 
			
		||||
      addStruct(store, this)
 | 
			
		||||
      this.content.integrate(transaction, this)
 | 
			
		||||
      // add parent to transaction.changed
 | 
			
		||||
      addChangedTypeToTransaction(transaction, parent, parentSub)
 | 
			
		||||
      if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
 | 
			
		||||
        // delete if parent is deleted or if this is not the current attribute value of parent
 | 
			
		||||
        this.delete(transaction)
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // parent is not defined. Integrate GC struct instead
 | 
			
		||||
      new GC(this.id, this.length).integrate(transaction, 0)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -481,7 +549,7 @@ export class Item extends AbstractStruct {
 | 
			
		||||
   */
 | 
			
		||||
  delete (transaction) {
 | 
			
		||||
    if (!this.deleted) {
 | 
			
		||||
      const parent = this.parent
 | 
			
		||||
      const parent = /** @type {AbstractType<any>} */ (this.parent)
 | 
			
		||||
      // adjust the length of parent
 | 
			
		||||
      if (this.countable && this.parentSub === null) {
 | 
			
		||||
        parent._length -= this.length
 | 
			
		||||
@ -534,7 +602,7 @@ export class Item extends AbstractStruct {
 | 
			
		||||
      writeID(encoder, rightOrigin)
 | 
			
		||||
    }
 | 
			
		||||
    if (origin === null && rightOrigin === null) {
 | 
			
		||||
      const parent = this.parent
 | 
			
		||||
      const parent = /** @type {AbstractType<any>} */ (this.parent)
 | 
			
		||||
      const parentItem = parent._item
 | 
			
		||||
      if (parentItem === null) {
 | 
			
		||||
        // parent type on y._map
 | 
			
		||||
@ -670,122 +738,49 @@ export class AbstractContent {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * @param {decoding.Decoder} decoder
 | 
			
		||||
 * @param {ID} id
 | 
			
		||||
 * @param {number} info
 | 
			
		||||
 * @param {Doc} doc
 | 
			
		||||
 */
 | 
			
		||||
export class ItemRef extends AbstractStructRef {
 | 
			
		||||
export const readItem = (decoder, id, info, doc) => {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {decoding.Decoder} decoder
 | 
			
		||||
   * @param {ID} id
 | 
			
		||||
   * @param {number} info
 | 
			
		||||
   * The item that was originally to the left of this item.
 | 
			
		||||
   * @type {ID | null}
 | 
			
		||||
   */
 | 
			
		||||
  constructor (decoder, id, info) {
 | 
			
		||||
    super(id)
 | 
			
		||||
    /**
 | 
			
		||||
     * The item that was originally to the left of this item.
 | 
			
		||||
     * @type {ID | null}
 | 
			
		||||
     */
 | 
			
		||||
    this.left = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
 | 
			
		||||
    /**
 | 
			
		||||
     * The item that was originally to the right of this item.
 | 
			
		||||
     * @type {ID | null}
 | 
			
		||||
     */
 | 
			
		||||
    this.right = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
 | 
			
		||||
    const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
 | 
			
		||||
    const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false
 | 
			
		||||
    /**
 | 
			
		||||
     * If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
 | 
			
		||||
     * and we read the next string as parentYKey.
 | 
			
		||||
     * It indicates how we store/retrieve parent from `y.share`
 | 
			
		||||
     * @type {string|null}
 | 
			
		||||
     */
 | 
			
		||||
    this.parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
 | 
			
		||||
    /**
 | 
			
		||||
     * The parent type.
 | 
			
		||||
     * @type {ID | null}
 | 
			
		||||
     */
 | 
			
		||||
    this.parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : null
 | 
			
		||||
    /**
 | 
			
		||||
     * If the parent refers to this item with some kind of key (e.g. YMap, the
 | 
			
		||||
     * key is specified here. The key is then used to refer to the list in which
 | 
			
		||||
     * to insert this item. If `parentSub = null` type._start is the list in
 | 
			
		||||
     * which to insert to. Otherwise it is `parent._map`.
 | 
			
		||||
     * @type {String | null}
 | 
			
		||||
     */
 | 
			
		||||
    this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
 | 
			
		||||
    const missing = this._missing
 | 
			
		||||
    // Only add items to missing if they don't preceed this item (indicating that it has already been added).
 | 
			
		||||
    // @todo Creating missing items could be done outside this constructor
 | 
			
		||||
    if (this.left !== null && this.left.client !== id.client) {
 | 
			
		||||
      missing.push(this.left)
 | 
			
		||||
    }
 | 
			
		||||
    if (this.right !== null && this.right.client !== id.client) {
 | 
			
		||||
      missing.push(this.right)
 | 
			
		||||
    }
 | 
			
		||||
    if (this.parent !== null) {
 | 
			
		||||
      missing.push(this.parent)
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {AbstractContent}
 | 
			
		||||
     */
 | 
			
		||||
    this.content = readItemContent(decoder, info)
 | 
			
		||||
    this.length = this.content.getLength()
 | 
			
		||||
  }
 | 
			
		||||
  const origin = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
 | 
			
		||||
  /**
 | 
			
		||||
   * The item that was originally to the right of this item.
 | 
			
		||||
   * @type {ID | null}
 | 
			
		||||
   */
 | 
			
		||||
  const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
 | 
			
		||||
  const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
 | 
			
		||||
  const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false
 | 
			
		||||
  /**
 | 
			
		||||
   * If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
 | 
			
		||||
   * and we read the next string as parentYKey.
 | 
			
		||||
   * It indicates how we store/retrieve parent from `y.share`
 | 
			
		||||
   * @type {string|null}
 | 
			
		||||
   */
 | 
			
		||||
  const parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
 | 
			
		||||
  /**
 | 
			
		||||
   * The parent type.
 | 
			
		||||
   * @type {ID | AbstractType<any> | null}
 | 
			
		||||
   */
 | 
			
		||||
  const parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : (parentYKey ? doc.get(parentYKey) : null)
 | 
			
		||||
  /**
 | 
			
		||||
   * If the parent refers to this item with some kind of key (e.g. YMap, the
 | 
			
		||||
   * key is specified here. The key is then used to refer to the list in which
 | 
			
		||||
   * to insert this item. If `parentSub = null` type._start is the list in
 | 
			
		||||
   * which to insert to. Otherwise it is `parent._map`.
 | 
			
		||||
   * @type {String | null}
 | 
			
		||||
   */
 | 
			
		||||
  const parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @param {StructStore} store
 | 
			
		||||
   * @param {number} offset
 | 
			
		||||
   * @return {Item|GC}
 | 
			
		||||
   * @type {AbstractContent}
 | 
			
		||||
   */
 | 
			
		||||
  toStruct (transaction, store, offset) {
 | 
			
		||||
    if (offset > 0) {
 | 
			
		||||
      this.id.clock += offset
 | 
			
		||||
      this.left = createID(this.id.client, this.id.clock - 1)
 | 
			
		||||
      this.content = this.content.splice(offset)
 | 
			
		||||
      this.length -= offset
 | 
			
		||||
    }
 | 
			
		||||
  const content = readItemContent(decoder, info)
 | 
			
		||||
 | 
			
		||||
    const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
 | 
			
		||||
    const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
 | 
			
		||||
    const parentId = this.parent
 | 
			
		||||
    let parent = null
 | 
			
		||||
    let parentSub = this.parentSub
 | 
			
		||||
    if (parentId !== null) {
 | 
			
		||||
      const parentItem = getItem(store, parentId)
 | 
			
		||||
      // Edge case: toStruct is called with an offset > 0. In this case left is defined.
 | 
			
		||||
      // Depending in which order structs arrive, left may be GC'd and the parent not
 | 
			
		||||
      // deleted. This is why we check if left is GC'd. Strictly we don't have
 | 
			
		||||
      // to check if right is GC'd, but we will in case we run into future issues
 | 
			
		||||
      if (!parentItem.deleted && (left === null || left.constructor !== GC) && (right === null || right.constructor !== GC)) {
 | 
			
		||||
        parent = /** @type {ContentType} */ (parentItem.content).type
 | 
			
		||||
      }
 | 
			
		||||
    } else if (this.parentYKey !== null) {
 | 
			
		||||
      parent = transaction.doc.get(this.parentYKey)
 | 
			
		||||
    } else if (left !== null) {
 | 
			
		||||
      if (left.constructor !== GC) {
 | 
			
		||||
        parent = left.parent
 | 
			
		||||
        parentSub = left.parentSub
 | 
			
		||||
      }
 | 
			
		||||
    } else if (right !== null) {
 | 
			
		||||
      if (right.constructor !== GC) {
 | 
			
		||||
        parent = right.parent
 | 
			
		||||
        parentSub = right.parentSub
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      throw error.unexpectedCase()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return parent === null
 | 
			
		||||
      ? new GC(this.id, this.length)
 | 
			
		||||
      : new Item(
 | 
			
		||||
        this.id,
 | 
			
		||||
        left,
 | 
			
		||||
        left && left.lastId,
 | 
			
		||||
        right,
 | 
			
		||||
        right && right.id,
 | 
			
		||||
        parent,
 | 
			
		||||
        parentSub,
 | 
			
		||||
        this.content
 | 
			
		||||
      )
 | 
			
		||||
  }
 | 
			
		||||
  return new Item(id, null, origin, null, rightOrigin, parent, parentSub, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ export const callTypeObservers = (type, transaction, event) => {
 | 
			
		||||
    if (type._item === null) {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
    type = type._item.parent
 | 
			
		||||
    type = /** @type {AbstractType<any>} */ (type._item.parent)
 | 
			
		||||
  }
 | 
			
		||||
  callEventHandlerListeners(changedType._eH, event, transaction)
 | 
			
		||||
}
 | 
			
		||||
@ -386,7 +386,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
 | 
			
		||||
  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(transaction)
 | 
			
		||||
      left.integrate(transaction, 0)
 | 
			
		||||
      jsonContent = []
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -405,12 +405,12 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
 | 
			
		||||
          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(transaction)
 | 
			
		||||
            left.integrate(transaction, 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(transaction)
 | 
			
		||||
              left.integrate(transaction, 0)
 | 
			
		||||
            } else {
 | 
			
		||||
              throw new Error('Unexpected content type in insert operation')
 | 
			
		||||
            }
 | 
			
		||||
@ -537,7 +537,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction)
 | 
			
		||||
  new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -153,7 +153,7 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
 | 
			
		||||
  const ownClientId = doc.clientID
 | 
			
		||||
  for (const [key, val] of negatedAttributes) {
 | 
			
		||||
    left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
 | 
			
		||||
    left.integrate(transaction)
 | 
			
		||||
    left.integrate(transaction, 0)
 | 
			
		||||
  }
 | 
			
		||||
  currPos.left = left
 | 
			
		||||
  currPos.right = right
 | 
			
		||||
@ -228,7 +228,7 @@ const insertAttributes = (transaction, parent, currPos, currentAttributes, attri
 | 
			
		||||
      negatedAttributes.set(key, currentVal)
 | 
			
		||||
      const { left, right } = currPos
 | 
			
		||||
      currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
 | 
			
		||||
      currPos.left.integrate(transaction)
 | 
			
		||||
      currPos.left.integrate(transaction, 0)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return negatedAttributes
 | 
			
		||||
@ -259,7 +259,7 @@ const insertText = (transaction, parent, currPos, currentAttributes, text, attri
 | 
			
		||||
  const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
 | 
			
		||||
  const { left, right } = currPos
 | 
			
		||||
  currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
 | 
			
		||||
  currPos.left.integrate(transaction)
 | 
			
		||||
  currPos.left.integrate(transaction, 0)
 | 
			
		||||
  return insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -320,7 +320,7 @@ const formatText = (transaction, parent, currPos, currentAttributes, length, att
 | 
			
		||||
      newlines += '\n'
 | 
			
		||||
    }
 | 
			
		||||
    left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentString(newlines))
 | 
			
		||||
    left.integrate(transaction)
 | 
			
		||||
    left.integrate(transaction, 0)
 | 
			
		||||
  }
 | 
			
		||||
  currPos.left = left
 | 
			
		||||
  currPos.right = right
 | 
			
		||||
 | 
			
		||||
@ -97,7 +97,7 @@ export class YXmlTreeWalker {
 | 
			
		||||
            } else if (n.parent === this._root) {
 | 
			
		||||
              n = null
 | 
			
		||||
            } else {
 | 
			
		||||
              n = n.parent._item
 | 
			
		||||
              n = /** @type {AbstractType<any>} */ (n.parent)._item
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -227,7 +227,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
 | 
			
		||||
    if (!(right instanceof Item)) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    type = right.parent
 | 
			
		||||
    type = /** @type {AbstractType<any>} */ (right.parent)
 | 
			
		||||
    if (type._item === null || !type._item.deleted) {
 | 
			
		||||
      index = right.deleted || !right.countable ? 0 : res.diff
 | 
			
		||||
      let n = right.left
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
import {
 | 
			
		||||
  GC,
 | 
			
		||||
  splitItem,
 | 
			
		||||
  AbstractStruct, GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
 | 
			
		||||
  AbstractStruct, Transaction, ID, Item // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as math from 'lib0/math.js'
 | 
			
		||||
@ -21,13 +21,13 @@ export class StructStore {
 | 
			
		||||
     * We could shift the array of refs instead, but shift is incredible
 | 
			
		||||
     * slow in Chrome for arrays with more than 100k elements
 | 
			
		||||
     * @see tryResumePendingStructRefs
 | 
			
		||||
     * @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
 | 
			
		||||
     * @type {Map<number,{i:number,refs:Array<GC|Item>}>}
 | 
			
		||||
     */
 | 
			
		||||
    this.pendingClientsStructRefs = new Map()
 | 
			
		||||
    /**
 | 
			
		||||
     * Stack of pending structs waiting for struct dependencies
 | 
			
		||||
     * Maximum length of stack is structReaders.size
 | 
			
		||||
     * @type {Array<GCRef|ItemRef>}
 | 
			
		||||
     * @type {Array<GC|Item>}
 | 
			
		||||
     */
 | 
			
		||||
    this.pendingStack = []
 | 
			
		||||
    /**
 | 
			
		||||
@ -129,7 +129,10 @@ export const findIndexSS = (structs, clock) => {
 | 
			
		||||
  if (mid.id.clock === clock) {
 | 
			
		||||
    return right
 | 
			
		||||
  }
 | 
			
		||||
  let midindex = math.floor((clock / (midclock + mid.length)) * right) // pivoting the search
 | 
			
		||||
  // @todo does it even make sense to pivot the search?
 | 
			
		||||
  // If a good split misses, it might actually increase the time to find the correct item.
 | 
			
		||||
  // Currently, the only advantage is that search with pivoting might find the item on the first try.
 | 
			
		||||
  let midindex = math.floor((clock / (midclock + mid.length - 1)) * right) // pivoting the search
 | 
			
		||||
  while (left <= right) {
 | 
			
		||||
    mid = structs[midindex]
 | 
			
		||||
    midclock = mid.id.clock
 | 
			
		||||
 | 
			
		||||
@ -156,8 +156,8 @@ const tryToMergeWithLeft = (structs, pos) => {
 | 
			
		||||
  if (left.deleted === right.deleted && left.constructor === right.constructor) {
 | 
			
		||||
    if (left.mergeWith(right)) {
 | 
			
		||||
      structs.splice(pos, 1)
 | 
			
		||||
      if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
 | 
			
		||||
        right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
 | 
			
		||||
      if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
 | 
			
		||||
        /** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -211,7 +211,7 @@ const getPathTo = (parent, child) => {
 | 
			
		||||
    } else {
 | 
			
		||||
      // parent is array-ish
 | 
			
		||||
      let i = 0
 | 
			
		||||
      let c = child._item.parent._start
 | 
			
		||||
      let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
 | 
			
		||||
      while (c !== child._item && c !== null) {
 | 
			
		||||
        if (!c.deleted) {
 | 
			
		||||
          i++
 | 
			
		||||
@ -220,7 +220,7 @@ const getPathTo = (parent, child) => {
 | 
			
		||||
      }
 | 
			
		||||
      path.unshift(i)
 | 
			
		||||
    }
 | 
			
		||||
    child = child._item.parent
 | 
			
		||||
    child = /** @type {AbstractType<any>} */ (child._item.parent)
 | 
			
		||||
  }
 | 
			
		||||
  return path
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,8 +16,6 @@
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  findIndexSS,
 | 
			
		||||
  GCRef,
 | 
			
		||||
  ItemRef,
 | 
			
		||||
  writeID,
 | 
			
		||||
  readID,
 | 
			
		||||
  getState,
 | 
			
		||||
@ -27,6 +25,7 @@ import {
 | 
			
		||||
  writeDeleteSet,
 | 
			
		||||
  createDeleteSetFromStructStore,
 | 
			
		||||
  transact,
 | 
			
		||||
  readItem,
 | 
			
		||||
  Doc, Transaction, GC, Item, StructStore, ID // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
@ -80,7 +79,9 @@ export const writeClientsStructs = (encoder, store, _sm) => {
 | 
			
		||||
  })
 | 
			
		||||
  // write # states that were updated
 | 
			
		||||
  encoding.writeVarUint(encoder, sm.size)
 | 
			
		||||
  sm.forEach((clock, client) => {
 | 
			
		||||
  // Write items with higher client ids first
 | 
			
		||||
  // This heavily improves the conflict algorithm.
 | 
			
		||||
  Array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    writeStructs(encoder, store.clients.get(client), client, clock)
 | 
			
		||||
  })
 | 
			
		||||
@ -88,13 +89,14 @@ export const writeClientsStructs = (encoder, store, _sm) => {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {decoding.Decoder} decoder The decoder object to read data from.
 | 
			
		||||
 * @param {Map<number,Array<GCRef|ItemRef>>} clientRefs
 | 
			
		||||
 * @return {Map<number,Array<GCRef|ItemRef>>}
 | 
			
		||||
 * @param {Map<number,Array<GC|Item>>} clientRefs
 | 
			
		||||
 * @param {Doc} doc
 | 
			
		||||
 * @return {Map<number,Array<GC|Item>>}
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 * @function
 | 
			
		||||
 */
 | 
			
		||||
export const readClientsStructRefs = (decoder, clientRefs) => {
 | 
			
		||||
export const readClientsStructRefs = (decoder, clientRefs, doc) => {
 | 
			
		||||
  const numOfStateUpdates = decoding.readVarUint(decoder)
 | 
			
		||||
  for (let i = 0; i < numOfStateUpdates; i++) {
 | 
			
		||||
    const numberOfStructs = decoding.readVarUint(decoder)
 | 
			
		||||
@ -102,15 +104,16 @@ export const readClientsStructRefs = (decoder, clientRefs) => {
 | 
			
		||||
    const nextIdClient = nextID.client
 | 
			
		||||
    let nextIdClock = nextID.clock
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Array<GCRef|ItemRef>}
 | 
			
		||||
     * @type {Array<GC|Item>}
 | 
			
		||||
     */
 | 
			
		||||
    const refs = []
 | 
			
		||||
    clientRefs.set(nextIdClient, refs)
 | 
			
		||||
    for (let i = 0; i < numberOfStructs; i++) {
 | 
			
		||||
      const info = decoding.readUint8(decoder)
 | 
			
		||||
      const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, createID(nextIdClient, nextIdClock), info) : new ItemRef(decoder, createID(nextIdClient, nextIdClock), info)
 | 
			
		||||
      refs.push(ref)
 | 
			
		||||
      nextIdClock += ref.length
 | 
			
		||||
      const id = createID(nextIdClient, nextIdClock)
 | 
			
		||||
      const struct = (binary.BITS5 & info) === 0 ? new GC(id, decoding.readVarUint(decoder)) : readItem(decoder, id, info, doc)
 | 
			
		||||
      refs.push(struct)
 | 
			
		||||
      nextIdClock += struct.length
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return clientRefs
 | 
			
		||||
@ -155,12 +158,12 @@ const resumeStructIntegration = (transaction, store) => {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const ref = stack[stack.length - 1]
 | 
			
		||||
    const m = ref._missing
 | 
			
		||||
    const refID = ref.id
 | 
			
		||||
    const client = refID.client
 | 
			
		||||
    const refClock = refID.clock
 | 
			
		||||
    const localClock = getState(store, client)
 | 
			
		||||
    const offset = refClock < localClock ? localClock - refClock : 0
 | 
			
		||||
    const missing = ref.getMissing(transaction, store)
 | 
			
		||||
    if (refClock + offset !== localClock) {
 | 
			
		||||
      // A previous message from this client is missing
 | 
			
		||||
      // check if there is a pending structRef with a smaller clock and switch them
 | 
			
		||||
@ -180,27 +183,21 @@ const resumeStructIntegration = (transaction, store) => {
 | 
			
		||||
      // wait until missing struct is available
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    while (m.length > 0) {
 | 
			
		||||
      const missing = m[m.length - 1]
 | 
			
		||||
      if (getState(store, missing.client) <= missing.clock) {
 | 
			
		||||
        const client = missing.client
 | 
			
		||||
        // get the struct reader that has the missing struct
 | 
			
		||||
        const structRefs = clientsStructRefs.get(client)
 | 
			
		||||
        if (structRefs === undefined) {
 | 
			
		||||
          // This update message causally depends on another update message.
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        stack.push(structRefs.refs[structRefs.i++])
 | 
			
		||||
        if (structRefs.i === structRefs.refs.length) {
 | 
			
		||||
          clientsStructRefs.delete(client)
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
    if (missing) {
 | 
			
		||||
      const client = missing.client
 | 
			
		||||
      // get the struct reader that has the missing struct
 | 
			
		||||
      const structRefs = clientsStructRefs.get(client)
 | 
			
		||||
      if (structRefs === undefined) {
 | 
			
		||||
        // This update message causally depends on another update message.
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      ref._missing.pop()
 | 
			
		||||
    }
 | 
			
		||||
    if (m.length === 0) {
 | 
			
		||||
      stack.push(structRefs.refs[structRefs.i++])
 | 
			
		||||
      if (structRefs.i === structRefs.refs.length) {
 | 
			
		||||
        clientsStructRefs.delete(client)
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (offset < ref.length) {
 | 
			
		||||
        ref.toStruct(transaction, store, offset).integrate(transaction)
 | 
			
		||||
        ref.integrate(transaction, offset)
 | 
			
		||||
      }
 | 
			
		||||
      stack.pop()
 | 
			
		||||
    }
 | 
			
		||||
@ -233,7 +230,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {StructStore} store
 | 
			
		||||
 * @param {Map<number, Array<GCRef|ItemRef>>} clientsStructsRefs
 | 
			
		||||
 * @param {Map<number, Array<GC|Item>>} clientsStructsRefs
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 * @function
 | 
			
		||||
@ -270,7 +267,7 @@ const mergeReadStructsIntoPendingReads = (store, clientsStructsRefs) => {
 | 
			
		||||
 */
 | 
			
		||||
export const readStructs = (decoder, transaction, store) => {
 | 
			
		||||
  const clientsStructRefs = new Map()
 | 
			
		||||
  readClientsStructRefs(decoder, clientsStructRefs)
 | 
			
		||||
  readClientsStructRefs(decoder, clientsStructRefs, transaction.doc)
 | 
			
		||||
  mergeReadStructsIntoPendingReads(store, clientsStructRefs)
 | 
			
		||||
  resumeStructIntegration(transaction, store)
 | 
			
		||||
  tryResumePendingDeleteReaders(transaction, store)
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ export const isParentOf = (parent, child) => {
 | 
			
		||||
    if (child.parent === parent) {
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    child = child.parent._item
 | 
			
		||||
    child = /** @type {AbstractType<any>} */ (child.parent)._item
 | 
			
		||||
  }
 | 
			
		||||
  return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -454,7 +454,7 @@ const mapTransactions = [
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testRepeatGeneratingYmapTests10 = tc => {
 | 
			
		||||
  applyRandomTests(tc, mapTransactions, 10)
 | 
			
		||||
  applyRandomTests(tc, mapTransactions, 3)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -209,16 +209,28 @@ export const testFormattingRemovedInMidText = tc => {
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testLargeFragmentedDocument = tc => {
 | 
			
		||||
  const { text0, testConnector } = init(tc, { users: 2 })
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  text0.doc.transact(() => {
 | 
			
		||||
    for (let i = 0; i < 1000000; i++) {
 | 
			
		||||
      text0.insert(0, '0')
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  t.measureTime('time to apply', () => {
 | 
			
		||||
    testConnector.flushAllMessages()
 | 
			
		||||
  })
 | 
			
		||||
  const itemsToInsert = 1000000
 | 
			
		||||
  let update = /** @type {any} */ (null)
 | 
			
		||||
  ;(() => {
 | 
			
		||||
    const doc1 = new Y.Doc()
 | 
			
		||||
    const text0 = doc1.getText('txt')
 | 
			
		||||
    t.measureTime(`time to insert ${itemsToInsert}`, () => {
 | 
			
		||||
      doc1.transact(() => {
 | 
			
		||||
        for (let i = 0; i < itemsToInsert; i++) {
 | 
			
		||||
          text0.insert(0, '0')
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    t.measureTime('time to encode', () => {
 | 
			
		||||
      update = Y.encodeStateAsUpdate(doc1)
 | 
			
		||||
    })
 | 
			
		||||
  })()
 | 
			
		||||
  ;(() => {
 | 
			
		||||
    const doc2 = new Y.Doc()
 | 
			
		||||
    t.measureTime('time to apply', () => {
 | 
			
		||||
      Y.applyUpdate(doc2, update)
 | 
			
		||||
    })
 | 
			
		||||
  })()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RANDOM TESTS
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user