2019-04-04 19:35:38 +02:00
import {
GC ,
2020-06-02 23:20:45 +02:00
getState ,
2019-04-04 19:35:38 +02:00
AbstractStruct ,
replaceStruct ,
addStruct ,
addToDeleteSet ,
2019-04-05 00:37:09 +02:00
findRootTypeKey ,
2019-04-06 15:55:20 +02:00
compareIDs ,
getItem ,
2019-04-07 23:08:08 +02:00
getItemCleanEnd ,
getItemCleanStart ,
2019-05-28 14:18:20 +02:00
readContentDeleted ,
readContentBinary ,
readContentJSON ,
2019-08-09 01:15:33 +02:00
readContentAny ,
2019-05-28 14:18:20 +02:00
readContentString ,
readContentEmbed ,
2020-09-10 01:54:16 +02:00
readContentDoc ,
2020-06-02 23:20:45 +02:00
createID ,
2019-05-28 14:18:20 +02:00
readContentFormat ,
readContentType ,
2019-06-18 17:41:19 +02:00
addChangedTypeToTransaction ,
2022-02-03 21:10:24 +01:00
isDeleted ,
2023-04-03 14:01:38 +02:00
StackItem , DeleteSet , UpdateDecoderV1 , UpdateDecoderV2 , UpdateEncoderV1 , UpdateEncoderV2 , ContentType , ContentDeleted , StructStore , ID , AbstractType , Transaction // eslint-disable-line
2019-04-04 19:35:38 +02:00
} from '../internals.js'
2021-05-14 18:53:24 +02:00
import * as error from 'lib0/error'
import * as binary from 'lib0/binary'
2023-04-03 14:01:38 +02:00
import * as array from 'lib0/array'
2019-03-26 01:14:15 +01:00
2019-06-24 23:04:53 +02:00
/ * *
2019-10-08 18:31:56 +02:00
* @ todo This should return several items
*
2019-06-24 23:04:53 +02:00
* @ 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 = getItem ( store , nextID )
diff = nextID . clock - item . id . clock
nextID = item . redone
2019-12-10 17:18:57 +01:00
} while ( nextID !== null && item instanceof Item )
2019-06-24 23:04:53 +02:00
return {
item , diff
}
}
2019-06-23 13:04:14 +02:00
/ * *
* Make sure that neither item nor any of its parents is ever deleted .
*
* This property does not persist when storing it into a database or when
* sending it to other peers
*
* @ param { Item | null } item
2020-01-27 03:42:32 +01:00
* @ param { boolean } keep
2019-06-23 13:04:14 +02:00
* /
2020-01-27 03:42:32 +01:00
export const keepItem = ( item , keep ) => {
while ( item !== null && item . keep !== keep ) {
item . keep = keep
2020-06-09 00:53:05 +02:00
item = /** @type {AbstractType<any>} */ ( item . parent ) . _item
2019-06-23 13:04:14 +02:00
}
}
2019-03-26 01:14:15 +01:00
/ * *
* Split leftItem into two items
2019-04-11 00:23:08 +02:00
* @ param { Transaction } transaction
2019-05-28 14:18:20 +02:00
* @ param { Item } leftItem
2019-03-26 01:14:15 +01:00
* @ param { number } diff
2019-05-28 14:18:20 +02:00
* @ return { Item }
2019-04-11 23:34:56 +02:00
*
2019-04-11 13:18:35 +02:00
* @ function
* @ private
2019-03-26 01:14:15 +01:00
* /
2019-04-11 00:23:08 +02:00
export const splitItem = ( transaction , leftItem , diff ) => {
2019-03-26 01:14:15 +01:00
// create rightItem
2020-06-02 23:20:45 +02:00
const { client , clock } = leftItem . id
2019-05-28 14:18:20 +02:00
const rightItem = new Item (
2020-06-02 23:20:45 +02:00
createID ( client , clock + diff ) ,
2019-04-05 19:46:18 +02:00
leftItem ,
2020-06-02 23:20:45 +02:00
createID ( client , clock + diff - 1 ) ,
2019-04-05 19:46:18 +02:00
leftItem . right ,
leftItem . rightOrigin ,
leftItem . parent ,
2019-05-28 14:18:20 +02:00
leftItem . parentSub ,
leftItem . content . splice ( diff )
2019-04-05 19:46:18 +02:00
)
2019-03-26 01:14:15 +01:00
if ( leftItem . deleted ) {
2020-06-09 16:34:07 +02:00
rightItem . markDeleted ( )
2019-03-26 01:14:15 +01:00
}
2019-06-23 13:04:14 +02:00
if ( leftItem . keep ) {
rightItem . keep = true
}
2019-06-24 23:04:53 +02:00
if ( leftItem . redone !== null ) {
rightItem . redone = createID ( leftItem . redone . client , leftItem . redone . clock + diff )
}
2019-03-26 01:14:15 +01:00
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
leftItem . right = rightItem
// update right
if ( rightItem . right !== null ) {
rightItem . right . left = rightItem
}
2019-04-11 00:23:08 +02:00
// right is more specific.
2020-06-02 23:20:45 +02:00
transaction . _mergeStructs . push ( rightItem )
2019-04-26 19:45:37 +02:00
// update parent._map
if ( rightItem . parentSub !== null && rightItem . right === null ) {
2020-06-09 00:53:05 +02:00
/** @type {AbstractType<any>} */ ( rightItem . parent ) . _map . set ( rightItem . parentSub , rightItem )
2019-04-26 19:45:37 +02:00
}
2019-05-28 14:18:20 +02:00
leftItem . length = diff
2019-04-05 19:46:18 +02:00
return rightItem
2019-03-26 01:14:15 +01:00
}
2023-04-03 14:01:38 +02:00
/ * *
* @ param { Array < StackItem > } stack
* @ param { ID } id
* /
const isDeletedByUndoStack = ( stack , id ) => array . some ( stack , /** @param {StackItem} s */ s => isDeleted ( s . deletions , id ) )
2019-06-23 13:04:14 +02:00
/ * *
* Redoes the effect of this operation .
*
* @ param { Transaction } transaction The Yjs instance .
* @ param { Item } item
* @ param { Set < Item > } redoitems
2022-02-03 21:10:24 +01:00
* @ param { DeleteSet } itemsToDelete
2022-03-26 10:29:19 +01:00
* @ param { boolean } ignoreRemoteMapChanges
2023-04-03 14:01:38 +02:00
* @ param { import ( '../utils/UndoManager.js' ) . UndoManager } um
2019-06-23 13:04:14 +02:00
*
* @ return { Item | null }
*
* @ private
* /
2023-04-03 14:01:38 +02:00
export const redoItem = ( transaction , item , redoitems , itemsToDelete , ignoreRemoteMapChanges , um ) => {
2020-06-02 23:20:45 +02:00
const doc = transaction . doc
const store = doc . store
const ownClientID = doc . clientID
const redone = item . redone
if ( redone !== null ) {
return getItemCleanStart ( transaction , redone )
2019-06-23 13:04:14 +02:00
}
2020-06-09 00:53:05 +02:00
let parentItem = /** @type {AbstractType<any>} */ ( item . parent ) . _item
2019-06-23 13:04:14 +02:00
/ * *
* @ type { Item | null }
* /
2022-02-03 21:10:24 +01:00
let left = null
2019-06-23 13:04:14 +02:00
/ * *
* @ type { Item | null }
* /
let right
// make sure that parent is redone
2022-02-03 21:10:24 +01:00
if ( parentItem !== null && parentItem . deleted === true ) {
2019-06-23 13:04:14 +02:00
// try to undo parent if it will be undone anyway
2023-04-03 14:01:38 +02:00
if ( parentItem . redone === null && ( ! redoitems . has ( parentItem ) || redoItem ( transaction , parentItem , redoitems , itemsToDelete , ignoreRemoteMapChanges , um ) === null ) ) {
2019-06-23 13:04:14 +02:00
return null
}
while ( parentItem . redone !== null ) {
2019-09-17 18:53:59 +02:00
parentItem = getItemCleanStart ( transaction , parentItem . redone )
2019-06-23 13:04:14 +02:00
}
2022-02-03 21:10:24 +01:00
}
const parentType = parentItem === null ? /** @type {AbstractType<any>} */ ( item . parent ) : /** @type {ContentType} */ ( parentItem . content ) . type
if ( item . parentSub === null ) {
// Is an array item. Insert at the old position
left = item . left
right = item
2019-06-23 13:04:14 +02:00
// find next cloned_redo items
while ( left !== null ) {
/ * *
* @ type { Item | null }
* /
let leftTrace = left
// trace redone until parent matches
2020-06-09 00:53:05 +02:00
while ( leftTrace !== null && /** @type {AbstractType<any>} */ ( leftTrace . parent ) . _item !== parentItem ) {
2019-09-17 18:53:59 +02:00
leftTrace = leftTrace . redone === null ? null : getItemCleanStart ( transaction , leftTrace . redone )
2019-06-23 13:04:14 +02:00
}
2020-06-09 00:53:05 +02:00
if ( leftTrace !== null && /** @type {AbstractType<any>} */ ( leftTrace . parent ) . _item === parentItem ) {
2019-06-23 13:04:14 +02:00
left = leftTrace
break
}
left = left . left
}
while ( right !== null ) {
/ * *
* @ type { Item | null }
* /
let rightTrace = right
// trace redone until parent matches
2020-06-09 00:53:05 +02:00
while ( rightTrace !== null && /** @type {AbstractType<any>} */ ( rightTrace . parent ) . _item !== parentItem ) {
2019-09-17 18:53:59 +02:00
rightTrace = rightTrace . redone === null ? null : getItemCleanStart ( transaction , rightTrace . redone )
2019-06-23 13:04:14 +02:00
}
2020-06-09 00:53:05 +02:00
if ( rightTrace !== null && /** @type {AbstractType<any>} */ ( rightTrace . parent ) . _item === parentItem ) {
2019-06-23 13:04:14 +02:00
right = rightTrace
break
}
right = right . right
}
2022-02-03 21:10:24 +01:00
} else {
right = null
2022-03-26 10:29:19 +01:00
if ( item . right && ! ignoreRemoteMapChanges ) {
2023-04-03 14:01:38 +02:00
left = item
2022-02-03 21:10:24 +01:00
// Iterate right while right is in itemsToDelete
// If it is intended to delete right while item is redone, we can expect that item should replace right.
2023-04-03 14:01:38 +02:00
while ( left !== null && left . right !== null && ( left . right . redone || isDeleted ( itemsToDelete , left . right . id ) || isDeletedByUndoStack ( um . undoStack , left . right . id ) || isDeletedByUndoStack ( um . redoStack , left . right . id ) ) ) {
2022-02-03 21:10:24 +01:00
left = left . right
2023-04-03 14:01:38 +02:00
// follow redone
while ( left . redone ) left = getItemCleanStart ( transaction , left . redone )
2022-02-03 21:10:24 +01:00
}
if ( left && left . right !== null ) {
// It is not possible to redo this item because it conflicts with a
// change from another client
return null
}
} else {
left = parentType . _map . get ( item . parentSub ) || null
2021-10-14 14:59:26 +02:00
}
2019-06-23 13:04:14 +02:00
}
2020-06-02 23:20:45 +02:00
const nextClock = getState ( store , ownClientID )
const nextId = createID ( ownClientID , nextClock )
2019-06-23 13:04:14 +02:00
const redoneItem = new Item (
2020-06-02 23:20:45 +02:00
nextId ,
left , left && left . lastId ,
right , right && right . id ,
2022-02-03 21:10:24 +01:00
parentType ,
2019-06-23 13:04:14 +02:00
item . parentSub ,
item . content . copy ( )
)
2020-06-02 23:20:45 +02:00
item . redone = nextId
2020-01-27 03:42:32 +01:00
keepItem ( redoneItem , true )
2020-06-09 00:53:05 +02:00
redoneItem . integrate ( transaction , 0 )
2019-06-23 13:04:14 +02:00
return redoneItem
}
2019-03-26 01:14:15 +01:00
/ * *
* Abstract class that represents any content .
* /
2019-05-28 14:18:20 +02:00
export class Item extends AbstractStruct {
2019-03-26 01:14:15 +01:00
/ * *
* @ param { ID } id
2019-05-28 14:18:20 +02:00
* @ param { Item | null } left
2019-04-05 19:46:18 +02:00
* @ param { ID | null } origin
2019-05-28 14:18:20 +02:00
* @ param { Item | null } right
2019-04-05 19:46:18 +02:00
* @ param { ID | null } rightOrigin
2020-06-09 00:53:05 +02:00
* @ param { AbstractType < any > | ID | null } parent Is a type if integrated , is null if it is possible to copy parent from left or right , is ID before integration to search for it .
2019-03-26 01:14:15 +01:00
* @ param { string | null } parentSub
2019-05-28 14:18:20 +02:00
* @ param { AbstractContent } content
2019-03-26 01:14:15 +01:00
* /
2019-05-28 14:18:20 +02:00
constructor ( id , left , origin , right , rightOrigin , parent , parentSub , content ) {
super ( id , content . getLength ( ) )
2019-03-26 01:14:15 +01:00
/ * *
* The item that was originally to the left of this item .
2019-04-05 19:46:18 +02:00
* @ type { ID | null }
2019-03-26 01:14:15 +01:00
* /
2019-04-05 19:46:18 +02:00
this . origin = origin
2019-03-26 01:14:15 +01:00
/ * *
* The item that is currently to the left of this item .
2019-05-28 14:18:20 +02:00
* @ type { Item | null }
2019-03-26 01:14:15 +01:00
* /
this . left = left
/ * *
* The item that is currently to the right of this item .
2019-05-28 14:18:20 +02:00
* @ type { Item | null }
2019-03-26 01:14:15 +01:00
* /
this . right = right
/ * *
* The item that was originally to the right of this item .
2019-04-05 19:46:18 +02:00
* @ type { ID | null }
2019-03-26 01:14:15 +01:00
* /
2019-04-05 19:46:18 +02:00
this . rightOrigin = rightOrigin
2019-03-26 01:14:15 +01:00
/ * *
2020-06-09 00:53:05 +02:00
* @ type { AbstractType < any > | ID | null }
2019-03-26 01:14:15 +01:00
* /
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
/ * *
2022-01-05 01:20:57 +08:00
* If this type ' s effect is redone this type refers to the type that undid
2019-03-26 01:14:15 +01:00
* this operation .
2019-06-24 23:04:53 +02:00
* @ type { ID | null }
2019-03-26 01:14:15 +01:00
* /
this . redone = null
2019-06-05 14:53:00 +02:00
/ * *
* @ type { AbstractContent }
* /
2019-05-28 14:18:20 +02:00
this . content = content
2020-07-15 22:03:02 +02:00
/ * *
* bit1 : keep
* bit2 : countable
* bit3 : deleted
* bit4 : mark - mark node as fast - search - marker
* @ type { number } byte
* /
2020-06-09 16:34:07 +02:00
this . info = this . content . isCountable ( ) ? binary . BIT2 : 0
}
2020-07-15 22:03:02 +02:00
/ * *
* 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
}
2020-06-09 16:34:07 +02:00
/ * *
* 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
}
2019-03-26 01:14:15 +01:00
}
2020-06-02 23:20:45 +02:00
get countable ( ) {
2020-06-09 16:34:07 +02:00
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
2020-06-02 23:20:45 +02:00
}
2019-03-26 01:14:15 +01:00
/ * *
2020-06-09 23:48:27 +02:00
* Return the creator clientID of the missing op or define missing items and return null .
2020-06-09 00:53:05 +02:00
*
2019-03-26 01:14:15 +01:00
* @ param { Transaction } transaction
2020-06-09 00:53:05 +02:00
* @ param { StructStore } store
2020-06-09 23:48:27 +02:00
* @ return { null | number }
2019-03-26 01:14:15 +01:00
* /
2020-06-09 00:53:05 +02:00
getMissing ( transaction , store ) {
2020-06-09 23:48:27 +02:00
if ( this . origin && this . origin . client !== this . id . client && this . origin . clock >= getState ( store , this . origin . client ) ) {
return this . origin . client
2019-03-26 01:14:15 +01:00
}
2020-06-09 23:48:27 +02:00
if ( this . rightOrigin && this . rightOrigin . client !== this . id . client && this . rightOrigin . clock >= getState ( store , this . rightOrigin . client ) ) {
return this . rightOrigin . client
2020-06-09 00:53:05 +02:00
}
2020-06-09 23:48:27 +02:00
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
2020-06-09 00:53:05 +02:00
}
// We have all missing ids, now find the items
2020-06-09 23:48:27 +02:00
if ( this . origin ) {
this . left = getItemCleanEnd ( transaction , store , this . origin )
2020-06-09 00:53:05 +02:00
this . origin = this . left . lastId
}
2020-06-09 23:48:27 +02:00
if ( this . rightOrigin ) {
this . right = getItemCleanStart ( transaction , this . rightOrigin )
2020-06-09 00:53:05 +02:00
this . rightOrigin = this . right . id
}
2020-06-18 00:31:25 +02:00
if ( ( this . left && this . left . constructor === GC ) || ( this . right && this . right . constructor === GC ) ) {
this . parent = null
2023-09-18 09:55:50 +02:00
} else if ( ! this . parent ) {
// only set parent if this shouldn't be garbage collected
2020-06-09 00:53:05 +02:00
if ( this . left && this . left . constructor === Item ) {
this . parent = this . left . parent
this . parentSub = this . left . parentSub
2025-02-24 20:30:48 +01:00
} else if ( this . right && this . right . constructor === Item ) {
2020-06-09 00:53:05 +02:00
this . parent = this . right . parent
this . parentSub = this . right . parentSub
}
2020-06-09 23:48:27 +02:00
} else if ( this . parent . constructor === ID ) {
const parentItem = getItem ( store , this . parent )
if ( parentItem . constructor === GC ) {
this . parent = null
} else {
this . parent = /** @type {ContentType} */ ( parentItem . content ) . type
}
2020-06-09 00:53:05 +02:00
}
return null
}
/ * *
* @ param { Transaction } transaction
* @ param { number } offset
* /
integrate ( transaction , offset ) {
if ( offset > 0 ) {
this . id . clock += offset
2020-06-09 16:34:07 +02:00
this . left = getItemCleanEnd ( transaction , transaction . doc . store , createID ( this . id . client , this . id . clock - 1 ) )
2020-06-09 00:53:05 +02:00
this . origin = this . left . lastId
this . content = this . content . splice ( offset )
this . length -= offset
}
2020-06-09 16:34:07 +02:00
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
2020-06-09 00:53:05 +02:00
2020-06-09 16:34:07 +02:00
/ * *
* @ 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 {AbstractType<any>} */ ( this . parent ) . _map . get ( this . parentSub ) || null
while ( o !== null && o . left !== null ) {
o = o . left
2020-06-09 00:53:05 +02:00
}
} else {
2020-06-09 16:34:07 +02:00
o = /** @type {AbstractType<any>} */ ( this . parent ) . _start
2020-06-09 00:53:05 +02:00
}
2020-06-09 16:34:07 +02:00
// 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 ( )
2020-06-18 00:31:25 +02:00
} 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 ( getItem ( transaction . doc . store , o . origin ) ) ) { // use getItem instead of getItemCleanEnd because we don't want / need to split items.
2020-06-09 16:34:07 +02:00
// case 2
2020-06-18 00:31:25 +02:00
if ( ! conflictingItems . has ( getItem ( transaction . doc . store , o . origin ) ) ) {
2020-06-09 16:34:07 +02:00
left = o
conflictingItems . clear ( )
}
} else {
break
}
o = o . right
}
this . left = left
2020-06-09 00:53:05 +02:00
}
// reconnect left/right + update parent map/start if necessary
2020-06-09 16:34:07 +02:00
if ( this . left !== null ) {
const right = this . left . right
2020-06-09 00:53:05 +02:00
this . right = right
2020-06-09 16:34:07 +02:00
this . left . right = this
2020-06-09 00:53:05 +02:00
} else {
let r
2020-06-09 16:34:07 +02:00
if ( this . parentSub !== null ) {
r = /** @type {AbstractType<any>} */ ( this . parent ) . _map . get ( this . parentSub ) || null
2020-06-09 00:53:05 +02:00
while ( r !== null && r . left !== null ) {
r = r . left
}
} else {
2020-06-09 16:34:07 +02:00
r = /** @type {AbstractType<any>} */ ( this . parent ) . _start
; /** @type {AbstractType<any>} */ ( this . parent ) . _start = this
2020-06-09 00:53:05 +02:00
}
this . right = r
2019-03-26 01:14:15 +01:00
}
2020-06-09 00:53:05 +02:00
if ( this . right !== null ) {
this . right . left = this
2020-06-09 16:34:07 +02:00
} else if ( this . parentSub !== null ) {
2020-06-09 00:53:05 +02:00
// set as current parent value if right === null and this is parentSub
2020-06-09 16:34:07 +02:00
/** @type {AbstractType<any>} */ ( this . parent ) . _map . set ( this . parentSub , this )
if ( this . left !== null ) {
2020-06-09 00:53:05 +02:00
// this is the current attribute value of parent. delete right
2020-06-09 16:34:07 +02:00
this . left . delete ( transaction )
2020-06-09 00:53:05 +02:00
}
}
// adjust length of parent
2020-06-09 16:34:07 +02:00
if ( this . parentSub === null && this . countable && ! this . deleted ) {
/** @type {AbstractType<any>} */ ( this . parent ) . _length += this . length
2020-06-09 00:53:05 +02:00
}
2020-06-09 16:34:07 +02:00
addStruct ( transaction . doc . store , this )
2020-06-09 00:53:05 +02:00
this . content . integrate ( transaction , this )
// add parent to transaction.changed
2020-06-09 16:34:07 +02:00
addChangedTypeToTransaction ( transaction , /** @type {AbstractType<any>} */ ( this . parent ) , this . parentSub )
2020-07-13 17:38:39 +02:00
if ( ( /** @type {AbstractType<any>} */ ( this . parent ) . _item !== null && /** @type {AbstractType<any>} */ ( this . parent ) . _item . deleted ) || ( this . parentSub !== null && this . right !== null ) ) {
2020-06-09 00:53:05 +02:00
// 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 )
2019-03-26 01:14:15 +01:00
}
}
/ * *
* Returns the next non - deleted item
* /
get next ( ) {
let n = this . right
2019-03-29 01:02:44 +01:00
while ( n !== null && n . deleted ) {
2019-03-26 01:14:15 +01:00
n = n . right
}
return n
}
/ * *
* Returns the previous non - deleted item
* /
get prev ( ) {
let n = this . left
2019-03-29 01:02:44 +01:00
while ( n !== null && n . deleted ) {
2019-03-26 01:14:15 +01:00
n = n . left
}
return n
}
/ * *
* Computes the last content address of this Item .
* /
get lastId ( ) {
2020-06-05 00:27:36 +02:00
// 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 )
2019-03-26 01:14:15 +01:00
}
2020-01-22 16:42:16 +01:00
2019-03-26 01:14:15 +01:00
/ * *
2019-05-28 14:18:20 +02:00
* Try to merge two items
2019-03-26 01:14:15 +01:00
*
2019-05-28 14:18:20 +02:00
* @ param { Item } right
* @ return { boolean }
2019-03-26 01:14:15 +01:00
* /
2019-05-28 14:18:20 +02:00
mergeWith ( right ) {
if (
2020-12-15 15:39:08 +01:00
this . constructor === right . constructor &&
2019-05-28 14:18:20 +02:00
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 &&
2019-06-23 13:04:14 +02:00
this . redone === null &&
right . redone === null &&
2019-05-28 14:18:20 +02:00
this . content . constructor === right . content . constructor &&
this . content . mergeWith ( right . content )
) {
2021-05-25 21:17:01 +02:00
const searchMarker = /** @type {AbstractType<any>} */ ( 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
2021-05-25 21:23:12 +02:00
if ( ! this . deleted && this . countable ) {
2021-05-25 21:17:01 +02:00
marker . index -= this . length
}
}
} )
}
2019-06-23 13:04:14 +02:00
if ( right . keep ) {
this . keep = true
}
2019-05-28 14:18:20 +02:00
this . right = right . right
if ( this . right !== null ) {
this . right . left = this
}
this . length += right . length
return true
}
return false
2019-03-26 01:14:15 +01:00
}
/ * *
* Mark this Item as deleted .
*
* @ param { Transaction } transaction
* /
2019-03-30 01:08:09 +01:00
delete ( transaction ) {
2019-03-26 01:14:15 +01:00
if ( ! this . deleted ) {
2020-06-09 00:53:05 +02:00
const parent = /** @type {AbstractType<any>} */ ( this . parent )
2019-03-26 01:14:15 +01:00
// adjust the length of parent
if ( this . countable && this . parentSub === null ) {
2019-03-29 01:02:44 +01:00
parent . _length -= this . length
2019-03-26 01:14:15 +01:00
}
2020-06-09 16:34:07 +02:00
this . markDeleted ( )
2020-07-12 18:25:45 +02:00
addToDeleteSet ( transaction . deleteSet , this . id . client , this . id . clock , this . length )
2020-11-04 00:35:08 +01:00
addChangedTypeToTransaction ( transaction , parent , this . parentSub )
2019-05-28 14:18:20 +02:00
this . content . delete ( transaction )
2019-03-26 01:14:15 +01:00
}
}
/ * *
2019-04-09 00:31:17 +02:00
* @ param { StructStore } store
2019-04-26 18:37:38 +02:00
* @ param { boolean } parentGCd
2019-03-26 01:14:15 +01:00
* /
2019-05-07 13:44:23 +02:00
gc ( store , parentGCd ) {
2019-04-28 17:20:35 +02:00
if ( ! this . deleted ) {
throw error . unexpectedCase ( )
}
2019-05-28 14:18:20 +02:00
this . content . gc ( store )
2019-04-26 18:37:38 +02:00
if ( parentGCd ) {
2019-05-28 14:18:20 +02:00
replaceStruct ( store , this , new GC ( this . id , this . length ) )
2019-04-05 19:46:18 +02:00
} else {
2019-05-28 14:18:20 +02:00
this . content = new ContentDeleted ( this . length )
2019-04-05 19:46:18 +02:00
}
2019-03-26 01:14:15 +01:00
}
/ * *
* 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 .
*
2020-12-29 16:59:27 +01:00
* @ param { UpdateEncoderV1 | UpdateEncoderV2 } encoder The encoder to write data to .
2019-04-02 23:08:58 +02:00
* @ param { number } offset
2019-03-26 01:14:15 +01:00
* /
2019-05-28 14:18:20 +02:00
write ( encoder , offset ) {
2019-04-08 13:41:28 +02:00
const origin = offset > 0 ? createID ( this . id . client , this . id . clock + offset - 1 ) : this . origin
const rightOrigin = this . rightOrigin
const parentSub = this . parentSub
2019-05-28 14:18:20 +02:00
const info = ( this . content . getRef ( ) & binary . BITS5 ) |
2019-04-08 13:41:28 +02:00
( 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
2020-07-12 18:25:45 +02:00
encoder . writeInfo ( info )
2019-04-08 13:41:28 +02:00
if ( origin !== null ) {
2020-07-12 18:25:45 +02:00
encoder . writeLeftID ( origin )
2019-03-26 01:14:15 +01:00
}
2019-04-08 13:41:28 +02:00
if ( rightOrigin !== null ) {
2020-07-12 18:25:45 +02:00
encoder . writeRightID ( rightOrigin )
2019-03-26 01:14:15 +01:00
}
2019-04-08 13:41:28 +02:00
if ( origin === null && rightOrigin === null ) {
2020-06-09 00:53:05 +02:00
const parent = /** @type {AbstractType<any>} */ ( this . parent )
2020-12-17 21:50:39 +01:00
if ( parent . _item !== undefined ) {
2020-12-13 16:24:43 +01:00
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 )
}
2020-12-17 21:50:39 +01:00
} 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 ( )
2019-03-26 01:14:15 +01:00
}
2019-04-08 13:41:28 +02:00
if ( parentSub !== null ) {
2020-07-12 18:25:45 +02:00
encoder . writeString ( parentSub )
2019-03-26 01:14:15 +01:00
}
}
2019-05-28 14:18:20 +02:00
this . content . write ( encoder , offset )
}
}
/ * *
2020-12-29 16:59:27 +01:00
* @ param { UpdateDecoderV1 | UpdateDecoderV2 } decoder
2019-05-28 14:18:20 +02:00
* @ param { number } info
* /
2020-07-12 18:25:45 +02:00
export const readItemContent = ( decoder , info ) => contentRefs [ info & binary . BITS5 ] ( decoder )
2019-05-28 14:18:20 +02:00
/ * *
* A lookup map for reading Item content .
*
2020-12-29 16:59:27 +01:00
* @ type { Array < function ( UpdateDecoderV1 | UpdateDecoderV2 ) : AbstractContent > }
2019-05-28 14:18:20 +02:00
* /
export const contentRefs = [
2020-12-15 15:39:08 +01:00
( ) => { error . unexpectedCase ( ) } , // GC is not ItemContent
2020-09-10 01:54:16 +02:00
readContentDeleted , // 1
readContentJSON , // 2
readContentBinary , // 3
readContentString , // 4
readContentEmbed , // 5
readContentFormat , // 6
readContentType , // 7
readContentAny , // 8
2020-12-15 15:39:08 +01:00
readContentDoc , // 9
( ) => { error . unexpectedCase ( ) } // 10 - Skip is not ItemContent
2019-05-28 14:18:20 +02:00
]
/ * *
* Do not implement this class !
* /
export class AbstractContent {
/ * *
* @ return { number }
* /
getLength ( ) {
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
* @ return { Array < any > }
* /
getContent ( ) {
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
* 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 ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
* @ return { AbstractContent }
* /
copy ( ) {
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
2023-04-03 14:10:26 +02:00
* @ param { number } _offset
2019-05-28 14:18:20 +02:00
* @ return { AbstractContent }
* /
2023-04-03 14:10:26 +02:00
splice ( _offset ) {
2019-05-28 14:18:20 +02:00
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
2023-04-03 14:10:26 +02:00
* @ param { AbstractContent } _right
2019-05-28 14:18:20 +02:00
* @ return { boolean }
* /
2023-04-03 14:10:26 +02:00
mergeWith ( _right ) {
2019-05-28 14:18:20 +02:00
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
2023-04-03 14:10:26 +02:00
* @ param { Transaction } _transaction
* @ param { Item } _item
2019-05-28 14:18:20 +02:00
* /
2023-04-03 14:10:26 +02:00
integrate ( _transaction , _item ) {
2019-05-28 14:18:20 +02:00
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
2023-04-03 14:10:26 +02:00
* @ param { Transaction } _transaction
2019-05-28 14:18:20 +02:00
* /
2023-04-03 14:10:26 +02:00
delete ( _transaction ) {
2019-05-28 14:18:20 +02:00
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
2023-04-03 14:10:26 +02:00
* @ param { StructStore } _store
2019-05-28 14:18:20 +02:00
* /
2023-04-03 14:10:26 +02:00
gc ( _store ) {
2019-05-28 14:18:20 +02:00
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
2023-04-03 14:10:26 +02:00
* @ param { UpdateEncoderV1 | UpdateEncoderV2 } _encoder
* @ param { number } _offset
2019-05-28 14:18:20 +02:00
* /
2023-04-03 14:10:26 +02:00
write ( _encoder , _offset ) {
2019-05-28 14:18:20 +02:00
throw error . methodUnimplemented ( )
}
2020-01-22 16:42:16 +01:00
2019-05-28 14:18:20 +02:00
/ * *
* @ return { number }
* /
getRef ( ) {
throw error . methodUnimplemented ( )
2019-03-26 01:14:15 +01:00
}
}