@y/y
Version:
Shared Editing Library
1,518 lines (1,387 loc) • 32.7 kB
JavaScript
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 {YNode|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 {YNode|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 {YNode} */ (this.parent)._map.get(this.parentSub) || null
while (o !== null && o.left !== null) {
o = o.left
}
} else {
o = /** @type {YNode} */ (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 {YNode} */ (this.parent)._map.get(this.parentSub) || null
while (r !== null && r.left !== null) {
r = r.left
}
} else {
r = /** @type {YNode} */ (this.parent)._start
;/** @type {YNode} */ (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 {YNode} */ (this.parent)._map.set(this.parentSub, this)
if (this.left !== null) {
// this is the current attribute value of parent. delete the previous value
this.left.delete(transaction)
}
}
// adjust length of parent
if (this.parentSub === null && this.countable && !this.deleted) {
/** @type {YNode} */ (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 {YNode} */ (this.parent), this.parentSub)
if ((/** @type {YNode} */ (this.parent)._item !== null && /** @type {YNode} */ (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 {YNode} */ (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 {YNode} */ (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 {YNode} */ (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 {YNode} */ (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('../ynode.js').YNode<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('../ynode.js').YNode} type
*/
constructor (type) {
/**
* @type {import('../ynode.js').YNode}
*/
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)
}
})
}
/**
* @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
}
}