UNPKG

@y/y

Version:

Shared Editing Library

1,519 lines (1,388 loc) • 32.7 kB
import * as error from 'lib0/error' import * as binary from 'lib0/binary' import * as env from 'lib0/environment' import * as object from 'lib0/object' import { AbstractStruct, addStructToIdSet } from '../structs/AbstractStruct.js' import { ID, createID, compareIDs, findRootTypeKey } from '../utils/ID.js' import { GC } from '../structs/GC.js' import { replaceStruct, getItemCleanEnd, addChangedTypeToTransaction } from '../utils/transaction-helpers.js' const isDevMode = env.getVariable('node_env') === 'development' /** * @todo This should return several items * * @param {StructStore} store * @param {ID} id * @return {{item:Item, diff:number}} */ export const followRedone = (store, id) => { /** * @type {ID|null} */ let nextID = id let diff = 0 let item do { if (diff > 0) { nextID = createID(nextID.client, nextID.clock + diff) } item = store.getItem(nextID) diff = nextID.clock - item.id.clock nextID = item.redone } while (nextID !== null && item.isItem) return { item, diff } } /** * Abstract class that represents any content. */ export class Item extends AbstractStruct { /** * @param {ID} id * @param {Item | null} left * @param {ID | null} origin * @param {Item | null} right * @param {ID | null} rightOrigin * @param {YType|ID|string|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, is string if child of top-level-parent * @param {string | null} parentSub * @param {AbstractContent} content */ constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) { super(id, content.getLength()) /** * The item that was originally to the left of this item. * @type {ID | null} */ this.origin = origin /** * The item that is currently to the left of this item. * @type {Item | null} */ this.left = left /** * The item that is currently to the right of this item. * @type {Item | null} */ this.right = right /** * The item that was originally to the right of this item. * @type {ID | null} */ this.rightOrigin = rightOrigin /** * @type {YType|ID|string|null} */ this.parent = parent /** * 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 = parentSub /** * If this type's effect is redone this type refers to the type that undid * this operation. * @type {ID | null} */ this.redone = null /** * @type {AbstractContent} */ this.content = content /** * bit1: keep * bit2: countable * bit3: deleted * bit4: mark - mark node as fast-search-marker * @type {number} byte */ this.info = this.content.isCountable() ? binary.BIT2 : 0 } /** * This is used to mark the item as an indexed fast-search marker * * @type {boolean} */ set marker (isMarked) { if (((this.info & binary.BIT4) > 0) !== isMarked) { this.info ^= binary.BIT4 } } get marker () { return (this.info & binary.BIT4) > 0 } /** * If true, do not garbage collect this Item. */ get keep () { return (this.info & binary.BIT1) > 0 } set keep (doKeep) { if (this.keep !== doKeep) { this.info ^= binary.BIT1 } } get countable () { return (this.info & binary.BIT2) > 0 } /** * Whether this item was deleted or not. * @type {Boolean} */ get deleted () { return (this.info & binary.BIT3) > 0 } set deleted (doDelete) { if (this.deleted !== doDelete) { this.info ^= binary.BIT3 } } markDeleted () { this.info |= binary.BIT3 } /** * @param {Transaction} transaction * @param {number} offset */ integrate (transaction, offset) { if (offset > 0) { this.id.clock += offset this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1)) this.origin = this.left.lastId this.content = this.content.splice(offset) this.length -= offset } if (this.parent) { if ((!this.left && (!this.right || this.right.left !== null)) || (this.left && this.left.right !== this.right)) { /** * @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 (this.parentSub !== null) { o = /** @type {YType} */ (this.parent)._map.get(this.parentSub) || null while (o !== null && o.left !== null) { o = o.left } } else { o = /** @type {YType} */ (this.parent)._start } // 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 (compareIDs(this.rightOrigin, o.rightOrigin)) { // this and o are conflicting and point to the same integration points. The id decides which item comes first. // Since this is to the left of o, we can break here break } // else, o might be integrated before an item that this conflicts with. If so, we will find it in the next iterations } else if (o.origin !== null && itemsBeforeOrigin.has(transaction.doc.store.getItem(o.origin))) { // use getItem instead of getItemCleanEnd because we don't want / need to split items. // case 2 if (!conflictingItems.has(transaction.doc.store.getItem(o.origin))) { left = o conflictingItems.clear() } } else { break } o = o.right } this.left = left } // reconnect left/right + update parent map/start if necessary if (this.left !== null) { const right = this.left.right this.right = right this.left.right = this } else { let r if (this.parentSub !== null) { r = /** @type {YType} */ (this.parent)._map.get(this.parentSub) || null while (r !== null && r.left !== null) { r = r.left } } else { r = /** @type {YType} */ (this.parent)._start ;/** @type {YType} */ (this.parent)._start = this } this.right = r } if (this.right !== null) { this.right.left = this } else if (this.parentSub !== null) { // set as current parent value if right === null and this is parentSub /** @type {YType} */ (this.parent)._map.set(this.parentSub, this) if (this.left !== null) { // 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 {YType} */ (this.parent)._length += this.length } addStructToIdSet(transaction.insertSet, this) transaction.doc.store.add(this) this.content.integrate(transaction, this) // add parent to transaction.changed addChangedTypeToTransaction(transaction, /** @type {YType} */ (this.parent), this.parentSub) if ((/** @type {YType} */ (this.parent)._item !== null && /** @type {YType} */ (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) } } else { // parent is not defined. Integrate GC struct instead new GC(this.id, this.length).integrate(transaction, 0) } } /** * Returns the next non-deleted item */ get next () { let n = this.right while (n !== null && n.deleted) { n = n.right } return n } /** * Returns the previous non-deleted item */ get prev () { let n = this.left while (n !== null && n.deleted) { n = n.left } return n } /** * Computes the last content address of this Item. */ get lastId () { // allocating ids is pretty costly because of the amount of ids created, so we try to reuse whenever possible return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1) } /** * Try to merge two items * * @param {Item} right * @return {boolean} */ mergeWith (right) { if ( this.constructor === right.constructor && compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin) && this.id.client === right.id.client && this.id.clock + this.length === right.id.clock && this.deleted === right.deleted && this.redone === null && right.redone === null && this.content.constructor === right.content.constructor && this.content.mergeWith(right.content) ) { const searchMarker = /** @type {YType} */ (this.parent)._searchMarker if (searchMarker) { searchMarker.forEach(marker => { if (marker.p === right) { // right is going to be "forgotten" so we need to update the marker marker.p = this // adjust marker index if (!this.deleted && this.countable) { marker.index -= this.length } } }) } if (right.keep) { this.keep = true } this.right = right.right if (this.right !== null) { this.right.left = this } this.length += right.length return true } return false } /** * Mark this Item as deleted. * * @param {Transaction} transaction */ delete (transaction) { if (!this.deleted) { const parent = /** @type {YType} */ (this.parent) // adjust the length of parent if (this.countable && this.parentSub === null) { parent._length -= this.length } this.markDeleted() transaction.deleteSet.add(this.id.client, this.id.clock, this.length) addChangedTypeToTransaction(transaction, parent, this.parentSub) this.content.delete(transaction) } } /** * @param {Transaction} tr * @param {boolean} parentGCd */ gc (tr, parentGCd) { if (!this.deleted) { throw error.unexpectedCase() } this.content.gc(tr) if (parentGCd) { replaceStruct(tr, this, new GC(this.id, this.length)) } else { this.content = new ContentDeleted(this.length) } } /** * Split this into two items * @param {Transaction?} transaction * @param {number} diff * @return {Item} */ split (transaction, diff) { // create rightItem const { client, clock } = this.id const rightItem = new Item( createID(client, clock + diff), this, createID(client, clock + diff - 1), this.right, this.rightOrigin, this.parent, this.parentSub, this.content.splice(diff) ) if (this.deleted) { rightItem.markDeleted() } if (this.keep) { rightItem.keep = true } if (this.redone !== null) { rightItem.redone = createID(this.redone.client, this.redone.clock + diff) } if (transaction != null) { // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing) this.right = rightItem // update right if (rightItem.right !== null) { rightItem.right.left = rightItem } // right is more specific. transaction._mergeStructs.push(rightItem) // update parent._map if (rightItem.parentSub !== null && rightItem.right === null) { /** @type {YType} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem) } } else { rightItem.left = null rightItem.right = null } this.length = diff return rightItem } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. * @param {number} offset * @param {number} offsetEnd */ write (encoder, offset, offsetEnd) { const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin const rightOrigin = this.rightOrigin const parentSub = this.parentSub const info = (this.content.getRef() & binary.BITS5) | (origin === null ? 0 : binary.BIT8) | // origin is defined (rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined (parentSub === null ? 0 : binary.BIT6) // parentSub is non-null encoder.writeInfo(info) if (origin !== null) { encoder.writeLeftID(origin) } if (rightOrigin !== null) { encoder.writeRightID(rightOrigin) } if (origin === null && rightOrigin === null) { const parent = /** @type {YType} */ (this.parent) if (parent._item !== undefined) { const parentItem = parent._item if (parentItem === null) { // parent type on y._map // find the correct key const ykey = findRootTypeKey(parent) encoder.writeParentInfo(true) // write parentYKey encoder.writeString(ykey) } else { encoder.writeParentInfo(false) // write parent id encoder.writeLeftID(parentItem.id) } } else if (parent.constructor === String) { // this edge case was added by differential updates encoder.writeParentInfo(true) // write parentYKey encoder.writeString(parent) } else if (parent.constructor === ID) { encoder.writeParentInfo(false) // write parent id encoder.writeLeftID(parent) } else { error.unexpectedCase() } if (parentSub !== null) { encoder.writeString(parentSub) } } this.content.write(encoder, offset, offsetEnd) } get ref () { return this.content.getRef() } } /** * @type {true} */ Item.prototype.isItem = true /** * Do not implement this class! */ export class AbstractContent { /** * @return {number} */ getLength () { throw error.methodUnimplemented() } /** * @return {Array<any>} */ getContent () { throw error.methodUnimplemented() } /** * Should return false if this Item is some kind of meta information * (e.g. format information). * * * Whether this Item should be addressable via `yarray.get(i)` * * Whether this Item should be counted when computing yarray.length * * @return {boolean} */ isCountable () { throw error.methodUnimplemented() } /** * @return {AbstractContent} */ copy () { throw error.methodUnimplemented() } /** * @param {number} _offset * @return {AbstractContent} */ splice (_offset) { throw error.methodUnimplemented() } /** * @param {AbstractContent} _right * @return {boolean} */ mergeWith (_right) { throw error.methodUnimplemented() } /** * @param {Transaction} _transaction * @param {Item} _item */ integrate (_transaction, _item) { throw error.methodUnimplemented() } /** * @param {Transaction} _transaction */ delete (_transaction) { throw error.methodUnimplemented() } /** * @param {Transaction} _transaction */ gc (_transaction) { throw error.methodUnimplemented() } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder * @param {number} _offset * @param {number} _offsetEnd */ write (_encoder, _offset, _offsetEnd) { throw error.methodUnimplemented() } /** * @return {1|2|3|4|5|6|7|8|9} */ getRef () { throw error.methodUnimplemented() } } export class ContentAny { /** * @param {Array<any>} arr */ constructor (arr) { /** * @type {Array<any>} */ this.arr = arr isDevMode && object.deepFreeze(arr) } /** * @return {number} */ getLength () { return this.arr.length } /** * @return {Array<any>} */ getContent () { return this.arr } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentAny} */ copy () { return new ContentAny(this.arr) } /** * @param {number} offset * @return {ContentAny} */ splice (offset) { const right = new ContentAny(this.arr.slice(offset)) this.arr = this.arr.slice(0, offset) return right } /** * @param {ContentAny} right * @return {boolean} */ mergeWith (right) { this.arr = this.arr.concat(right.arr) return true } /** * @param {Transaction} _transaction * @param {Item} _item */ integrate (_transaction, _item) {} /** * @param {Transaction} _transaction */ delete (_transaction) {} /** * @param {Transaction} _tr */ gc (_tr) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset * @param {number} offsetEnd */ write (encoder, offset, offsetEnd) { const end = this.arr.length - offsetEnd encoder.writeLen(end - offset) for (let i = offset; i < end; i++) { const c = this.arr[i] encoder.writeAny(c) } } /** * @return {8} */ getRef () { return 8 } } export class ContentBinary { /** * @param {Uint8Array} content */ constructor (content) { this.content = content } /** * @return {number} */ getLength () { return 1 } /** * @return {Array<any>} */ getContent () { return [this.content] } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentBinary} */ copy () { return new ContentBinary(this.content) } /** * @param {number} _offset * @return {ContentBinary} */ splice (_offset) { throw error.methodUnimplemented() } /** * @param {ContentBinary} _right * @return {boolean} */ mergeWith (_right) { return false } /** * @param {Transaction} _transaction * @param {Item} _item */ integrate (_transaction, _item) {} /** * @param {Transaction} _transaction */ delete (_transaction) {} /** * @param {Transaction} _tr */ gc (_tr) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} _offset * @param {number} _offsetEnd */ write (encoder, _offset, _offsetEnd) { encoder.writeBuf(this.content) } /** * @return {3} */ getRef () { return 3 } } export class ContentDeleted { /** * @param {number} len */ constructor (len) { this.len = len } /** * @return {number} */ getLength () { return this.len } /** * @return {Array<any>} */ getContent () { return [] } /** * @return {boolean} */ isCountable () { return false } /** * @return {ContentDeleted} */ copy () { return new ContentDeleted(this.len) } /** * @param {number} offset * @return {ContentDeleted} */ splice (offset) { const right = new ContentDeleted(this.len - offset) this.len = offset return right } /** * @param {ContentDeleted} right * @return {boolean} */ mergeWith (right) { this.len += right.len return true } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) { transaction.deleteSet.add(item.id.client, item.id.clock, this.len) item.markDeleted() } /** * @param {Transaction} _transaction */ delete (_transaction) {} /** * @param {Transaction} _tr */ gc (_tr) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset * @param {number} offsetEnd */ write (encoder, offset, offsetEnd) { encoder.writeLen(this.len - offset - offsetEnd) } /** * @return {1} */ getRef () { return 1 } } /** * @private */ export class ContentDoc { /** * @param {string} guid * @param {Object<string,any>} opts */ constructor (guid, opts) { /** * @type {Doc?} */ this.doc = null this.guid = guid this.opts = opts } /** * @return {number} */ getLength () { return 1 } /** * @return {Array<any>} */ getContent () { return [this.doc] } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentDoc} */ copy () { return new ContentDoc(this.guid, this.opts) } /** * @param {number} _offset * @return {ContentDoc} */ splice (_offset) { throw error.methodUnimplemented() } /** * @param {ContentDoc} _right * @return {boolean} */ mergeWith (_right) { return false } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) { const opts = this.opts if (this.doc == null) { // we get the constructor from the existing doc to avoid import the doc module, leading to a // circular dependency this.doc = /** @type {Doc} */ (new /** @type {any} */ (transaction.doc.constructor)({ guid: this.guid, ...this.opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false })) } this.doc._item = item transaction.subdocsAdded.add(this.doc) if (this.doc.shouldLoad) { transaction.subdocsLoaded.add(this.doc) } } /** * @param {Transaction} transaction */ delete (transaction) { if (this.doc) { if (transaction.subdocsAdded.has(this.doc)) { transaction.subdocsAdded.delete(this.doc) } else { transaction.subdocsRemoved.add(this.doc) } } } /** * @param {Transaction} _tr */ gc (_tr) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} _offset * @param {number} _offsetEnd */ write (encoder, _offset, _offsetEnd) { encoder.writeString(this.guid) encoder.writeAny(this.opts) } /** * @return {9} */ getRef () { return 9 } } /** * @param {Doc} ydoc */ export const createContentDocFromDoc = ydoc => { /** * @type {any} */ const opts = {} if (!ydoc.gc) { opts.gc = false } if (ydoc.autoLoad) { opts.autoLoad = true } if (ydoc.meta !== null) { opts.meta = ydoc.meta } const c = new ContentDoc(ydoc.guid, opts) c.doc = ydoc return c } /** * @private */ export class ContentEmbed { /** * @param {Object} embed */ constructor (embed) { this.embed = embed } /** * @return {number} */ getLength () { return 1 } /** * @return {Array<any>} */ getContent () { return [this.embed] } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentEmbed} */ copy () { return new ContentEmbed(this.embed) } /** * @param {number} _offset * @return {ContentEmbed} */ splice (_offset) { throw error.methodUnimplemented() } /** * @param {ContentEmbed} _right * @return {boolean} */ mergeWith (_right) { return false } /** * @param {Transaction} _transaction * @param {Item} _item */ integrate (_transaction, _item) {} /** * @param {Transaction} _transaction */ delete (_transaction) {} /** * @param {Transaction} _tr */ gc (_tr) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} _offset * @param {number} _offsetEnd */ write (encoder, _offset, _offsetEnd) { encoder.writeJSON(this.embed) } /** * @return {5} */ getRef () { return 5 } } /** * @private */ export class ContentFormat { /** * @param {string} key * @param {Object} value */ constructor (key, value) { this.key = key this.value = value } /** * @return {number} */ getLength () { return 1 } /** * @return {Array<any>} */ getContent () { return [] } /** * @return {boolean} */ isCountable () { return false } /** * @return {ContentFormat} */ copy () { return new ContentFormat(this.key, this.value) } /** * @param {number} _offset * @return {ContentFormat} */ splice (_offset) { throw error.methodUnimplemented() } /** * @param {ContentFormat} _right * @return {boolean} */ mergeWith (_right) { return false } /** * @param {Transaction} _transaction * @param {Item} item */ integrate (_transaction, item) { // @todo searchmarker are currently unsupported for rich text documents const p = /** @type {import('../ytype.js').YType<any>} */ (item.parent) p._searchMarker = null p._hasFormatting = true } /** * @param {Transaction} _transaction */ delete (_transaction) {} /** * @param {Transaction} _tr */ gc (_tr) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} _offset * @param {number} _offsetEnd */ write (encoder, _offset, _offsetEnd) { encoder.writeKey(this.key) encoder.writeJSON(this.value) } /** * @return {6} */ getRef () { return 6 } } /** * @private */ export class ContentJSON { /** * @param {Array<any>} arr */ constructor (arr) { /** * @type {Array<any>} */ this.arr = arr } /** * @return {number} */ getLength () { return this.arr.length } /** * @return {Array<any>} */ getContent () { return this.arr } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentJSON} */ copy () { return new ContentJSON(this.arr) } /** * @param {number} offset * @return {ContentJSON} */ splice (offset) { const right = new ContentJSON(this.arr.slice(offset)) this.arr = this.arr.slice(0, offset) return right } /** * @param {ContentJSON} right * @return {boolean} */ mergeWith (right) { this.arr = this.arr.concat(right.arr) return true } /** * @param {Transaction} _transaction * @param {Item} _item */ integrate (_transaction, _item) {} /** * @param {Transaction} _transaction */ delete (_transaction) {} /** * @param {Transaction} _tr */ gc (_tr) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset * @param {number} offsetEnd */ write (encoder, offset, offsetEnd) { const end = this.arr.length - offsetEnd encoder.writeLen(end - offset) for (let i = offset; i < end; i++) { const c = this.arr[i] encoder.writeString(c === undefined ? 'undefined' : JSON.stringify(c)) } } /** * @return {2} */ getRef () { return 2 } } /** * @private */ export class ContentString { /** * @param {string} str */ constructor (str) { /** * @type {string} */ this.str = str } /** * @return {number} */ getLength () { return this.str.length } /** * @return {Array<any>} */ getContent () { return this.str.split('') } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentString} */ copy () { return new ContentString(this.str) } /** * @param {number} offset * @return {ContentString} */ splice (offset) { const right = new ContentString(this.str.slice(offset)) this.str = this.str.slice(0, offset) // Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248 const firstCharCode = this.str.charCodeAt(offset - 1) if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF) { // Last character of the left split is the start of a surrogate utf16/ucs2 pair. // We don't support splitting of surrogate pairs because this may lead to invalid documents. // Replace the invalid character with a unicode replacement character (� / U+FFFD) this.str = this.str.slice(0, offset - 1) + '�' // replace right as well right.str = '�' + right.str.slice(1) } return right } /** * @param {ContentString} right * @return {boolean} */ mergeWith (right) { this.str += right.str return true } /** * @param {Transaction} _transaction * @param {Item} _item */ integrate (_transaction, _item) {} /** * @param {Transaction} _transaction */ delete (_transaction) {} /** * @param {Transaction} _tr */ gc (_tr) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset * @param {number} offsetEnd */ write (encoder, offset, offsetEnd) { encoder.writeString((offset === 0 && offsetEnd === 0) ? this.str : this.str.slice(offset, this.str.length - offsetEnd)) } /** * @return {4} */ getRef () { return 4 } } export const YArrayRefID = 0 export const YMapRefID = 1 export const YTextRefID = 2 export const YXmlElementRefID = 3 export const YXmlFragmentRefID = 4 export const YXmlHookRefID = 5 export const YXmlTextRefID = 6 /** * @private */ export class ContentType { /** * @param {import('../ytype.js').YType} type */ constructor (type) { /** * @type {import('../ytype.js').YType} */ this.type = type } /** * @return {number} */ getLength () { return 1 } /** * @return {Array<any>} */ getContent () { return [this.type] } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentType} */ copy () { return new ContentType(this.type._copy()) } /** * @param {number} _offset * @return {ContentType} */ splice (_offset) { throw error.methodUnimplemented() } /** * @param {ContentType} _right * @return {boolean} */ mergeWith (_right) { return false } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) { this.type._integrate(transaction.doc, item) } /** * @param {Transaction} transaction */ delete (transaction) { let item = this.type._start while (item !== null) { if (!item.deleted) { item.delete(transaction) } else if (!transaction.insertSet.hasId(item.id)) { // This will be gc'd later and we want to merge it if possible // We try to merge all deleted items after each transaction, // but we have no knowledge about that this needs to be merged // since it is not in transaction.ds. Hence we add it to transaction._mergeStructs transaction._mergeStructs.push(item) } item = item.right } this.type._map.forEach(item => { if (!item.deleted) { item.delete(transaction) } else if (!transaction.insertSet.hasId(item.id)) { // same as above transaction._mergeStructs.push(item) } }) transaction.changed.delete(this.type) } /** * @param {Transaction} tr */ gc (tr) { let item = this.type._start while (item !== null) { item.gc(tr, true) item = item.right } this.type._start = null this.type._map.forEach(/** @param {Item | null} item */ (item) => { while (item !== null) { item.gc(tr, true) item = item.left } }) this.type._map = new Map() } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} _offset * @param {number} _offsetEnd */ write (encoder, _offset, _offsetEnd) { this.type._write(encoder) } /** * @return {7} */ getRef () { return 7 } }