Move content and list iteration abstraction
This commit is contained in:
parent
294ba351b6
commit
53a7b286b8
@ -38,6 +38,7 @@ export * from './structs/ContentFormat.js'
|
|||||||
export * from './structs/ContentJSON.js'
|
export * from './structs/ContentJSON.js'
|
||||||
export * from './structs/ContentAny.js'
|
export * from './structs/ContentAny.js'
|
||||||
export * from './structs/ContentString.js'
|
export * from './structs/ContentString.js'
|
||||||
|
export * from './structs/ContentMove.js'
|
||||||
export * from './structs/ContentType.js'
|
export * from './structs/ContentType.js'
|
||||||
export * from './structs/Item.js'
|
export * from './structs/Item.js'
|
||||||
export * from './structs/Skip.js'
|
export * from './structs/Skip.js'
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
createID,
|
createID,
|
||||||
readContentFormat,
|
readContentFormat,
|
||||||
readContentType,
|
readContentType,
|
||||||
|
readContentMove,
|
||||||
addChangedTypeToTransaction,
|
addChangedTypeToTransaction,
|
||||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
@ -281,11 +282,14 @@ export class Item extends AbstractStruct {
|
|||||||
*/
|
*/
|
||||||
this.parentSub = parentSub
|
this.parentSub = parentSub
|
||||||
/**
|
/**
|
||||||
* If this type's effect is reundone this type refers to the type that undid
|
* If this type is deleted: If this type's effect is reundone this type refers to the type-id that undid
|
||||||
* this operation.
|
* this operation.
|
||||||
* @type {ID | null}
|
*
|
||||||
|
* If this item is not deleted: This property is reused by the moved prop. In this case this property refers to an Item.
|
||||||
|
*
|
||||||
|
* @type {ID | Item | null}
|
||||||
*/
|
*/
|
||||||
this.redone = null
|
this._ref = null
|
||||||
/**
|
/**
|
||||||
* @type {AbstractContent}
|
* @type {AbstractContent}
|
||||||
*/
|
*/
|
||||||
@ -295,11 +299,57 @@ export class Item extends AbstractStruct {
|
|||||||
* bit2: countable
|
* bit2: countable
|
||||||
* bit3: deleted
|
* bit3: deleted
|
||||||
* bit4: mark - mark node as fast-search-marker
|
* bit4: mark - mark node as fast-search-marker
|
||||||
|
* bit5: moved - whether this item has been moved. The moved item is then referred to on the "redone" prop.
|
||||||
* @type {number} byte
|
* @type {number} byte
|
||||||
*/
|
*/
|
||||||
this.info = this.content.isCountable() ? binary.BIT2 : 0
|
this.info = this.content.isCountable() ? binary.BIT2 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this type's effect is reundone this type refers to the type-id that undid
|
||||||
|
* this operation.
|
||||||
|
*
|
||||||
|
* @return {ID | null}
|
||||||
|
*/
|
||||||
|
get redone () {
|
||||||
|
return /** @type {ID | null} */ (this._ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ID | null} id
|
||||||
|
*/
|
||||||
|
set redone (id) {
|
||||||
|
this._ref = id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this item has been moved, the moved property will referr to the item that moved this content.
|
||||||
|
*
|
||||||
|
* @param {Item | null} item
|
||||||
|
*/
|
||||||
|
set movedBy (item) {
|
||||||
|
this._ref = item
|
||||||
|
if (item != null) {
|
||||||
|
this.info |= binary.BIT5
|
||||||
|
} else if (this.moved) {
|
||||||
|
this.info ^= binary.BIT5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Item | null}
|
||||||
|
*/
|
||||||
|
get movedBy () {
|
||||||
|
return this.moved ? /** @type {Item} */ (this._ref) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get moved () {
|
||||||
|
return (this.info & binary.BIT5) > 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is used to mark the item as an indexed fast-search marker
|
* This is used to mark the item as an indexed fast-search marker
|
||||||
*
|
*
|
||||||
@ -371,7 +421,7 @@ export class Item extends AbstractStruct {
|
|||||||
// We have all missing ids, now find the items
|
// We have all missing ids, now find the items
|
||||||
|
|
||||||
if (this.origin) {
|
if (this.origin) {
|
||||||
this.left = getItemCleanEnd(transaction, store, this.origin)
|
this.left = getItemCleanEnd(transaction, this.origin)
|
||||||
this.origin = this.left.lastId
|
this.origin = this.left.lastId
|
||||||
}
|
}
|
||||||
if (this.rightOrigin) {
|
if (this.rightOrigin) {
|
||||||
@ -409,7 +459,7 @@ export class Item extends AbstractStruct {
|
|||||||
integrate (transaction, offset) {
|
integrate (transaction, offset) {
|
||||||
if (offset > 0) {
|
if (offset > 0) {
|
||||||
this.id.clock += offset
|
this.id.clock += offset
|
||||||
this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1))
|
this.left = getItemCleanEnd(transaction, createID(this.id.client, this.id.clock - 1))
|
||||||
this.origin = this.left.lastId
|
this.origin = this.left.lastId
|
||||||
this.content = this.content.splice(offset)
|
this.content = this.content.splice(offset)
|
||||||
this.length -= offset
|
this.length -= offset
|
||||||
@ -567,8 +617,8 @@ export class Item extends AbstractStruct {
|
|||||||
this.id.client === right.id.client &&
|
this.id.client === right.id.client &&
|
||||||
this.id.clock + this.length === right.id.clock &&
|
this.id.clock + this.length === right.id.clock &&
|
||||||
this.deleted === right.deleted &&
|
this.deleted === right.deleted &&
|
||||||
this.redone === null &&
|
this._ref === right._ref &&
|
||||||
right.redone === null &&
|
(!this.deleted || this.redone === null) &&
|
||||||
this.content.constructor === right.content.constructor &&
|
this.content.constructor === right.content.constructor &&
|
||||||
this.content.mergeWith(right.content)
|
this.content.mergeWith(right.content)
|
||||||
) {
|
) {
|
||||||
@ -613,7 +663,7 @@ export class Item extends AbstractStruct {
|
|||||||
this.markDeleted()
|
this.markDeleted()
|
||||||
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
|
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
|
||||||
addChangedTypeToTransaction(transaction, parent, this.parentSub)
|
addChangedTypeToTransaction(transaction, parent, this.parentSub)
|
||||||
this.content.delete(transaction)
|
this.content.delete(transaction, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -710,7 +760,8 @@ export const contentRefs = [
|
|||||||
readContentType, // 7
|
readContentType, // 7
|
||||||
readContentAny, // 8
|
readContentAny, // 8
|
||||||
readContentDoc, // 9
|
readContentDoc, // 9
|
||||||
() => { error.unexpectedCase() } // 10 - Skip is not ItemContent
|
() => { error.unexpectedCase() }, // 10 - Skip is not ItemContent
|
||||||
|
readContentMove // 11
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -777,8 +828,9 @@ export class AbstractContent {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
|
* @param {Item} item
|
||||||
*/
|
*/
|
||||||
delete (transaction) {
|
delete (transaction, item) {
|
||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
ContentAny,
|
ContentAny,
|
||||||
ContentBinary,
|
ContentBinary,
|
||||||
getItemCleanStart,
|
getItemCleanStart,
|
||||||
|
ContentMove,
|
||||||
|
getMovedCoords,
|
||||||
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@ -395,6 +397,131 @@ export class AbstractType {
|
|||||||
toJSON () {}
|
toJSON () {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ListPosition {
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {Transaction} tr
|
||||||
|
*/
|
||||||
|
constructor (type, tr) {
|
||||||
|
this.type = type
|
||||||
|
/**
|
||||||
|
* Current index-position
|
||||||
|
*/
|
||||||
|
this.index = 0
|
||||||
|
/**
|
||||||
|
* Relative position to the current item (if item.content.length > 1)
|
||||||
|
*/
|
||||||
|
this.rel = 0
|
||||||
|
/**
|
||||||
|
* This refers to the current right item, unless reachedEnd is true. Then it refers to the left item.
|
||||||
|
*/
|
||||||
|
this.item = type._start
|
||||||
|
this.reachedEnd = type._start === null
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
this.currMove = null
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
this.currMoveEnd = null
|
||||||
|
/**
|
||||||
|
* @type {Array<{ end: Item | null, move: Item }>}
|
||||||
|
*/
|
||||||
|
this.movedStack = []
|
||||||
|
this.tr = tr
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
forward (len) {
|
||||||
|
let item = this.item
|
||||||
|
this.index += len
|
||||||
|
if (this.rel) {
|
||||||
|
len += this.rel
|
||||||
|
this.rel = 0
|
||||||
|
}
|
||||||
|
while (item && !this.reachedEnd && (len > 0 || (len === 0 && (!item.countable || item.deleted)))) {
|
||||||
|
if (item.countable && !item.deleted && item.movedBy === this.currMove) {
|
||||||
|
len -= item.length
|
||||||
|
if (len <= 0) {
|
||||||
|
this.rel = -len
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (item.content.constructor === ContentMove) {
|
||||||
|
if (this.currMove) {
|
||||||
|
this.movedStack.push({ end: this.currMoveEnd, move: this.currMove })
|
||||||
|
}
|
||||||
|
const { start, end } = getMovedCoords(item.content, this.tr)
|
||||||
|
this.currMove = item
|
||||||
|
this.currMoveEnd = end
|
||||||
|
this.item = start
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (item === this.currMoveEnd) {
|
||||||
|
this.item = this.currMove // we iterate to the right after the current condition
|
||||||
|
const { end, move } = this.movedStack.pop() || { end: null, move: null }
|
||||||
|
this.currMove = move
|
||||||
|
this.currMoveEnd = end
|
||||||
|
}
|
||||||
|
if (item.right) {
|
||||||
|
item = item.right
|
||||||
|
} else {
|
||||||
|
this.reachedEnd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.index -= len
|
||||||
|
this.item = item
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
slice (len) {
|
||||||
|
const result = []
|
||||||
|
while (len > 0 && !this.reachedEnd) {
|
||||||
|
while (this.item && this.item.countable && !this.reachedEnd && len > 0) {
|
||||||
|
if (!this.item.deleted) {
|
||||||
|
const content = this.item.content.getContent()
|
||||||
|
const slicedContent = content.length <= len || this.rel > 0 ? content : content.slice(this.rel, len)
|
||||||
|
len -= slicedContent.length
|
||||||
|
result.push(...slicedContent)
|
||||||
|
if (content.length !== slicedContent.length) {
|
||||||
|
if (this.rel + slicedContent.length === content.length) {
|
||||||
|
this.rel = 0
|
||||||
|
} else {
|
||||||
|
this.rel += slicedContent.length
|
||||||
|
continue // do not iterate to item.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.item.right) {
|
||||||
|
this.item = this.item.right
|
||||||
|
} else {
|
||||||
|
this.reachedEnd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.item && !this.reachedEnd && len > 0) {
|
||||||
|
this.forward(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator] () {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
next () {
|
||||||
|
const [value] = this.slice(1)
|
||||||
|
return {
|
||||||
|
done: value == null,
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractType<any>} type
|
* @param {AbstractType<any>} type
|
||||||
* @param {number} start
|
* @param {number} start
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
YEvent,
|
YEvent,
|
||||||
AbstractType,
|
AbstractType,
|
||||||
typeListGet,
|
typeListGet,
|
||||||
typeListToArray,
|
|
||||||
typeListForEach,
|
typeListForEach,
|
||||||
typeListCreateIterator,
|
typeListCreateIterator,
|
||||||
typeListInsertGenerics,
|
typeListInsertGenerics,
|
||||||
@ -15,6 +14,7 @@ import {
|
|||||||
YArrayRefID,
|
YArrayRefID,
|
||||||
callTypeObservers,
|
callTypeObservers,
|
||||||
transact,
|
transact,
|
||||||
|
ListPosition,
|
||||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
import { typeListSlice } from './AbstractType.js'
|
import { typeListSlice } from './AbstractType.js'
|
||||||
@ -188,7 +188,9 @@ export class YArray extends AbstractType {
|
|||||||
* @return {Array<T>}
|
* @return {Array<T>}
|
||||||
*/
|
*/
|
||||||
toArray () {
|
toArray () {
|
||||||
return typeListToArray(this)
|
return transact(/** @type {Doc} */ (this.doc), tr =>
|
||||||
|
new ListPosition(this, tr).slice(this.length)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -199,19 +199,18 @@ export const getItemCleanStart = (transaction, id) => {
|
|||||||
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
*
|
*
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {StructStore} store
|
|
||||||
* @param {ID} id
|
* @param {ID} id
|
||||||
* @return {Item}
|
* @return {Item}
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const getItemCleanEnd = (transaction, store, id) => {
|
export const getItemCleanEnd = (transaction, id) => {
|
||||||
/**
|
/**
|
||||||
* @type {Array<Item>}
|
* @type {Array<Item>}
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const structs = store.clients.get(id.client)
|
const structs = transaction.doc.store.clients.get(id.client)
|
||||||
const index = findIndexSS(structs, id.clock)
|
const index = findIndexSS(structs, id.clock)
|
||||||
const struct = structs[index]
|
const struct = structs[index]
|
||||||
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
|
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
|
||||||
|
@ -377,9 +377,12 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
/**
|
/**
|
||||||
* Implements the functionality of `y.transact(()=>{..})`
|
* Implements the functionality of `y.transact(()=>{..})`
|
||||||
*
|
*
|
||||||
|
* @template T
|
||||||
|
*
|
||||||
* @param {Doc} doc
|
* @param {Doc} doc
|
||||||
* @param {function(Transaction):void} f
|
* @param {function(Transaction):T} f
|
||||||
* @param {any} [origin=true]
|
* @param {any} [origin=true]
|
||||||
|
* @return {T}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
@ -395,8 +398,9 @@ export const transact = (doc, f, origin = null, local = true) => {
|
|||||||
}
|
}
|
||||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||||
}
|
}
|
||||||
|
let res
|
||||||
try {
|
try {
|
||||||
f(doc._transaction)
|
res = f(doc._transaction)
|
||||||
} finally {
|
} finally {
|
||||||
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
||||||
// The first transaction ended, now process observer calls.
|
// The first transaction ended, now process observer calls.
|
||||||
@ -410,4 +414,5 @@ export const transact = (doc, f, origin = null, local = true) => {
|
|||||||
cleanupTransactions(transactionCleanups, 0)
|
cleanupTransactions(transactionCleanups, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
readContentFormat,
|
readContentFormat,
|
||||||
readContentAny,
|
readContentAny,
|
||||||
readContentDoc,
|
readContentDoc,
|
||||||
|
readContentMove,
|
||||||
Doc,
|
Doc,
|
||||||
PermanentUserData,
|
PermanentUserData,
|
||||||
encodeStateAsUpdate,
|
encodeStateAsUpdate,
|
||||||
@ -24,7 +25,8 @@ import * as Y from '../src/index.js'
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testStructReferences = tc => {
|
export const testStructReferences = tc => {
|
||||||
t.assert(contentRefs.length === 11)
|
t.assert(contentRefs.length === 12)
|
||||||
|
// contentRefs[0] is reserved for GC
|
||||||
t.assert(contentRefs[1] === readContentDeleted)
|
t.assert(contentRefs[1] === readContentDeleted)
|
||||||
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
|
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
|
||||||
t.assert(contentRefs[3] === readContentBinary)
|
t.assert(contentRefs[3] === readContentBinary)
|
||||||
@ -35,6 +37,7 @@ export const testStructReferences = tc => {
|
|||||||
t.assert(contentRefs[8] === readContentAny)
|
t.assert(contentRefs[8] === readContentAny)
|
||||||
t.assert(contentRefs[9] === readContentDoc)
|
t.assert(contentRefs[9] === readContentDoc)
|
||||||
// contentRefs[10] is reserved for Skip structs
|
// contentRefs[10] is reserved for Skip structs
|
||||||
|
t.assert(contentRefs[11] === readContentMove)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user