draft of YWeakLink type
This commit is contained in:
		
							parent
							
								
									9a7b659919
								
							
						
					
					
						commit
						f3de5b0add
					
				@ -10,6 +10,8 @@ export {
 | 
			
		||||
  YXmlHook as XmlHook,
 | 
			
		||||
  YXmlElement as XmlElement,
 | 
			
		||||
  YXmlFragment as XmlFragment,
 | 
			
		||||
  YWeakLink as WeakLink,
 | 
			
		||||
  YWeakLinkEvent,
 | 
			
		||||
  YXmlEvent,
 | 
			
		||||
  YMapEvent,
 | 
			
		||||
  YArrayEvent,
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ export * from './types/YXmlElement.js'
 | 
			
		||||
export * from './types/YXmlEvent.js'
 | 
			
		||||
export * from './types/YXmlHook.js'
 | 
			
		||||
export * from './types/YXmlText.js'
 | 
			
		||||
export * from './types/YWeakLink.js'
 | 
			
		||||
 | 
			
		||||
export * from './structs/AbstractStruct.js'
 | 
			
		||||
export * from './structs/GC.js'
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,11 @@ import {
 | 
			
		||||
  readYXmlElement,
 | 
			
		||||
  readYXmlFragment,
 | 
			
		||||
  readYXmlHook,
 | 
			
		||||
  readYXmlText,
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
 | 
			
		||||
  readYXmlText,  
 | 
			
		||||
  readYWeakLink,
 | 
			
		||||
  unlinkFrom,
 | 
			
		||||
  YWeakLink,
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType, ID, // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as error from 'lib0/error'
 | 
			
		||||
@ -33,6 +36,7 @@ export const YXmlElementRefID = 3
 | 
			
		||||
export const YXmlFragmentRefID = 4
 | 
			
		||||
export const YXmlHookRefID = 5
 | 
			
		||||
export const YXmlTextRefID = 6
 | 
			
		||||
export const YWeakLinkRefID = 7
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
@ -104,6 +108,22 @@ export class ContentType {
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   */
 | 
			
		||||
  delete (transaction) {
 | 
			
		||||
    if (this.type.constructor === YWeakLink) {
 | 
			
		||||
      // when removing weak links, remove references to them
 | 
			
		||||
      // from type they're pointing to
 | 
			
		||||
      const type = /** @type {YWeakLink<any>} */ (this.type)
 | 
			
		||||
      const end = /** @type {ID} */ (type._quoteEnd.item)
 | 
			
		||||
      for (let item = type._firstItem; item !== null; item = item.right) {
 | 
			
		||||
        if (item.linked) {
 | 
			
		||||
          unlinkFrom(transaction, item, type)
 | 
			
		||||
        }
 | 
			
		||||
        const lastId = item.lastId
 | 
			
		||||
        if (lastId.client === end.client && lastId.clock === end.clock) {
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      type._firstItem = null
 | 
			
		||||
    }
 | 
			
		||||
    let item = this.type._start
 | 
			
		||||
    while (item !== null) {
 | 
			
		||||
      if (!item.deleted) {
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,9 @@ import {
 | 
			
		||||
  readContentType,
 | 
			
		||||
  addChangedTypeToTransaction,
 | 
			
		||||
  isDeleted,
 | 
			
		||||
  StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
 | 
			
		||||
  StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction, // eslint-disable-line
 | 
			
		||||
  YWeakLink,
 | 
			
		||||
  joinLinkedRange
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as error from 'lib0/error'
 | 
			
		||||
@ -105,6 +107,14 @@ export const splitItem = (transaction, leftItem, diff) => {
 | 
			
		||||
  if (leftItem.redone !== null) {
 | 
			
		||||
    rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff)
 | 
			
		||||
  }
 | 
			
		||||
  if (leftItem.linked) {
 | 
			
		||||
    rightItem.linked = true
 | 
			
		||||
    const allLinks = transaction.doc.store.linkedBy
 | 
			
		||||
    const linkedBy = allLinks.get(leftItem)
 | 
			
		||||
    if (linkedBy !== undefined) {
 | 
			
		||||
      allLinks.set(rightItem, new Set(linkedBy))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
 | 
			
		||||
  leftItem.right = rightItem
 | 
			
		||||
  // update right
 | 
			
		||||
@ -304,11 +314,28 @@ export class Item extends AbstractStruct {
 | 
			
		||||
     * bit2: countable
 | 
			
		||||
     * bit3: deleted
 | 
			
		||||
     * bit4: mark - mark node as fast-search-marker
 | 
			
		||||
     * bit9: linked - this item is linked by Weak Link references
 | 
			
		||||
     * @type {number} byte
 | 
			
		||||
     */
 | 
			
		||||
    this.info = this.content.isCountable() ? binary.BIT2 : 0
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
     * This is used to mark the item as linked by weak link references.
 | 
			
		||||
     * Reference dependencies are being kept in StructStore.
 | 
			
		||||
     *
 | 
			
		||||
     * @type {boolean}
 | 
			
		||||
     */
 | 
			
		||||
  set linked (isLinked) {
 | 
			
		||||
    if (((this.info & binary.BIT9) > 0) !== isLinked) {
 | 
			
		||||
      this.info ^= binary.BIT9
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get linked () {
 | 
			
		||||
    return (this.info & binary.BIT9) > 0
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This is used to mark the item as an indexed fast-search marker
 | 
			
		||||
   *
 | 
			
		||||
@ -376,6 +403,20 @@ export class Item extends AbstractStruct {
 | 
			
		||||
    if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) {
 | 
			
		||||
      return this.parent.client
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (this.content.constructor === ContentType && /** @type {ContentType} */ (this.content).type.constructor === YWeakLink) {
 | 
			
		||||
      // make sure that linked content is integrated first
 | 
			
		||||
      const content = /** @type {ContentType} */ (this.content)
 | 
			
		||||
      const link = /** @type {YWeakLink<any>} */ (content.type)
 | 
			
		||||
      const start = link._quoteStart.item
 | 
			
		||||
      if (start !== null && start.clock >= getState(store, start.client)) {
 | 
			
		||||
        return start.client
 | 
			
		||||
      }
 | 
			
		||||
      const end = link._quoteEnd.item
 | 
			
		||||
      if (end !== null && end.clock >= getState(store, end.client)) {
 | 
			
		||||
        return end.client
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We have all missing ids, now find the items
 | 
			
		||||
 | 
			
		||||
@ -508,18 +549,43 @@ export class Item extends AbstractStruct {
 | 
			
		||||
        // set as current parent value if right === null and this is parentSub
 | 
			
		||||
        /** @type {AbstractType<any>} */ (this.parent)._map.set(this.parentSub, this)
 | 
			
		||||
        if (this.left !== null) {
 | 
			
		||||
          // move links from block we're overriding
 | 
			
		||||
          this.linked = this.left.linked
 | 
			
		||||
          this.left.linked = false
 | 
			
		||||
          const allLinks = transaction.doc.store.linkedBy
 | 
			
		||||
          const links = allLinks.get(this.left)
 | 
			
		||||
          if (links !== undefined) {
 | 
			
		||||
            allLinks.set(this, links)
 | 
			
		||||
            // since left is being deleted, it will remove
 | 
			
		||||
            // its links from store.linkedBy anyway
 | 
			
		||||
          }
 | 
			
		||||
          // this is the current attribute value of parent. delete right
 | 
			
		||||
          this.left.delete(transaction)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // adjust length of parent
 | 
			
		||||
      if (this.parentSub === null && this.countable && !this.deleted) {
 | 
			
		||||
        /** @type {AbstractType<any>} */ (this.parent)._length += this.length
 | 
			
		||||
      if (this.parentSub === null && !this.deleted) {
 | 
			
		||||
        if (this.countable) {
 | 
			
		||||
          // adjust length of parent
 | 
			
		||||
          /** @type {AbstractType<any>} */ (this.parent)._length += this.length
 | 
			
		||||
        }
 | 
			
		||||
        if (this.left && this.left.linked && this.right && this.right.linked) {
 | 
			
		||||
          // this item exists within a quoted range
 | 
			
		||||
          joinLinkedRange(transaction, this)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      addStruct(transaction.doc.store, this)
 | 
			
		||||
      this.content.integrate(transaction, this)
 | 
			
		||||
      // add parent to transaction.changed
 | 
			
		||||
      addChangedTypeToTransaction(transaction, /** @type {AbstractType<any>} */ (this.parent), this.parentSub)
 | 
			
		||||
      if (this.linked) {
 | 
			
		||||
        // notify links about changes
 | 
			
		||||
        const linkedBy = transaction.doc.store.linkedBy.get(this)
 | 
			
		||||
        if (linkedBy !== undefined) {
 | 
			
		||||
          for (const link of linkedBy) {
 | 
			
		||||
            addChangedTypeToTransaction(transaction, link, this.parentSub)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if ((/** @type {AbstractType<any>} */ (this.parent)._item !== null && /** @type {AbstractType<any>} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) {
 | 
			
		||||
        // delete if parent is deleted or if this is not the current attribute value of parent
 | 
			
		||||
        this.delete(transaction)
 | 
			
		||||
@ -577,6 +643,7 @@ export class Item extends AbstractStruct {
 | 
			
		||||
      this.deleted === right.deleted &&
 | 
			
		||||
      this.redone === null &&
 | 
			
		||||
      right.redone === null &&
 | 
			
		||||
      !this.linked && !right.linked && // linked items cannot be merged
 | 
			
		||||
      this.content.constructor === right.content.constructor &&
 | 
			
		||||
      this.content.mergeWith(right.content)
 | 
			
		||||
    ) {
 | 
			
		||||
@ -622,6 +689,19 @@ export class Item extends AbstractStruct {
 | 
			
		||||
      addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
 | 
			
		||||
      addChangedTypeToTransaction(transaction, parent, this.parentSub)
 | 
			
		||||
      this.content.delete(transaction)
 | 
			
		||||
 | 
			
		||||
      if (this.linked) {
 | 
			
		||||
        // notify links that current element has been removed
 | 
			
		||||
        const allLinks = transaction.doc.store.linkedBy
 | 
			
		||||
        const linkedBy = allLinks.get(this)
 | 
			
		||||
        if (linkedBy !== undefined) {
 | 
			
		||||
          for (const link of linkedBy) {
 | 
			
		||||
            addChangedTypeToTransaction(transaction, link, this.parentSub)
 | 
			
		||||
          }
 | 
			
		||||
          allLinks.delete(this)
 | 
			
		||||
        }
 | 
			
		||||
        this.linked = false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ import {
 | 
			
		||||
  ContentAny,
 | 
			
		||||
  ContentBinary,
 | 
			
		||||
  getItemCleanStart,
 | 
			
		||||
  ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
 | 
			
		||||
  ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, YWeakLink, // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as map from 'lib0/map'
 | 
			
		||||
@ -233,8 +233,9 @@ export const getTypeChildren = t => {
 | 
			
		||||
 * @param {AbstractType<EventType>} type
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
 * @param {EventType} event
 | 
			
		||||
 * @param {Set<YWeakLink<any>>|null} visitedLinks
 | 
			
		||||
 */
 | 
			
		||||
export const callTypeObservers = (type, transaction, event) => {
 | 
			
		||||
export const callTypeObservers = (type, transaction, event, visitedLinks = null) => {
 | 
			
		||||
  const changedType = type
 | 
			
		||||
  const changedParentTypes = transaction.changedParentTypes
 | 
			
		||||
  while (true) {
 | 
			
		||||
@ -242,6 +243,18 @@ export const callTypeObservers = (type, transaction, event) => {
 | 
			
		||||
    map.setIfUndefined(changedParentTypes, type, () => []).push(event)
 | 
			
		||||
    if (type._item === null) {
 | 
			
		||||
      break
 | 
			
		||||
    } else if (type._item.linked) {
 | 
			
		||||
      const linkedBy = transaction.doc.store.linkedBy.get(type._item)
 | 
			
		||||
      if (linkedBy !== undefined) {
 | 
			
		||||
        for (let link of linkedBy) {
 | 
			
		||||
          if (visitedLinks === null || !visitedLinks.has(link)) {
 | 
			
		||||
            visitedLinks = visitedLinks !== null ? visitedLinks : new Set()
 | 
			
		||||
            visitedLinks.add(link)
 | 
			
		||||
            // recursive call
 | 
			
		||||
            callTypeObservers(link, transaction, /** @type {any} */ (event), visitedLinks)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    type = /** @type {AbstractType<any>} */ (type._item.parent)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,8 @@ import {
 | 
			
		||||
  YArrayRefID,
 | 
			
		||||
  callTypeObservers,
 | 
			
		||||
  transact,
 | 
			
		||||
  ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
 | 
			
		||||
  arrayWeakLink,
 | 
			
		||||
  ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
import { typeListSlice } from './AbstractType.js'
 | 
			
		||||
 | 
			
		||||
@ -200,6 +201,25 @@ export class YArray extends AbstractType {
 | 
			
		||||
  get (index) {
 | 
			
		||||
    return typeListGet(this, index)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the weak link that allows to refer and observe live changes of contents of an YArray.
 | 
			
		||||
   * It points at a consecutive range of elements, starting at give `index` and spanning over provided
 | 
			
		||||
   * length of elements.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {number} index The index of the element to return from the YArray
 | 
			
		||||
   * @param {number} length The number of elements to include in returned weak link reference.
 | 
			
		||||
   * @return {YWeakLink<T>}
 | 
			
		||||
   */
 | 
			
		||||
  quote (index, length = 1) {
 | 
			
		||||
    if (this.doc !== null) {
 | 
			
		||||
      return transact(this.doc, transaction => {
 | 
			
		||||
        return arrayWeakLink(transaction, this, index, length)
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new Error('cannot quote an YArray that has not been integrated into YDoc')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transforms this YArray to a JavaScript Array.
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,8 @@ import {
 | 
			
		||||
  YMapRefID,
 | 
			
		||||
  callTypeObservers,
 | 
			
		||||
  transact,
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
 | 
			
		||||
  mapWeakLink,
 | 
			
		||||
  UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as iterator from 'lib0/iterator'
 | 
			
		||||
@ -232,6 +233,16 @@ export class YMap extends AbstractType {
 | 
			
		||||
  get (key) {
 | 
			
		||||
    return /** @type {any} */ (typeMapGet(this, key))
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a weak reference link to another element stored in the same document.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string} key
 | 
			
		||||
   * @return {YWeakLink<MapType>|undefined}
 | 
			
		||||
   */
 | 
			
		||||
  link (key) {
 | 
			
		||||
    return mapWeakLink(this, key)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a boolean indicating whether the specified key exists or not.
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,8 @@ import {
 | 
			
		||||
  typeMapGetAll,
 | 
			
		||||
  updateMarkerChanges,
 | 
			
		||||
  ContentType,
 | 
			
		||||
  ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
 | 
			
		||||
  ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, YWeakLink, // eslint-disable-line
 | 
			
		||||
  quoteText
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as object from 'lib0/object'
 | 
			
		||||
@ -1000,107 +1001,7 @@ export class YText extends AbstractType {
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toDelta (snapshot, prevSnapshot, computeYChange) {
 | 
			
		||||
    /**
 | 
			
		||||
     * @type{Array<any>}
 | 
			
		||||
     */
 | 
			
		||||
    const ops = []
 | 
			
		||||
    const currentAttributes = new Map()
 | 
			
		||||
    const doc = /** @type {Doc} */ (this.doc)
 | 
			
		||||
    let str = ''
 | 
			
		||||
    let n = this._start
 | 
			
		||||
    function packStr () {
 | 
			
		||||
      if (str.length > 0) {
 | 
			
		||||
        // pack str with attributes to ops
 | 
			
		||||
        /**
 | 
			
		||||
         * @type {Object<string,any>}
 | 
			
		||||
         */
 | 
			
		||||
        const attributes = {}
 | 
			
		||||
        let addAttributes = false
 | 
			
		||||
        currentAttributes.forEach((value, key) => {
 | 
			
		||||
          addAttributes = true
 | 
			
		||||
          attributes[key] = value
 | 
			
		||||
        })
 | 
			
		||||
        /**
 | 
			
		||||
         * @type {Object<string,any>}
 | 
			
		||||
         */
 | 
			
		||||
        const op = { insert: str }
 | 
			
		||||
        if (addAttributes) {
 | 
			
		||||
          op.attributes = attributes
 | 
			
		||||
        }
 | 
			
		||||
        ops.push(op)
 | 
			
		||||
        str = ''
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const computeDelta = () => {
 | 
			
		||||
      while (n !== null) {
 | 
			
		||||
        if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
 | 
			
		||||
          switch (n.content.constructor) {
 | 
			
		||||
            case ContentString: {
 | 
			
		||||
              const cur = currentAttributes.get('ychange')
 | 
			
		||||
              if (snapshot !== undefined && !isVisible(n, snapshot)) {
 | 
			
		||||
                if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') {
 | 
			
		||||
                  packStr()
 | 
			
		||||
                  currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
 | 
			
		||||
                }
 | 
			
		||||
              } else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
 | 
			
		||||
                if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') {
 | 
			
		||||
                  packStr()
 | 
			
		||||
                  currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
 | 
			
		||||
                }
 | 
			
		||||
              } else if (cur !== undefined) {
 | 
			
		||||
                packStr()
 | 
			
		||||
                currentAttributes.delete('ychange')
 | 
			
		||||
              }
 | 
			
		||||
              str += /** @type {ContentString} */ (n.content).str
 | 
			
		||||
              break
 | 
			
		||||
            }
 | 
			
		||||
            case ContentType:
 | 
			
		||||
            case ContentEmbed: {
 | 
			
		||||
              packStr()
 | 
			
		||||
              /**
 | 
			
		||||
               * @type {Object<string,any>}
 | 
			
		||||
               */
 | 
			
		||||
              const op = {
 | 
			
		||||
                insert: n.content.getContent()[0]
 | 
			
		||||
              }
 | 
			
		||||
              if (currentAttributes.size > 0) {
 | 
			
		||||
                const attrs = /** @type {Object<string,any>} */ ({})
 | 
			
		||||
                op.attributes = attrs
 | 
			
		||||
                currentAttributes.forEach((value, key) => {
 | 
			
		||||
                  attrs[key] = value
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
              ops.push(op)
 | 
			
		||||
              break
 | 
			
		||||
            }
 | 
			
		||||
            case ContentFormat:
 | 
			
		||||
              if (isVisible(n, snapshot)) {
 | 
			
		||||
                packStr()
 | 
			
		||||
                updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
 | 
			
		||||
              }
 | 
			
		||||
              break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        n = n.right
 | 
			
		||||
      }
 | 
			
		||||
      packStr()
 | 
			
		||||
    }
 | 
			
		||||
    if (snapshot || prevSnapshot) {
 | 
			
		||||
      // snapshots are merged again after the transaction, so we need to keep the
 | 
			
		||||
      // transaction alive until we are done
 | 
			
		||||
      transact(doc, transaction => {
 | 
			
		||||
        if (snapshot) {
 | 
			
		||||
          splitSnapshotAffectedStructs(transaction, snapshot)
 | 
			
		||||
        }
 | 
			
		||||
        if (prevSnapshot) {
 | 
			
		||||
          splitSnapshotAffectedStructs(transaction, prevSnapshot)
 | 
			
		||||
        }
 | 
			
		||||
        computeDelta()
 | 
			
		||||
      }, 'cleanup')
 | 
			
		||||
    } else {
 | 
			
		||||
      computeDelta()
 | 
			
		||||
    }
 | 
			
		||||
    return ops
 | 
			
		||||
    return rangeDelta(this, null, null, snapshot, prevSnapshot, computeYChange)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -1155,6 +1056,31 @@ export class YText extends AbstractType {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a WeakLink representing a dynamic quotation of a range of elements.
 | 
			
		||||
   *
 | 
			
		||||
   * In case when quotation happens in a middle of formatting range, formatting
 | 
			
		||||
   * attributes will be split into before|within|after eg. quoting fragment of
 | 
			
		||||
   * `<i>hello world</i>` could result in `<i>he</i>"<i>llo wo</i>"<i>rld</i>`
 | 
			
		||||
   * where `"<i>llo wo</i>"` represents quoted range.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {number} index The index where quoted range should start
 | 
			
		||||
   * @param {number} length Number of quoted elements
 | 
			
		||||
   * @return {YWeakLink<string>}
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  quote (index, length) {
 | 
			
		||||
    const y = this.doc
 | 
			
		||||
    if (y !== null) {
 | 
			
		||||
      return transact(y, transaction => {
 | 
			
		||||
        const pos = findPosition(transaction, this, index)
 | 
			
		||||
        return quoteText(transaction, this, pos, length)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    throw new Error('Quoted text was not integrated into Doc')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Deletes text starting from an index.
 | 
			
		||||
   *
 | 
			
		||||
@ -1280,6 +1206,146 @@ export class YText extends AbstractType {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns a delta representation that happens between `start` and `end` ranges (both sides inclusive).
 | 
			
		||||
 *
 | 
			
		||||
 * @param {AbstractType<any>} parent
 | 
			
		||||
 * @param {ID|null} start
 | 
			
		||||
 * @param {ID|null} end
 | 
			
		||||
 * @param {Snapshot|undefined} snapshot
 | 
			
		||||
 * @param {Snapshot|undefined} prevSnapshot
 | 
			
		||||
 * @param {(function('removed' | 'added', ID):any)|undefined} computeYChange
 | 
			
		||||
 * @returns {any} The Delta representation of this type.
 | 
			
		||||
 */
 | 
			
		||||
export const rangeDelta = (parent, start, end, snapshot, prevSnapshot, computeYChange) => {
 | 
			
		||||
  /**
 | 
			
		||||
     * @type{Array<any>}
 | 
			
		||||
     */
 | 
			
		||||
  const ops = []
 | 
			
		||||
  const currentAttributes = new Map()
 | 
			
		||||
  const doc = /** @type {Doc} */ (parent.doc)
 | 
			
		||||
  let str = ''
 | 
			
		||||
  let n = parent._start
 | 
			
		||||
  function packStr () {
 | 
			
		||||
    if (str.length > 0) {
 | 
			
		||||
      // pack str with attributes to ops
 | 
			
		||||
      /**
 | 
			
		||||
         * @type {Object<string,any>}
 | 
			
		||||
         */
 | 
			
		||||
      const attributes = {}
 | 
			
		||||
      let addAttributes = false
 | 
			
		||||
      currentAttributes.forEach((value, key) => {
 | 
			
		||||
        addAttributes = true
 | 
			
		||||
        attributes[key] = value
 | 
			
		||||
      })
 | 
			
		||||
      /**
 | 
			
		||||
         * @type {Object<string,any>}
 | 
			
		||||
         */
 | 
			
		||||
      const op = { insert: str }
 | 
			
		||||
      if (addAttributes) {
 | 
			
		||||
        op.attributes = attributes
 | 
			
		||||
      }
 | 
			
		||||
      ops.push(op)
 | 
			
		||||
      str = ''
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const computeDelta = () => {
 | 
			
		||||
    // startOffset represents offset at current block from which we're intersted in picking string
 | 
			
		||||
    // if it's -1 it means, we're out of scope and we should break at this point
 | 
			
		||||
    let startOffset = start === null ? 0 : -1
 | 
			
		||||
    // eslint-disable-next-line no-labels
 | 
			
		||||
    loop: while (n !== null) {
 | 
			
		||||
      if (startOffset < 0 && start !== null) {
 | 
			
		||||
        if (start.client === n.id.client && start.clock >= n.id.clock && start.clock < n.id.clock + n.length) {
 | 
			
		||||
          startOffset = start.clock - n.id.clock
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
 | 
			
		||||
        switch (n.content.constructor) {
 | 
			
		||||
          case ContentString: {
 | 
			
		||||
            const cur = currentAttributes.get('ychange')
 | 
			
		||||
            if (snapshot !== undefined && !isVisible(n, snapshot)) {
 | 
			
		||||
              if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') {
 | 
			
		||||
                packStr()
 | 
			
		||||
                currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
 | 
			
		||||
              }
 | 
			
		||||
            } else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
 | 
			
		||||
              if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') {
 | 
			
		||||
                packStr()
 | 
			
		||||
                currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
 | 
			
		||||
              }
 | 
			
		||||
            } else if (cur !== undefined) {
 | 
			
		||||
              packStr()
 | 
			
		||||
              currentAttributes.delete('ychange')
 | 
			
		||||
            }
 | 
			
		||||
            const s = /** @type {ContentString} */ (n.content).str
 | 
			
		||||
            if (startOffset > 0) {
 | 
			
		||||
              str += s.slice(startOffset)
 | 
			
		||||
              startOffset = 0
 | 
			
		||||
            } else if (end !== null && end.client === n.id.client && end.clock >= n.id.clock && end.clock < n.id.clock + n.length) {
 | 
			
		||||
              // we reached the end or range
 | 
			
		||||
              const endOffset = n.id.clock + n.length - end.clock - 1
 | 
			
		||||
              str += s.slice(0, s.length + endOffset) // scope is negative
 | 
			
		||||
              packStr()
 | 
			
		||||
              // eslint-disable-next-line no-labels
 | 
			
		||||
              break loop
 | 
			
		||||
            } else if (startOffset === 0) {
 | 
			
		||||
              str += s
 | 
			
		||||
            }
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
          case ContentType:
 | 
			
		||||
          case ContentEmbed: {
 | 
			
		||||
            packStr()
 | 
			
		||||
            /**
 | 
			
		||||
               * @type {Object<string,any>}
 | 
			
		||||
               */
 | 
			
		||||
            const op = {
 | 
			
		||||
              insert: n.content.getContent()[0]
 | 
			
		||||
            }
 | 
			
		||||
            if (currentAttributes.size > 0) {
 | 
			
		||||
              const attrs = /** @type {Object<string,any>} */ ({})
 | 
			
		||||
              op.attributes = attrs
 | 
			
		||||
              currentAttributes.forEach((value, key) => {
 | 
			
		||||
                attrs[key] = value
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
            ops.push(op)
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
          case ContentFormat:
 | 
			
		||||
            if (isVisible(n, snapshot)) {
 | 
			
		||||
              packStr()
 | 
			
		||||
              updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
 | 
			
		||||
            }
 | 
			
		||||
            break
 | 
			
		||||
        }
 | 
			
		||||
      } else if (end !== null && end.client === n.id.client && end.clock >= n.id.clock && end.clock < n.id.clock + n.length) {
 | 
			
		||||
        // block may not passed visibility check, but we still need to verify boundaries
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
      n = n.right
 | 
			
		||||
    }
 | 
			
		||||
    packStr()
 | 
			
		||||
  }
 | 
			
		||||
  if (snapshot || prevSnapshot) {
 | 
			
		||||
    // snapshots are merged again after the transaction, so we need to keep the
 | 
			
		||||
    // transaction alive until we are done
 | 
			
		||||
    transact(doc, transaction => {
 | 
			
		||||
      if (snapshot) {
 | 
			
		||||
        splitSnapshotAffectedStructs(transaction, snapshot)
 | 
			
		||||
      }
 | 
			
		||||
      if (prevSnapshot) {
 | 
			
		||||
        splitSnapshotAffectedStructs(transaction, prevSnapshot)
 | 
			
		||||
      }
 | 
			
		||||
      computeDelta()
 | 
			
		||||
    }, 'cleanup')
 | 
			
		||||
  } else {
 | 
			
		||||
    computeDelta()
 | 
			
		||||
  }
 | 
			
		||||
  return ops
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
 | 
			
		||||
 * @return {YText}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										469
									
								
								src/types/YWeakLink.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										469
									
								
								src/types/YWeakLink.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,469 @@
 | 
			
		||||
import { decoding, encoding, error } from 'lib0'
 | 
			
		||||
import * as map from 'lib0/map'
 | 
			
		||||
import * as set from 'lib0/set'
 | 
			
		||||
import {
 | 
			
		||||
  YEvent, AbstractType,
 | 
			
		||||
  transact,
 | 
			
		||||
  getItemCleanEnd,
 | 
			
		||||
  createID,
 | 
			
		||||
  getItemCleanStart,
 | 
			
		||||
  callTypeObservers,
 | 
			
		||||
  YWeakLinkRefID,
 | 
			
		||||
  writeID,
 | 
			
		||||
  readID,
 | 
			
		||||
  RelativePosition,
 | 
			
		||||
  ContentString,
 | 
			
		||||
  rangeDelta,
 | 
			
		||||
  formatXmlString,
 | 
			
		||||
  YText,
 | 
			
		||||
  YXmlText,
 | 
			
		||||
  Transaction, Item, Doc, ID, Snapshot, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ItemTextListPosition // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @template T extends AbstractType<any>
 | 
			
		||||
 * @extends YEvent<any>
 | 
			
		||||
 * Event that describes the changes on a YMap.
 | 
			
		||||
 */
 | 
			
		||||
export class YWeakLinkEvent extends YEvent {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {YWeakLink<T>} ylink The YWeakLink to which this event was propagated to.
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   */
 | 
			
		||||
  // eslint-disable-next-line no-useless-constructor
 | 
			
		||||
  constructor (ylink, transaction) {
 | 
			
		||||
    super(ylink, transaction)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @template T
 | 
			
		||||
 * @extends AbstractType<YWeakLinkEvent<T>>
 | 
			
		||||
 *
 | 
			
		||||
 * Weak link to another value stored somewhere in the document.
 | 
			
		||||
 */
 | 
			
		||||
export class YWeakLink extends AbstractType {
 | 
			
		||||
  /**
 | 
			
		||||
    * @param {RelativePosition} start
 | 
			
		||||
    * @param {RelativePosition} end
 | 
			
		||||
    * @param {Item|null} firstItem
 | 
			
		||||
    */
 | 
			
		||||
  constructor (start, end, firstItem) {
 | 
			
		||||
    super()
 | 
			
		||||
    /** @type {RelativePosition} */
 | 
			
		||||
    this._quoteStart = start
 | 
			
		||||
    /** @type {RelativePosition} */
 | 
			
		||||
    this._quoteEnd = end
 | 
			
		||||
    this._firstItem = firstItem
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Position descriptor of the start of a quoted range.
 | 
			
		||||
   * 
 | 
			
		||||
   * @returns {RelativePosition}
 | 
			
		||||
   */
 | 
			
		||||
  get quoteStart () {
 | 
			
		||||
    return this._quoteStart
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Position descriptor of the end of a quoted range.
 | 
			
		||||
   * 
 | 
			
		||||
   * @returns {RelativePosition}
 | 
			
		||||
   */
 | 
			
		||||
  get quoteEnd () {
 | 
			
		||||
    return this._quoteEnd
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Check if current link contains only a single element.
 | 
			
		||||
   *
 | 
			
		||||
   * @returns {boolean}
 | 
			
		||||
   */
 | 
			
		||||
  get isSingle () {
 | 
			
		||||
    return this._quoteStart.item === this._quoteEnd.item
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a reference to an underlying value existing somewhere on in the document.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {T|undefined}
 | 
			
		||||
   */
 | 
			
		||||
  deref () {
 | 
			
		||||
    if (this._firstItem !== null) {
 | 
			
		||||
      let item = this._firstItem
 | 
			
		||||
      if (item.parentSub !== null) {
 | 
			
		||||
        while (item.right !== null) {
 | 
			
		||||
          item = item.right
 | 
			
		||||
        }
 | 
			
		||||
        // we don't support quotations over maps
 | 
			
		||||
        this._firstItem = item
 | 
			
		||||
      }
 | 
			
		||||
      if (!this._firstItem.deleted) {
 | 
			
		||||
        return this._firstItem.content.getContent()[0]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return undefined
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an array of references to all elements quoted by current weak link.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  unquote () {
 | 
			
		||||
    let result = /** @type {Array<any>} */ ([])
 | 
			
		||||
    let item = this._firstItem
 | 
			
		||||
    const end = /** @type {ID} */ (this._quoteEnd.item)
 | 
			
		||||
    // TODO: moved elements
 | 
			
		||||
    while (item !== null) {
 | 
			
		||||
      if (!item.deleted) {
 | 
			
		||||
        result = result.concat(item.content.getContent())
 | 
			
		||||
      }
 | 
			
		||||
      const lastId = item.lastId
 | 
			
		||||
      if (lastId.client === end.client && lastId.clock === end.clock) {
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
      item = item.right
 | 
			
		||||
    }
 | 
			
		||||
    return result
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Integrate this type into the Yjs instance.
 | 
			
		||||
   *
 | 
			
		||||
   * * Save this struct in the os
 | 
			
		||||
   * * This type is sent to other client
 | 
			
		||||
   * * Observer functions are fired
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Doc} y The Yjs instance
 | 
			
		||||
   * @param {Item|null} item
 | 
			
		||||
   */
 | 
			
		||||
  _integrate (y, item) {
 | 
			
		||||
    super._integrate(y, item)
 | 
			
		||||
    if (item !== null) {
 | 
			
		||||
      transact(y, (transaction) => {
 | 
			
		||||
        // link may refer to a single element in multi-element block
 | 
			
		||||
        // in such case we need to cut of the linked element into a
 | 
			
		||||
        // separate block
 | 
			
		||||
        let firstItem = this._firstItem !== null ? this._firstItem : getItemCleanStart(transaction, /** @type {ID} */ (this._quoteStart.item))
 | 
			
		||||
        getItemCleanEnd(transaction, y.store, /** @type {ID} */(this._quoteEnd.item))
 | 
			
		||||
        if (firstItem.parentSub !== null) {
 | 
			
		||||
          // for maps, advance to most recent item
 | 
			
		||||
          while (firstItem.right !== null) {
 | 
			
		||||
            firstItem = firstItem.right
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        this._firstItem = firstItem
 | 
			
		||||
 | 
			
		||||
        /** @type {Item|null} */
 | 
			
		||||
        let item = firstItem
 | 
			
		||||
        const end = /** @type {ID} */ (this._quoteEnd.item)
 | 
			
		||||
        for (;item !== null; item = item.right) {
 | 
			
		||||
          createLink(transaction, item, this)
 | 
			
		||||
          const lastId = item.lastId
 | 
			
		||||
          if (lastId.client === end.client && lastId.clock === end.clock) {
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @return {YWeakLink<T>}
 | 
			
		||||
   */
 | 
			
		||||
  _copy () {
 | 
			
		||||
    return new YWeakLink(this._quoteStart, this._quoteEnd, this._firstItem)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @return {YWeakLink<T>}
 | 
			
		||||
   */
 | 
			
		||||
  clone () {
 | 
			
		||||
    return new YWeakLink(this._quoteStart, this._quoteEnd, this._firstItem)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates YWeakLinkEvent and calls observers.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Transaction} transaction
 | 
			
		||||
   * @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
 | 
			
		||||
   */
 | 
			
		||||
  _callObserver (transaction, parentSubs) {
 | 
			
		||||
    super._callObserver(transaction, parentSubs)
 | 
			
		||||
    callTypeObservers(this, transaction, new YWeakLinkEvent(this, transaction))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
 | 
			
		||||
   */
 | 
			
		||||
  _write (encoder) {
 | 
			
		||||
    encoder.writeTypeRef(YWeakLinkRefID)
 | 
			
		||||
    const isSingle = this.isSingle
 | 
			
		||||
    const info = (isSingle ? 0 : 1) | (this._quoteStart.assoc >= 0 ? 2 : 0) | (this._quoteEnd.assoc >= 0 ? 4 : 0)
 | 
			
		||||
    encoding.writeUint8(encoder.restEncoder, info)
 | 
			
		||||
    writeID(encoder.restEncoder, /** @type {ID} */ (this._quoteStart.item))
 | 
			
		||||
    if (!isSingle) {
 | 
			
		||||
      writeID(encoder.restEncoder, /** @type {ID} */ (this._quoteEnd.item))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the unformatted string representation of this quoted text range.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toString () {
 | 
			
		||||
    if (this._firstItem !== null) {
 | 
			
		||||
      switch (/** @type {AbstractType<any>} */ (this._firstItem.parent).constructor) {
 | 
			
		||||
        case YText: {
 | 
			
		||||
          let str = ''
 | 
			
		||||
          /**
 | 
			
		||||
           * @type {Item|null}
 | 
			
		||||
           */
 | 
			
		||||
          let n = this._firstItem
 | 
			
		||||
          const end = /** @type {ID} */ (this._quoteEnd.item)
 | 
			
		||||
          while (n !== null) {
 | 
			
		||||
            if (!n.deleted && n.countable && n.content.constructor === ContentString) {
 | 
			
		||||
              str += /** @type {ContentString} */ (n.content).str
 | 
			
		||||
            }
 | 
			
		||||
            const lastId = n.lastId
 | 
			
		||||
            if (lastId.client === end.client && lastId.clock === end.clock) {
 | 
			
		||||
              break
 | 
			
		||||
            }
 | 
			
		||||
            n = n.right
 | 
			
		||||
          }
 | 
			
		||||
          return str
 | 
			
		||||
        }
 | 
			
		||||
        case YXmlText:
 | 
			
		||||
          return this.toDelta().map(delta => formatXmlString(delta)).join('')
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return ''
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the Delta representation of quoted part of underlying text type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Snapshot|undefined} [snapshot]
 | 
			
		||||
   * @param {Snapshot|undefined} [prevSnapshot]
 | 
			
		||||
   * @param {function('removed' | 'added', ID):any} [computeYChange]
 | 
			
		||||
   * @returns {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  toDelta (snapshot, prevSnapshot, computeYChange) {
 | 
			
		||||
    if (this._firstItem !== null && this._quoteStart.item !== null && this._quoteEnd.item !== null) {
 | 
			
		||||
      const parent = /** @type {AbstractType<any>} */ (this._firstItem.parent)
 | 
			
		||||
      return rangeDelta(parent, this._quoteStart.item, this._quoteEnd.item, snapshot, prevSnapshot, computeYChange)
 | 
			
		||||
    } else {
 | 
			
		||||
      return []
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
 | 
			
		||||
 * @return {YWeakLink<any>}
 | 
			
		||||
 */
 | 
			
		||||
export const readYWeakLink = decoder => {
 | 
			
		||||
  const info = decoding.readUint8(decoder.restDecoder)
 | 
			
		||||
  const isSingle = (info & 1) !== 1
 | 
			
		||||
  const startAssoc = (info & 2) === 2 ? 0 : -1
 | 
			
		||||
  const endAssoc = (info & 4) === 4 ? 0 : -1
 | 
			
		||||
  const startID = readID(decoder.restDecoder)
 | 
			
		||||
  const start = new RelativePosition(null, null, startID, startAssoc)
 | 
			
		||||
  const end = new RelativePosition(null, null, isSingle ? startID : readID(decoder.restDecoder), endAssoc)
 | 
			
		||||
  return new YWeakLink(start, end, null)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const invalidQuotedRange = error.create('Invalid quoted range length.')
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns a {WeakLink} to an YArray element at given index.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
 * @param {AbstractType<any>} parent
 | 
			
		||||
 * @param {number} index
 | 
			
		||||
 * @return {YWeakLink<any>}
 | 
			
		||||
 */
 | 
			
		||||
export const arrayWeakLink = (transaction, parent, index, length = 1) => {
 | 
			
		||||
  if (length <= 0) {
 | 
			
		||||
    throw invalidQuotedRange
 | 
			
		||||
  }
 | 
			
		||||
  let startItem = parent._start
 | 
			
		||||
  for (;startItem !== null; startItem = startItem.right) {
 | 
			
		||||
    if (!startItem.deleted && startItem.countable) {
 | 
			
		||||
      if (index < startItem.length) {
 | 
			
		||||
        if (index > 0) {
 | 
			
		||||
          startItem = getItemCleanStart(transaction, createID(startItem.id.client, startItem.id.clock + index))
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
      index -= startItem.length
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  let endItem = startItem
 | 
			
		||||
  let remaining = length
 | 
			
		||||
  for (;endItem !== null; endItem = endItem.right) {
 | 
			
		||||
    if (!endItem.deleted && endItem.countable) {
 | 
			
		||||
      if (remaining > endItem.length) {
 | 
			
		||||
        remaining -= endItem.length
 | 
			
		||||
      } else {
 | 
			
		||||
        endItem = getItemCleanEnd(transaction, transaction.doc.store, createID(endItem.id.client, endItem.id.clock + remaining - 1))
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (startItem !== null && endItem !== null) {
 | 
			
		||||
    const start = new RelativePosition(null, null, startItem.id, 0)
 | 
			
		||||
    const end = new RelativePosition(null, null, endItem.lastId, -1)
 | 
			
		||||
    const link = new YWeakLink(start, end, startItem)
 | 
			
		||||
    if (parent.doc !== null) {
 | 
			
		||||
      transact(parent.doc, (transaction) => {
 | 
			
		||||
        const end = /** @type {ID} */ (link._quoteEnd.item)
 | 
			
		||||
        for (let item = link._firstItem; item !== null; item = item = item.right) {
 | 
			
		||||
          createLink(transaction, item, link)
 | 
			
		||||
          const lastId = item.lastId
 | 
			
		||||
          if (lastId.client === end.client && lastId.clock === end.clock) {
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    return link
 | 
			
		||||
  }
 | 
			
		||||
  throw invalidQuotedRange
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns a {WeakLink} to an YMap element at given key.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
 * @param {AbstractType<any>} parent
 | 
			
		||||
 * @param {ItemTextListPosition} pos
 | 
			
		||||
 * @param {number} length
 | 
			
		||||
 * @return {YWeakLink<string>}
 | 
			
		||||
 */
 | 
			
		||||
export const quoteText = (transaction, parent, pos, length) => {
 | 
			
		||||
  if (pos.right !== null) {
 | 
			
		||||
    const startItem = pos.right
 | 
			
		||||
    const endIndex = pos.index + length
 | 
			
		||||
    while (pos.index < endIndex) {
 | 
			
		||||
      pos.forward()
 | 
			
		||||
    }
 | 
			
		||||
    if (pos.left !== null) {
 | 
			
		||||
      let endItem = pos.left
 | 
			
		||||
      if (pos.index > endIndex) {
 | 
			
		||||
        const overflow = pos.index - endIndex
 | 
			
		||||
        endItem = getItemCleanEnd(transaction, transaction.doc.store, createID(endItem.id.client, endItem.id.clock + endItem.length - overflow - 1))
 | 
			
		||||
      }
 | 
			
		||||
      const start = new RelativePosition(null, null, startItem.id, 0)
 | 
			
		||||
      const end = new RelativePosition(null, null, endItem.lastId, -1)
 | 
			
		||||
      const link = new YWeakLink(start, end, startItem)
 | 
			
		||||
      if (parent.doc !== null) {
 | 
			
		||||
        transact(parent.doc, (transaction) => {
 | 
			
		||||
          const end = /** @type {ID} */ (link._quoteEnd.item)
 | 
			
		||||
          for (let item = link._firstItem; item !== null; item = item = item.right) {
 | 
			
		||||
            createLink(transaction, item, link)
 | 
			
		||||
            const lastId = item.lastId
 | 
			
		||||
            if (lastId.client === end.client && lastId.clock === end.clock) {
 | 
			
		||||
              break
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      return link
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  throw invalidQuotedRange
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns a {WeakLink} to an YMap element at given key.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {AbstractType<any>} parent
 | 
			
		||||
 * @param {string} key
 | 
			
		||||
 * @return {YWeakLink<any>|undefined}
 | 
			
		||||
 */
 | 
			
		||||
export const mapWeakLink = (parent, key) => {
 | 
			
		||||
  const item = parent._map.get(key)
 | 
			
		||||
  if (item !== undefined) {
 | 
			
		||||
    const start = new RelativePosition(null, null, item.id, 0)
 | 
			
		||||
    const end = new RelativePosition(null, null, item.id, -1)
 | 
			
		||||
    const link = new YWeakLink(start, end, item)
 | 
			
		||||
    if (parent.doc !== null) {
 | 
			
		||||
      transact(parent.doc, (transaction) => {
 | 
			
		||||
        createLink(transaction, item, link)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    return link
 | 
			
		||||
  } else {
 | 
			
		||||
    return undefined
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Establishes a link between source and weak link reference.
 | 
			
		||||
 * It assumes that source has already been split if necessary.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
 * @param {Item} source
 | 
			
		||||
 * @param {YWeakLink<any>} linkRef
 | 
			
		||||
 */
 | 
			
		||||
export const createLink = (transaction, source, linkRef) => {
 | 
			
		||||
  const allLinks = transaction.doc.store.linkedBy
 | 
			
		||||
  map.setIfUndefined(allLinks, source, set.create).add(linkRef)
 | 
			
		||||
  source.linked = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Deletes the link between source and a weak link reference.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
 * @param {Item} source
 | 
			
		||||
 * @param {YWeakLink<any>} linkRef
 | 
			
		||||
 */
 | 
			
		||||
export const unlinkFrom = (transaction, source, linkRef) => {
 | 
			
		||||
  const allLinks = transaction.doc.store.linkedBy
 | 
			
		||||
  const linkedBy = allLinks.get(source)
 | 
			
		||||
  if (linkedBy !== undefined) {
 | 
			
		||||
    linkedBy.delete(linkRef)
 | 
			
		||||
    if (linkedBy.size === 0) {
 | 
			
		||||
      allLinks.delete(source)
 | 
			
		||||
      source.linked = false
 | 
			
		||||
      if (source.countable) {
 | 
			
		||||
        // since linked property is blocking items from merging,
 | 
			
		||||
        // it may turn out that source item can be merged now
 | 
			
		||||
        transaction._mergeStructs.push(source)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Rebinds linkedBy links pointed between neighbours of a current item.
 | 
			
		||||
 * This method expects that current item has both left and right neighbours.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Transaction} transaction
 | 
			
		||||
 * @param {Item} item
 | 
			
		||||
 */
 | 
			
		||||
export const joinLinkedRange = (transaction, item) => {
 | 
			
		||||
  item.linked = true
 | 
			
		||||
  const allLinks = transaction.doc.store.linkedBy
 | 
			
		||||
  const leftLinks = allLinks.get(/** @type {Item} */ (item.left))
 | 
			
		||||
  const rightLinks = allLinks.get(/** @type {Item} */ (item.right))
 | 
			
		||||
  if (leftLinks && rightLinks) {
 | 
			
		||||
    const common = new Set()
 | 
			
		||||
    for (const link of leftLinks) {
 | 
			
		||||
      if (rightLinks.has(link)) {
 | 
			
		||||
        common.add(link)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (common.size !== 0) {
 | 
			
		||||
      allLinks.set(item, common)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -64,36 +64,7 @@ export class YXmlText extends YText {
 | 
			
		||||
 | 
			
		||||
  toString () {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    return this.toDelta().map(delta => {
 | 
			
		||||
      const nestedNodes = []
 | 
			
		||||
      for (const nodeName in delta.attributes) {
 | 
			
		||||
        const attrs = []
 | 
			
		||||
        for (const key in delta.attributes[nodeName]) {
 | 
			
		||||
          attrs.push({ key, value: delta.attributes[nodeName][key] })
 | 
			
		||||
        }
 | 
			
		||||
        // sort attributes to get a unique order
 | 
			
		||||
        attrs.sort((a, b) => a.key < b.key ? -1 : 1)
 | 
			
		||||
        nestedNodes.push({ nodeName, attrs })
 | 
			
		||||
      }
 | 
			
		||||
      // sort node order to get a unique order
 | 
			
		||||
      nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
 | 
			
		||||
      // now convert to dom string
 | 
			
		||||
      let str = ''
 | 
			
		||||
      for (let i = 0; i < nestedNodes.length; i++) {
 | 
			
		||||
        const node = nestedNodes[i]
 | 
			
		||||
        str += `<${node.nodeName}`
 | 
			
		||||
        for (let j = 0; j < node.attrs.length; j++) {
 | 
			
		||||
          const attr = node.attrs[j]
 | 
			
		||||
          str += ` ${attr.key}="${attr.value}"`
 | 
			
		||||
        }
 | 
			
		||||
        str += '>'
 | 
			
		||||
      }
 | 
			
		||||
      str += delta.insert
 | 
			
		||||
      for (let i = nestedNodes.length - 1; i >= 0; i--) {
 | 
			
		||||
        str += `</${nestedNodes[i].nodeName}>`
 | 
			
		||||
      }
 | 
			
		||||
      return str
 | 
			
		||||
    }).join('')
 | 
			
		||||
    return this.toDelta().map(delta => formatXmlString(delta)).join('')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -111,6 +82,43 @@ export class YXmlText extends YText {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Formats individual delta segment provided by `Text.toDelta` into XML-formatted string.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {any} delta
 | 
			
		||||
 * @returns {string}
 | 
			
		||||
 */
 | 
			
		||||
export const formatXmlString = (delta) => {
 | 
			
		||||
  const nestedNodes = []
 | 
			
		||||
  for (const nodeName in delta.attributes) {
 | 
			
		||||
    const attrs = []
 | 
			
		||||
    for (const key in delta.attributes[nodeName]) {
 | 
			
		||||
      attrs.push({ key, value: delta.attributes[nodeName][key] })
 | 
			
		||||
    }
 | 
			
		||||
    // sort attributes to get a unique order
 | 
			
		||||
    attrs.sort((a, b) => a.key < b.key ? -1 : 1)
 | 
			
		||||
    nestedNodes.push({ nodeName, attrs })
 | 
			
		||||
  }
 | 
			
		||||
  // sort node order to get a unique order
 | 
			
		||||
  nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
 | 
			
		||||
  // now convert to dom string
 | 
			
		||||
  let str = ''
 | 
			
		||||
  for (let i = 0; i < nestedNodes.length; i++) {
 | 
			
		||||
    const node = nestedNodes[i]
 | 
			
		||||
    str += `<${node.nodeName}`
 | 
			
		||||
    for (let j = 0; j < node.attrs.length; j++) {
 | 
			
		||||
      const attr = node.attrs[j]
 | 
			
		||||
      str += ` ${attr.key}="${attr.value}"`
 | 
			
		||||
    }
 | 
			
		||||
    str += '>'
 | 
			
		||||
  }
 | 
			
		||||
  str += delta.insert
 | 
			
		||||
  for (let i = nestedNodes.length - 1; i >= 0; i--) {
 | 
			
		||||
    str += `</${nestedNodes[i].nodeName}>`
 | 
			
		||||
  }
 | 
			
		||||
  return str
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
 | 
			
		||||
 * @return {YXmlText}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
import {
 | 
			
		||||
  GC,
 | 
			
		||||
  splitItem,
 | 
			
		||||
  Transaction, ID, Item, DSDecoderV2 // eslint-disable-line
 | 
			
		||||
  Transaction, ID, Item, DSDecoderV2, YWeakLink // eslint-disable-line
 | 
			
		||||
} from '../internals.js'
 | 
			
		||||
 | 
			
		||||
import * as math from 'lib0/math'
 | 
			
		||||
@ -14,6 +14,13 @@ export class StructStore {
 | 
			
		||||
     * @type {Map<number,Array<GC|Item>>}
 | 
			
		||||
     */
 | 
			
		||||
    this.clients = new Map()
 | 
			
		||||
    /**
 | 
			
		||||
     * If this item was referenced by other weak links, here we keep the references
 | 
			
		||||
     * to these weak refs.
 | 
			
		||||
     *
 | 
			
		||||
     * @type {Map<Item, Set<YWeakLink<any>>>}
 | 
			
		||||
     */
 | 
			
		||||
    this.linkedBy = new Map()
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {null | { missing: Map<number, number>, update: Uint8Array }}
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import * as map from './y-map.tests.js'
 | 
			
		||||
import * as array from './y-array.tests.js'
 | 
			
		||||
import * as text from './y-text.tests.js'
 | 
			
		||||
import * as xml from './y-xml.tests.js'
 | 
			
		||||
import * as weak from './y-weak-link.tests.js'
 | 
			
		||||
import * as encoding from './encoding.tests.js'
 | 
			
		||||
import * as undoredo from './undo-redo.tests.js'
 | 
			
		||||
import * as compatibility from './compatibility.tests.js'
 | 
			
		||||
@ -20,7 +21,7 @@ if (isBrowser) {
 | 
			
		||||
  log.createVConsole(document.body)
 | 
			
		||||
}
 | 
			
		||||
runTests({
 | 
			
		||||
  doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
 | 
			
		||||
  doc, map, array, text, xml, weak, encoding, undoredo, compatibility, snapshot, updates, relativePositions
 | 
			
		||||
}).then(success => {
 | 
			
		||||
  /* istanbul ignore next */
 | 
			
		||||
  if (isNode) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										778
									
								
								tests/y-weak-link.tests.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										778
									
								
								tests/y-weak-link.tests.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,778 @@
 | 
			
		||||
import * as Y from '../src/index.js'
 | 
			
		||||
import * as t from 'lib0/testing'
 | 
			
		||||
import { init } from './testHelper.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testBasicMap = tc => {
 | 
			
		||||
  const doc = new Y.Doc()
 | 
			
		||||
  const map = doc.getMap('map')
 | 
			
		||||
 | 
			
		||||
  const nested = new Y.Map()
 | 
			
		||||
  nested.set('a1', 'hello')
 | 
			
		||||
  map.set('a', nested)
 | 
			
		||||
  const link = map.link('a')
 | 
			
		||||
  map.set('b', link)
 | 
			
		||||
 | 
			
		||||
  const link2 = /** @type {Y.WeakLink<any>} */ (map.get('b'))
 | 
			
		||||
  const expected = nested.toJSON()
 | 
			
		||||
  const actual = link2.deref().toJSON()
 | 
			
		||||
  t.compare(actual, expected)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testBasicArray = tc => {
 | 
			
		||||
  const { testConnector, array0, array1 } = init(tc, { users: 2 })
 | 
			
		||||
  array0.insert(0, [1, 2, 3])
 | 
			
		||||
  array0.insert(3, [array0.quote(1)])
 | 
			
		||||
 | 
			
		||||
  t.compare(array0.get(0), 1)
 | 
			
		||||
  t.compare(array0.get(1), 2)
 | 
			
		||||
  t.compare(array0.get(2), 3)
 | 
			
		||||
  t.compare(array0.get(3).deref(), 2)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  t.compare(array1.get(0), 1)
 | 
			
		||||
  t.compare(array1.get(1), 2)
 | 
			
		||||
  t.compare(array1.get(2), 3)
 | 
			
		||||
  t.compare(array1.get(3).deref(), 2)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testArrayQuoteMultipleElements = tc => {
 | 
			
		||||
  const { testConnector, array0, array1 } = init(tc, { users: 2 })
 | 
			
		||||
  const nested = new Y.Map([['key', 'value']])
 | 
			
		||||
  array0.insert(0, [1, 2, nested, 3])
 | 
			
		||||
  array0.insert(0, [array0.quote(1, 3)])
 | 
			
		||||
 | 
			
		||||
  const link0 = array0.get(0)
 | 
			
		||||
  t.compare(link0.unquote(), [2, nested, 3])
 | 
			
		||||
  t.compare(array0.get(1), 1)
 | 
			
		||||
  t.compare(array0.get(2), 2)
 | 
			
		||||
  t.compare(array0.get(3), nested)
 | 
			
		||||
  t.compare(array0.get(4), 3)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const link1 = array1.get(0)
 | 
			
		||||
  let unquoted = link1.unquote()
 | 
			
		||||
  t.compare(unquoted[0], 2)
 | 
			
		||||
  t.compare(unquoted[1].toJSON(), { key: 'value' })
 | 
			
		||||
  t.compare(unquoted[2], 3)
 | 
			
		||||
  t.compare(array1.get(1), 1)
 | 
			
		||||
  t.compare(array1.get(2), 2)
 | 
			
		||||
  t.compare(array1.get(3).toJSON(), { key: 'value' })
 | 
			
		||||
  t.compare(array1.get(4), 3)
 | 
			
		||||
 | 
			
		||||
  array1.insert(3, ['A', 'B'])
 | 
			
		||||
  unquoted = link1.unquote()
 | 
			
		||||
  t.compare(unquoted[0], 2)
 | 
			
		||||
  t.compare(unquoted[1], 'A')
 | 
			
		||||
  t.compare(unquoted[2], 'B')
 | 
			
		||||
  t.compare(unquoted[3].toJSON(), { key: 'value' })
 | 
			
		||||
  t.compare(unquoted[4], 3)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  t.compare(array0.get(0).unquote(), [2, 'A', 'B', nested, 3])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testSelfQuotation = tc => {
 | 
			
		||||
  const { testConnector, array0, array1 } = init(tc, { users: 2 })
 | 
			
		||||
  array0.insert(0, [1, 2, 3, 4])
 | 
			
		||||
  const link0 = array0.quote(0, 3)
 | 
			
		||||
  array0.insert(1, [link0]) // link is inserted into its own range
 | 
			
		||||
 | 
			
		||||
  t.compare(link0.unquote(), [1, link0, 2, 3])
 | 
			
		||||
  t.compare(array0.get(0), 1)
 | 
			
		||||
  t.compare(array0.get(1), link0)
 | 
			
		||||
  t.compare(array0.get(2), 2)
 | 
			
		||||
  t.compare(array0.get(3), 3)
 | 
			
		||||
  t.compare(array0.get(4), 4)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const link1 = array1.get(1)
 | 
			
		||||
  const unquoted = link1.unquote()
 | 
			
		||||
  t.compare(unquoted, [1, link1, 2, 3])
 | 
			
		||||
  t.compare(array1.get(0), 1)
 | 
			
		||||
  t.compare(array1.get(1), link1)
 | 
			
		||||
  t.compare(array1.get(2), 2)
 | 
			
		||||
  t.compare(array1.get(3), 3)
 | 
			
		||||
  t.compare(array1.get(4), 4)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testUpdate = tc => {
 | 
			
		||||
  const { testConnector, map0, map1 } = init(tc, { users: 2 })
 | 
			
		||||
  map0.set('a', new Y.Map([['a1', 'hello']]))
 | 
			
		||||
  const link0 = /** @type {Y.WeakLink<Y.Map<any>>} */ (map0.link('a'))
 | 
			
		||||
  map0.set('b', link0)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<Y.Map<any>>} */ (map1.get('b'))
 | 
			
		||||
  let l1 = /** @type {Y.Map<any>} */ (link1.deref())
 | 
			
		||||
  let l0 = /** @type {Y.Map<any>} */ (link0.deref())
 | 
			
		||||
  t.compare(l1.get('a1'), l0.get('a1'))
 | 
			
		||||
 | 
			
		||||
  map1.get('a').set('a2', 'world')
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  l1 = /** @type {Y.Map<any>} */ (link1.deref())
 | 
			
		||||
  l0 = /** @type {Y.Map<any>} */ (link0.deref())
 | 
			
		||||
  t.compare(l1.get('a2'), l0.get('a2'))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testDeleteWeakLink = tc => {
 | 
			
		||||
  const { testConnector, map0, map1 } = init(tc, { users: 2 })
 | 
			
		||||
  map0.set('a', new Y.Map([['a1', 'hello']]))
 | 
			
		||||
  const link0 = /** @type {Y.WeakLink<Y.Map<any>>} */ (map0.link('a'))
 | 
			
		||||
  map0.set('b', link0)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<Y.Map>} */ map1.get('b')
 | 
			
		||||
  const l1 = /** @type {Y.Map<any>} */ (link1.deref())
 | 
			
		||||
  const l0 = /** @type {Y.Map<any>} */ (link0.deref())
 | 
			
		||||
  t.compare(l1.get('a1'), l0.get('a1'))
 | 
			
		||||
 | 
			
		||||
  map1.delete('b') // delete links
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  // since links have been deleted, they no longer refer to any content
 | 
			
		||||
  t.compare(link0.deref(), undefined)
 | 
			
		||||
  t.compare(link1.deref(), undefined)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testDeleteSource = tc => {
 | 
			
		||||
  const { testConnector, map0, map1 } = init(tc, { users: 2 })
 | 
			
		||||
  map0.set('a', new Y.Map([['a1', 'hello']]))
 | 
			
		||||
  const link0 = /** @type {Y.WeakLink<Y.Map<any>>} */ (map0.link('a'))
 | 
			
		||||
  map0.set('b', link0)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<Y.Map<any>>} */ (map1.get('b'))
 | 
			
		||||
  const l1 = /** @type {Y.Map<any>} */ (link1.deref())
 | 
			
		||||
  const l0 = /** @type {Y.Map<any>} */ (link0.deref())
 | 
			
		||||
  t.compare(l1.get('a1'), l0.get('a1'))
 | 
			
		||||
 | 
			
		||||
  map1.delete('a') // delete source of the link
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  // since source have been deleted, links no longer refer to any content
 | 
			
		||||
  t.compare(link0.deref(), undefined)
 | 
			
		||||
  t.compare(link1.deref(), undefined)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testObserveMapUpdate = tc => {
 | 
			
		||||
  const { testConnector, map0, map1 } = init(tc, { users: 2 })
 | 
			
		||||
  map0.set('a', 'value')
 | 
			
		||||
  const link0 = /** @type {Y.WeakLink<String>} */ (map0.link('a'))
 | 
			
		||||
  map0.set('b', link0)
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {any}
 | 
			
		||||
   */
 | 
			
		||||
  let target0
 | 
			
		||||
  link0.observe((e) => {
 | 
			
		||||
    target0 = e.target
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<String>} */ (map1.get('b'))
 | 
			
		||||
  t.compare(link1.deref(), 'value')
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {any}
 | 
			
		||||
   */
 | 
			
		||||
  let target1
 | 
			
		||||
  link1.observe((e) => {
 | 
			
		||||
    target1 = e.target
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  map0.set('a', 'value2')
 | 
			
		||||
  t.compare(target0.deref(), 'value2')
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
  t.compare(target1.deref(), 'value2')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testObserveMapDelete = tc => {
 | 
			
		||||
  const { testConnector, map0, map1 } = init(tc, { users: 2 })
 | 
			
		||||
  map0.set('a', 'value')
 | 
			
		||||
  const link0 = /** @type {Y.WeakLink<String>} */ (map0.link('a'))
 | 
			
		||||
  map0.set('b', link0)
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {any}
 | 
			
		||||
   */
 | 
			
		||||
  let target0
 | 
			
		||||
  link0.observe((e) => {
 | 
			
		||||
    target0 = e.target
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<String>} */ (map1.get('b'))
 | 
			
		||||
  t.compare(link1.deref(), 'value')
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {any}
 | 
			
		||||
   */
 | 
			
		||||
  let target1
 | 
			
		||||
  link1.observe((e) => {
 | 
			
		||||
    target1 = e.target
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  map0.delete('a')
 | 
			
		||||
  t.compare(target0.deref(), undefined)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
  t.compare(target1.deref(), undefined)
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testObserveArray = tc => {
 | 
			
		||||
  const { testConnector, array0, array1 } = init(tc, { users: 2 })
 | 
			
		||||
  array0.insert(0, ['A', 'B', 'C'])
 | 
			
		||||
  const link0 = /** @type {Y.WeakLink<String>} */ (array0.quote(1, 2))
 | 
			
		||||
  array0.insert(0, [link0])
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {any}
 | 
			
		||||
   */
 | 
			
		||||
  let target0
 | 
			
		||||
  link0.observe((e) => {
 | 
			
		||||
    target0 = e.target
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<String>} */ (array1.get(0))
 | 
			
		||||
  t.compare(link1.unquote(), ['B', 'C'])
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {any}
 | 
			
		||||
   */
 | 
			
		||||
  let target1
 | 
			
		||||
  link1.observe((e) => {
 | 
			
		||||
    target1 = e.target
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  array0.delete(2)
 | 
			
		||||
  t.compare(target0.unquote(), ['C'])
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
  t.compare(target1.unquote(), ['C'])
 | 
			
		||||
 | 
			
		||||
  array1.delete(2)
 | 
			
		||||
  t.compare(target1.unquote(), [])
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
  t.compare(target0.unquote(), [])
 | 
			
		||||
 | 
			
		||||
  target0 = null
 | 
			
		||||
  array0.delete(1)
 | 
			
		||||
  t.compare(target0, null)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testDeepObserveTransitive = tc => {
 | 
			
		||||
  // test observers in a face of linked chains of values
 | 
			
		||||
  const doc = new Y.Doc()
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
     Structure:
 | 
			
		||||
       - map1
 | 
			
		||||
         - link-key: <=+-+
 | 
			
		||||
       - map2:         | |
 | 
			
		||||
         - key: value1-+ |
 | 
			
		||||
         - link-link: <--+
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  const map1 = doc.getMap('map1')
 | 
			
		||||
  const map2 = doc.getMap('map2')
 | 
			
		||||
 | 
			
		||||
  map2.set('key', 'value1')
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<String>} */ (map2.link('key'))
 | 
			
		||||
  map1.set('link-key', link1)
 | 
			
		||||
  const link2 = /** @type {Y.WeakLink<String>} */ (map1.link('link-key'))
 | 
			
		||||
  map2.set('link-link', link2)
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  let events = []
 | 
			
		||||
  link2.observeDeep((e) => {
 | 
			
		||||
    events = e
 | 
			
		||||
  })
 | 
			
		||||
  map2.set('key', 'value2')
 | 
			
		||||
  const values = events.map((e) => e.target.deref())
 | 
			
		||||
  t.compare(values, ['value2'])
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testDeepObserveTransitive2 = tc => {
 | 
			
		||||
  // test observers in a face of multi-layer linked chains of values
 | 
			
		||||
  const doc = new Y.Doc()
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
     Structure:
 | 
			
		||||
       - map1
 | 
			
		||||
         - link-key: <=+-+
 | 
			
		||||
       - map2:         | |
 | 
			
		||||
         - key: value1-+ |
 | 
			
		||||
         - link-link: <==+--+
 | 
			
		||||
       - map3:              |
 | 
			
		||||
         - link-link-link:<-+
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  const map1 = doc.getMap('map1')
 | 
			
		||||
  const map2 = doc.getMap('map2')
 | 
			
		||||
  const map3 = doc.getMap('map3')
 | 
			
		||||
 | 
			
		||||
  map2.set('key', 'value1')
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<String>} */ (map2.link('key'))
 | 
			
		||||
  map1.set('link-key', link1)
 | 
			
		||||
  const link2 = /** @type {Y.WeakLink<String>} */ (map1.link('link-key'))
 | 
			
		||||
  map2.set('link-link', link2)
 | 
			
		||||
  const link3 = /** @type {Y.WeakLink<String>} */ (map2.link('link-link'))
 | 
			
		||||
  map3.set('link-link-link', link3)
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  let events = []
 | 
			
		||||
  link3.observeDeep((e) => {
 | 
			
		||||
    events = e
 | 
			
		||||
  })
 | 
			
		||||
  map2.set('key', 'value2')
 | 
			
		||||
  const values = events.map((e) => e.target.deref())
 | 
			
		||||
  t.compare(values, ['value2'])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testDeepObserveMap = tc => {
 | 
			
		||||
  // test observers in a face of linked chains of values
 | 
			
		||||
  const doc = new Y.Doc()
 | 
			
		||||
  /*
 | 
			
		||||
     Structure:
 | 
			
		||||
       - map (observed):
 | 
			
		||||
         - link:<----+
 | 
			
		||||
       - array:      |
 | 
			
		||||
          0: nested:-+
 | 
			
		||||
            - key: value
 | 
			
		||||
   */
 | 
			
		||||
  const map = doc.getMap('map')
 | 
			
		||||
  const array = doc.getArray('array')
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  let events = []
 | 
			
		||||
  map.observeDeep((es) => {
 | 
			
		||||
    events = es.map((e) => {
 | 
			
		||||
      return { target: e.target, keys: e.keys }
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const nested = new Y.Map()
 | 
			
		||||
  array.insert(0, [nested])
 | 
			
		||||
  const link = array.quote(0)
 | 
			
		||||
  map.set('link', link)
 | 
			
		||||
 | 
			
		||||
  // update entry in linked map
 | 
			
		||||
  events = []
 | 
			
		||||
  nested.set('key', 'value')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, nested)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]]))
 | 
			
		||||
 | 
			
		||||
  // delete entry in linked map
 | 
			
		||||
  events = []
 | 
			
		||||
  nested.delete('key')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, nested)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['key', { action: 'delete', oldValue: 'value' }]]))
 | 
			
		||||
 | 
			
		||||
  // delete linked map
 | 
			
		||||
  array.delete(0)
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, link)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testDeepObserveArray = tc => { // FIXME
 | 
			
		||||
  // test observers in a face of linked chains of values
 | 
			
		||||
  const doc = new Y.Doc()
 | 
			
		||||
  /*
 | 
			
		||||
     Structure:
 | 
			
		||||
       - map:
 | 
			
		||||
         - nested: --------+
 | 
			
		||||
           - key: value    |
 | 
			
		||||
       - array (observed): |
 | 
			
		||||
         0: <--------------+
 | 
			
		||||
   */
 | 
			
		||||
  const map = doc.getMap('map')
 | 
			
		||||
  const array = doc.getArray('array')
 | 
			
		||||
 | 
			
		||||
  const nested = new Y.Map()
 | 
			
		||||
  map.set('nested', nested)
 | 
			
		||||
  const link = map.link('nested')
 | 
			
		||||
  array.insert(0, [link])
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  let events = []
 | 
			
		||||
  array.observeDeep((evts) => {
 | 
			
		||||
    events = []
 | 
			
		||||
    for (const e of evts) {
 | 
			
		||||
      switch (e.constructor) {
 | 
			
		||||
        case Y.YMapEvent:
 | 
			
		||||
          events.push({ target: e.target, keys: e.keys })
 | 
			
		||||
          break
 | 
			
		||||
        case Y.YWeakLinkEvent:
 | 
			
		||||
          events.push({ target: e.target })
 | 
			
		||||
          break
 | 
			
		||||
        default: throw new Error('unexpected event type ' + e.constructor)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // update entry in linked map
 | 
			
		||||
  events = []
 | 
			
		||||
  nested.set('key', 'value')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, nested)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]]))
 | 
			
		||||
 | 
			
		||||
  nested.set('key', 'value2')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, nested)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['key', { action: 'update', oldValue: 'value' }]]))
 | 
			
		||||
 | 
			
		||||
  // delete entry in linked map
 | 
			
		||||
  nested.delete('key')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, nested)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['key', { action: 'delete', oldValue: 'value2' }]]))
 | 
			
		||||
 | 
			
		||||
  // delete linked map
 | 
			
		||||
  map.delete('nested')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, link)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testDeepObserveNewElementWithinQuotedRange = tc => {
 | 
			
		||||
  const { testConnector, array0, array1 } = init(tc, { users: 2 })
 | 
			
		||||
  const m1 = new Y.Map()
 | 
			
		||||
  const m3 = new Y.Map()
 | 
			
		||||
  array0.insert(0, [1, m1, m3, 2])
 | 
			
		||||
  const link0 = array0.quote(1, 2)
 | 
			
		||||
  array0.insert(0, [link0])
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  let e0 = []
 | 
			
		||||
  link0.observeDeep((evts) => {
 | 
			
		||||
    e0 = []
 | 
			
		||||
    for (const e of evts) {
 | 
			
		||||
      switch (e.constructor) {
 | 
			
		||||
        case Y.YMapEvent:
 | 
			
		||||
          e0.push({ target: e.target, keys: e.keys })
 | 
			
		||||
          break
 | 
			
		||||
        case Y.YWeakLinkEvent:
 | 
			
		||||
          e0.push({ target: e.target })
 | 
			
		||||
          break
 | 
			
		||||
        default: throw new Error('unexpected event type ' + e.constructor)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const link1 = /** @type {Y.WeakLink<any>} */ (array1.get(0))
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  let e1 = []
 | 
			
		||||
  link1.observeDeep((evts) => {
 | 
			
		||||
    e1 = []
 | 
			
		||||
    for (const e of evts) {
 | 
			
		||||
      switch (e.constructor) {
 | 
			
		||||
        case Y.YMapEvent:
 | 
			
		||||
          e1.push({ target: e.target, keys: e.keys })
 | 
			
		||||
          break
 | 
			
		||||
        case Y.YWeakLinkEvent:
 | 
			
		||||
          e1.push({ target: e.target })
 | 
			
		||||
          break
 | 
			
		||||
        default: throw new Error('unexpected event type ' + e.constructor)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const m20 = new Y.Map()
 | 
			
		||||
  array0.insert(3, [m20])
 | 
			
		||||
 | 
			
		||||
  m20.set('key', 'value')
 | 
			
		||||
  t.compare(e0.length, 1)
 | 
			
		||||
  t.compare(e0[0].target, m20)
 | 
			
		||||
  t.compare(e0[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]]))
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const m21 = array1.get(3)
 | 
			
		||||
  t.compare(e1.length, 1)
 | 
			
		||||
  t.compare(e1[0].target, m21)
 | 
			
		||||
  t.compare(e1[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]]))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testMapDeepObserve = tc => { // FIXME
 | 
			
		||||
  const doc = new Y.Doc()
 | 
			
		||||
  const outer = doc.getMap('outer')
 | 
			
		||||
  const inner = new Y.Map()
 | 
			
		||||
  outer.set('inner', inner)
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  let events = []
 | 
			
		||||
  outer.observeDeep((evts) => {
 | 
			
		||||
    events = []
 | 
			
		||||
    for (const e of evts) {
 | 
			
		||||
      switch (e.constructor) {
 | 
			
		||||
        case Y.YMapEvent:
 | 
			
		||||
          events.push({ target: e.target, keys: e.keys })
 | 
			
		||||
          break
 | 
			
		||||
        case Y.YWeakLinkEvent:
 | 
			
		||||
          events.push({ target: e.target })
 | 
			
		||||
          break
 | 
			
		||||
        default: throw new Error('unexpected event type ' + e.constructor)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  inner.set('key', 'value1')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, inner)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['key', { action: 'add', oldValue: undefined }]]))
 | 
			
		||||
 | 
			
		||||
  events = []
 | 
			
		||||
  inner.set('key', 'value2')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, inner)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['key', { action: 'update', oldValue: 'value1' }]]))
 | 
			
		||||
 | 
			
		||||
  events = []
 | 
			
		||||
  inner.delete('key')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, inner)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['key', { action: 'delete', oldValue: 'value2' }]]))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testDeepObserveRecursive = tc => {
 | 
			
		||||
  // test observers in a face of cycled chains of values
 | 
			
		||||
  const doc = new Y.Doc()
 | 
			
		||||
  /*
 | 
			
		||||
     Structure:
 | 
			
		||||
      array (observed):
 | 
			
		||||
        m0:--------+
 | 
			
		||||
         - k1:<-+  |
 | 
			
		||||
                |  |
 | 
			
		||||
        m1------+  |
 | 
			
		||||
         - k2:<-+  |
 | 
			
		||||
                |  |
 | 
			
		||||
        m2------+  |
 | 
			
		||||
         - k0:<----+
 | 
			
		||||
   */
 | 
			
		||||
  const root = doc.getArray('array')
 | 
			
		||||
 | 
			
		||||
  const m0 = new Y.Map()
 | 
			
		||||
  const m1 = new Y.Map()
 | 
			
		||||
  const m2 = new Y.Map()
 | 
			
		||||
 | 
			
		||||
  root.insert(0, [m0])
 | 
			
		||||
  root.insert(1, [m1])
 | 
			
		||||
  root.insert(2, [m2])
 | 
			
		||||
 | 
			
		||||
  const l0 = root.quote(0)
 | 
			
		||||
  const l1 = root.quote(1)
 | 
			
		||||
  const l2 = root.quote(2)
 | 
			
		||||
 | 
			
		||||
  // create cyclic reference between links
 | 
			
		||||
  m0.set('k1', l1)
 | 
			
		||||
  m1.set('k2', l2)
 | 
			
		||||
  m2.set('k0', l0)
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @type {Array<any>}
 | 
			
		||||
   */
 | 
			
		||||
  let events = []
 | 
			
		||||
  m0.observeDeep((es) => {
 | 
			
		||||
    events = es.map((e) => { 
 | 
			
		||||
      return { target: e.target, keys: e.keys }
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  m1.set('test-key1', 'value1')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, m1)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['test-key1', { action: 'add', oldValue: undefined }]]))
 | 
			
		||||
 | 
			
		||||
  events = []
 | 
			
		||||
  m2.set('test-key2', 'value2')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, m2)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['test-key2', { action: 'add', oldValue: undefined }]]))
 | 
			
		||||
 | 
			
		||||
  m1.delete('test-key1')
 | 
			
		||||
  t.compare(events.length, 1)
 | 
			
		||||
  t.compare(events[0].target, m1)
 | 
			
		||||
  t.compare(events[0].keys, new Map([['test-key1', { action: 'delete', oldValue: 'value1' }]]))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testRemoteMapUpdate = tc => {
 | 
			
		||||
  const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
 | 
			
		||||
 | 
			
		||||
  map0.set('key', 1)
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  map1.set('link', map1.link('key'))
 | 
			
		||||
  map0.set('key', 2)
 | 
			
		||||
  map0.set('key', 3)
 | 
			
		||||
 | 
			
		||||
  // apply updated content first, link second
 | 
			
		||||
  Y.applyUpdate(users[2], Y.encodeStateAsUpdate(users[0]))
 | 
			
		||||
  Y.applyUpdate(users[2], Y.encodeStateAsUpdate(users[1]))
 | 
			
		||||
 | 
			
		||||
  // make sure that link can find the most recent block
 | 
			
		||||
  const link2 = map2.get('link')
 | 
			
		||||
  t.compare(link2.deref(), 3)
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const link1 = map1.get('link')
 | 
			
		||||
  const link0 = map0.get('link')
 | 
			
		||||
 | 
			
		||||
  t.compare(link0.deref(), 3)
 | 
			
		||||
  t.compare(link1.deref(), 3)
 | 
			
		||||
  t.compare(link2.deref(), 3)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testTextBasic = tc => {
 | 
			
		||||
  const { testConnector, text0, text1 } = init(tc, { users: 2 })
 | 
			
		||||
 | 
			
		||||
  text0.insert(0, 'abcd') // 'abcd'
 | 
			
		||||
  const link0 = text0.quote(1, 2) // quote: [bc]
 | 
			
		||||
  t.compare(link0.toString(), 'bc')
 | 
			
		||||
  text0.insert(2, 'ef') // 'abefcd', quote: [befc]
 | 
			
		||||
  t.compare(link0.toString(), 'befc')
 | 
			
		||||
  text0.delete(3, 3) // 'abe', quote: [be]
 | 
			
		||||
  t.compare(link0.toString(), 'be')
 | 
			
		||||
  text0.insertEmbed(3, link0) // 'abe[be]'
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
 | 
			
		||||
  const delta = text1.toDelta()
 | 
			
		||||
  const { insert } = delta[1] // YWeakLink
 | 
			
		||||
  t.compare(insert.toString(), 'be')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testXmlTextBasic = tc => {
 | 
			
		||||
  const { testConnector, xml0, xml1 } = init(tc, { users: 2 })
 | 
			
		||||
  const text0 = new Y.XmlText()
 | 
			
		||||
  xml0.insert(0, [text0])
 | 
			
		||||
 | 
			
		||||
  text0.insert(0, 'abcd') // 'abcd'
 | 
			
		||||
  const link0 = text0.quote(1, 2) // quote: [bc]
 | 
			
		||||
  t.compare(link0.toString(), 'bc')
 | 
			
		||||
  text0.insert(2, 'ef') // 'abefcd', quote: [befc]
 | 
			
		||||
  t.compare(link0.toString(), 'befc')
 | 
			
		||||
  text0.delete(3, 3) // 'abe', quote: [be]
 | 
			
		||||
  t.compare(link0.toString(), 'be')
 | 
			
		||||
  text0.insertEmbed(3, link0) // 'abe[be]'
 | 
			
		||||
 | 
			
		||||
  testConnector.flushAllMessages()
 | 
			
		||||
  const text1 = /** @type {Y.XmlText} */ (xml1.get(0))
 | 
			
		||||
  const delta = text1.toDelta()
 | 
			
		||||
  const { insert } = delta[1] // YWeakLink
 | 
			
		||||
  t.compare(insert.toString(), 'be')
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * @param {t.TestCase} tc
 | 
			
		||||
 */
 | 
			
		||||
export const testQuoteFormattedText = tc => {
 | 
			
		||||
  const doc = new Y.Doc()
 | 
			
		||||
  const text = /** @type {Y.XmlText} */ (doc.get('text', Y.XmlText))
 | 
			
		||||
  const text2 = /** @type {Y.XmlText} */ (doc.get('text2', Y.XmlText))
 | 
			
		||||
 | 
			
		||||
  text.insert(0, 'abcde')
 | 
			
		||||
  text.format(0, 1, { b: true })
 | 
			
		||||
  text.format(1, 3, { i: true }) // '<b>a</b><i>bcd</i>e'
 | 
			
		||||
  const l1 = text.quote(0, 2)
 | 
			
		||||
  t.compare(l1.toString(), '<b>a</b><i>b</i>')
 | 
			
		||||
  const l2 = text.quote(2, 1) // '<i>c</i>'
 | 
			
		||||
  t.compare(l2.toString(), '<i>c</i>')
 | 
			
		||||
  const l3 = text.quote(3, 2) // '<i>d</i>e'
 | 
			
		||||
  t.compare(l3.toString(), '<i>d</i>e')
 | 
			
		||||
 | 
			
		||||
  text2.insertEmbed(0, l1)
 | 
			
		||||
  text2.insertEmbed(1, l2)
 | 
			
		||||
  text2.insertEmbed(2, l3)
 | 
			
		||||
 | 
			
		||||
  const delta = text2.toDelta()
 | 
			
		||||
  t.compare(delta, [
 | 
			
		||||
    { insert: l1 },
 | 
			
		||||
    { insert: l2 },
 | 
			
		||||
    { insert: l3 }
 | 
			
		||||
  ])
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user