import { getStructReference } from '../Util/structReferences.mjs' import ID from '../Util/ID/ID.mjs' import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.mjs' import Delete from './Delete.mjs' import { transactionTypeChanged, writeStructToTransaction } from '../Transaction.mjs' import GC from './GC.mjs' /** * @private * Helper utility to split an Item (see {@link Item#_splitAt}) * - copies all properties from a to b * - connects a to b * - assigns the correct _id * - saves b to os */ export function splitHelper (y, a, b, diff) { const aID = a._id b._id = new ID(aID.user, aID.clock + diff) b._origin = a b._left = a b._right = a._right if (b._right !== null) { b._right._left = b } b._right_origin = a._right_origin // do not set a._right_origin, as this will lead to problems when syncing a._right = b b._parent = a._parent b._parentSub = a._parentSub b._deleted = a._deleted // now search all relevant items to the right and update origin // if origin is not it foundOrigins, we don't have to search any longer let foundOrigins = new Set() foundOrigins.add(a) let o = b._right while (o !== null && foundOrigins.has(o._origin)) { if (o._origin === a) { o._origin = b } foundOrigins.add(o) o = o._right } y.os.put(b) if (y._transaction.newTypes.has(a)) { y._transaction.newTypes.add(b) } else if (y._transaction.deletedStructs.has(a)) { y._transaction.deletedStructs.add(b) } } /** * Abstract class that represents any content. */ export default class Item { constructor () { /** * The uniqe identifier of this type. * @type {ID} */ this._id = null /** * The item that was originally to the left of this item. * @type {Item} */ this._origin = null /** * The item that is currently to the left of this item. * @type {Item} */ this._left = null /** * The item that is currently to the right of this item. * @type {Item} */ this._right = null /** * The item that was originally to the right of this item. * @type {Item} */ this._right_origin = null /** * The parent type. * @type {Y|YType} */ this._parent = null /** * If the parent refers to this item with some kind of key (e.g. YMap, the * key is specified here. The key is then used to refer to the list in which * to insert this item. If `parentSub = null` type._start is the list in * which to insert to. Otherwise it is `parent._start`. * @type {String} */ this._parentSub = null /** * Whether this item was deleted or not. * @type {Boolean} */ this._deleted = false /** * If this type's effect is reundone this type refers to the type that undid * this operation. * @type {Item} */ this._redone = null } /** * Creates an Item with the same effect as this Item (without position effect) * * @private */ _copy () { return new this.constructor() } /** * Redoes the effect of this operation. * * @param {Y} y The Yjs instance. * * @private */ _redo (y) { if (this._redone !== null) { return this._redone } let struct = this._copy() let left = this._left let right = this let parent = this._parent // make sure that parent is redone if (parent._deleted === true && parent._redone === null) { parent._redo(y) } if (parent._redone !== null) { parent = parent._redone // find next cloned items while (left !== null) { if (left._redone !== null && left._redone._parent === parent) { left = left._redone break } left = left._left } while (right !== null) { if (right._redone !== null && right._redone._parent === parent) { right = right._redone } right = right._right } } struct._origin = left struct._left = left struct._right = right struct._right_origin = right struct._parent = parent struct._parentSub = this._parentSub struct._integrate(y) this._redone = struct return struct } /** * Computes the last content address of this Item. * * @private */ get _lastId () { return new ID(this._id.user, this._id.clock + this._length - 1) } /** * Computes the length of this Item. * * @private */ get _length () { return 1 } /** * 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 * * @private */ get _countable () { return true } /** * Splits this Item so that another Items can be inserted in-between. * This must be overwritten if _length > 1 * Returns right part after split * * diff === 0 => this * * diff === length => this._right * * otherwise => split _content and return right part of split * (see {@link ItemJSON}/{@link ItemString} for implementation) * * @private */ _splitAt (y, diff) { if (diff === 0) { return this } return this._right } /** * Mark this Item as deleted. * * @param {Y} y The Yjs instance * @param {boolean} createDelete Whether to propagate a message that this * Type was deleted. * * @private */ _delete (y, createDelete = true) { if (!this._deleted) { this._deleted = true y.ds.mark(this._id, this._length, false) let del = new Delete() del._targetID = this._id del._length = this._length if (createDelete) { // broadcast and persists Delete del._integrate(y, true) } else if (y.persistence !== null) { // only persist Delete y.persistence.saveStruct(y, del) } transactionTypeChanged(y, this._parent, this._parentSub) y._transaction.deletedStructs.add(this) } } _gcChildren (y) {} _gc (y) { const gc = new GC() gc._id = this._id gc._length = this._length y.os.delete(this._id) gc._integrate(y) } /** * This is called right before this Item receives any children. * It can be overwritten to apply pending changes before applying remote changes * * @private */ _beforeChange () { // nop } /** * Integrates this Item into the shared structure. * * This method actually applies the change to the Yjs instance. In case of * Item it connects _left and _right to this Item and calls the * {@link Item#beforeChange} method. * * * Integrate the struct so that other types/structs can see it * * Add this struct to y.os * * Check if this is struct deleted * * @private */ _integrate (y) { y._transaction.newTypes.add(this) const parent = this._parent const selfID = this._id const user = selfID === null ? y.userID : selfID.user const userState = y.ss.getState(user) if (selfID === null) { this._id = y.ss.getNextID(this._length) } else if (selfID.user === RootFakeUserID) { // nop } else if (selfID.clock < userState) { // already applied.. return [] } else if (selfID.clock === userState) { y.ss.setState(selfID.user, userState + this._length) } else { // missing content from user throw new Error('Can not apply yet!') } if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) { // this is the first time parent is updated // or this types is new this._parent._beforeChange() } /* # $this has to find a unique position between origin and the next known character # case 1: $origin equals $o.origin: the $creator parameter decides if left or right # let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4 # o2,o3 and o4 origin is 1 (the position of o2) # there is the case that $this.creator < o2.creator, but o3.creator < $this.creator # then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex # therefore $this would be always to the right of o3 # case 2: $origin < $o.origin # if current $this insert_position > $o origin: $this ins # else $insert_position will not change # (maybe we encounter case 1 later, then this will be to the right of $o) # case 3: $origin > $o.origin # $this insert_position is to the left of $o (forever!) */ // handle conflicts let o // set o to the first conflicting item if (this._left !== null) { o = this._left._right } else if (this._parentSub !== null) { o = this._parent._map.get(this._parentSub) || null } else { o = this._parent._start } let conflictingItems = new Set() let 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 (this._origin === o._origin) { // case 1 if (o._id.user < this._id.user) { this._left = o conflictingItems.clear() } } else if (itemsBeforeOrigin.has(o._origin)) { // case 2 if (!conflictingItems.has(o._origin)) { this._left = o conflictingItems.clear() } } else { break } // TODO: try to use right_origin instead. // Then you could basically omit conflictingItems! // Note: you probably can't use right_origin in every case.. only when setting _left o = o._right } // reconnect left/right + update parent map/start if necessary const parentSub = this._parentSub if (this._left === null) { let right if (parentSub !== null) { const pmap = parent._map right = pmap.get(parentSub) || null pmap.set(parentSub, this) } else { right = parent._start parent._start = this } this._right = right if (right !== null) { right._left = this } } else { const left = this._left const right = left._right this._right = right left._right = this if (right !== null) { right._left = this } } if (parent._deleted) { this._delete(y, false) } y.os.put(this) transactionTypeChanged(y, parent, parentSub) if (this._id.user !== RootFakeUserID) { if (y.connector !== null && (y.connector._forwardAppliedStructs || this._id.user === y.userID)) { y.connector.broadcastStruct(this) } if (y.persistence !== null) { y.persistence.saveStruct(y, this) } writeStructToTransaction(y._transaction, this) } } /** * 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 {BinaryEncoder} encoder The encoder to write data to. * * @private */ _toBinary (encoder) { encoder.writeUint8(getStructReference(this.constructor)) let info = 0 if (this._origin !== null) { info += 0b1 // origin is defined } // TODO: remove /* no longer send _left if (this._left !== this._origin) { info += 0b10 // do not copy origin to left } */ if (this._right_origin !== null) { info += 0b100 } if (this._parentSub !== null) { info += 0b1000 } encoder.writeUint8(info) encoder.writeID(this._id) if (info & 0b1) { encoder.writeID(this._origin._lastId) } // TODO: remove /* see above if (info & 0b10) { encoder.writeID(this._left._lastId) } */ if (info & 0b100) { encoder.writeID(this._right_origin._id) } if ((info & 0b101) === 0) { // neither origin nor right is defined encoder.writeID(this._parent._id) } if (info & 0b1000) { encoder.writeVarString(JSON.stringify(this._parentSub)) } } /** * Read the next Item in a Decoder and fill this Item with the read data. * * This is called when data is received from a remote peer. * * @param {Y} y The Yjs instance that this Item belongs to. * @param {BinaryDecoder} decoder The decoder object to read data from. * * @private */ _fromBinary (y, decoder) { let missing = [] const info = decoder.readUint8() const id = decoder.readID() this._id = id // read origin if (info & 0b1) { // origin != null const originID = decoder.readID() // we have to query for left again because it might have been split/merged.. const origin = y.os.getItemCleanEnd(originID) if (origin === null) { missing.push(originID) } else { this._origin = origin this._left = this._origin } } // read right if (info & 0b100) { // right != null const rightID = decoder.readID() // we have to query for right again because it might have been split/merged.. const right = y.os.getItemCleanStart(rightID) if (right === null) { missing.push(rightID) } else { this._right = right this._right_origin = right } } // read parent if ((info & 0b101) === 0) { // neither origin nor right is defined const parentID = decoder.readID() // parent does not change, so we don't have to search for it again if (this._parent === null) { let parent if (parentID.constructor === RootID) { parent = y.os.get(parentID) } else { parent = y.os.getItem(parentID) } if (parent === null) { missing.push(parentID) } else { this._parent = parent } } } else if (this._parent === null) { if (this._origin !== null) { if (this._origin.constructor === GC) { // if origin is a gc, set parent also gc'd this._parent = this._origin } else { this._parent = this._origin._parent } } else if (this._right_origin !== null) { // if origin is a gc, set parent also gc'd if (this._right_origin.constructor === GC) { this._parent = this._right_origin } else { this._parent = this._right_origin._parent } } } if (info & 0b1000) { // TODO: maybe put this in read parent condition (you can also read parentsub from left/right) this._parentSub = JSON.parse(decoder.readVarString()) } if (y.ss.getState(id.user) < id.clock) { missing.push(new ID(id.user, id.clock - 1)) } return missing } }