383 lines
9.7 KiB
JavaScript
383 lines
9.7 KiB
JavaScript
/**
|
|
* @module types
|
|
*/
|
|
|
|
import { Type } from '../structs/Type.mjs'
|
|
import { ItemJSON } from '../structs/ItemJSON.mjs'
|
|
import { ItemString } from '../structs/ItemString.mjs'
|
|
import { stringifyItemID, logItemHelper } from '../protocols/syncProtocol.mjs'
|
|
import { YEvent } from '../utils/YEvent.mjs'
|
|
import { Transaction } from '../utils/Transaction.mjs' // eslint-disable-line
|
|
import { Item } from '../structs/Item.mjs' // eslint-disable-line
|
|
|
|
/**
|
|
* Event that describes the changes on a YArray
|
|
*/
|
|
export class YArrayEvent extends YEvent {
|
|
/**
|
|
* @param {YArray} yarray The changed type
|
|
* @param {Boolean} remote Whether the changed was caused by a remote peer
|
|
* @param {Transaction} transaction The transaction object
|
|
*/
|
|
constructor (yarray, remote, transaction) {
|
|
super(yarray)
|
|
this.remote = remote
|
|
this._transaction = transaction
|
|
this._addedElements = null
|
|
this._removedElements = null
|
|
}
|
|
|
|
/**
|
|
* Child elements that were added in this transaction.
|
|
*
|
|
* @return {Set}
|
|
*/
|
|
get addedElements () {
|
|
if (this._addedElements === null) {
|
|
const target = this.target
|
|
const transaction = this._transaction
|
|
const addedElements = new Set()
|
|
transaction.newTypes.forEach(type => {
|
|
if (type._parent === target && !transaction.deletedStructs.has(type)) {
|
|
addedElements.add(type)
|
|
}
|
|
})
|
|
this._addedElements = addedElements
|
|
}
|
|
return this._addedElements
|
|
}
|
|
|
|
/**
|
|
* Child elements that were removed in this transaction.
|
|
*
|
|
* @return {Set}
|
|
*/
|
|
get removedElements () {
|
|
if (this._removedElements === null) {
|
|
const target = this.target
|
|
const transaction = this._transaction
|
|
const removedElements = new Set()
|
|
transaction.deletedStructs.forEach(struct => {
|
|
if (struct._parent === target && !transaction.newTypes.has(struct)) {
|
|
removedElements.add(struct)
|
|
}
|
|
})
|
|
this._removedElements = removedElements
|
|
}
|
|
return this._removedElements
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A shared Array implementation.
|
|
*/
|
|
export class YArray extends Type {
|
|
/**
|
|
* Creates YArray Event and calls observers.
|
|
*
|
|
* @private
|
|
*/
|
|
_callObserver (transaction, parentSubs, remote) {
|
|
this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction))
|
|
}
|
|
|
|
/**
|
|
* Returns the i-th element from a YArray.
|
|
*
|
|
* @param {number} index The index of the element to return from the YArray
|
|
*/
|
|
get (index) {
|
|
let n = this._start
|
|
while (n !== null) {
|
|
if (!n._deleted && n._countable) {
|
|
if (index < n._length) {
|
|
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
|
return n._content[index]
|
|
} else {
|
|
return n
|
|
}
|
|
}
|
|
index -= n._length
|
|
}
|
|
n = n._right
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transforms this YArray to a JavaScript Array.
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
toArray () {
|
|
return this.map(c => c)
|
|
}
|
|
|
|
/**
|
|
* Transforms this Shared Type to a JSON object.
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
toJSON () {
|
|
return this.map(c => {
|
|
if (c instanceof Type) {
|
|
return c.toJSON()
|
|
}
|
|
return c
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Returns an Array with the result of calling a provided function on every
|
|
* element of this YArray.
|
|
*
|
|
* @param {Function} f Function that produces an element of the new Array
|
|
* @return {Array} A new array with each element being the result of the
|
|
* callback function
|
|
*/
|
|
map (f) {
|
|
const res = []
|
|
this.forEach((c, i) => {
|
|
res.push(f(c, i, this))
|
|
})
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* Executes a provided function on once on overy element of this YArray.
|
|
*
|
|
* @param {Function} f A function to execute on every element of this YArray.
|
|
*/
|
|
forEach (f) {
|
|
let index = 0
|
|
let n = this._start
|
|
while (n !== null) {
|
|
if (!n._deleted && n._countable) {
|
|
if (n instanceof Type) {
|
|
f(n, index++, this)
|
|
} else {
|
|
const content = n._content
|
|
const contentLen = content.length
|
|
for (let i = 0; i < contentLen; i++) {
|
|
index++
|
|
f(content[i], index, this)
|
|
}
|
|
}
|
|
}
|
|
n = n._right
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes the length of this YArray.
|
|
*/
|
|
get length () {
|
|
let length = 0
|
|
let n = this._start
|
|
while (n !== null) {
|
|
if (!n._deleted && n._countable) {
|
|
length += n._length
|
|
}
|
|
n = n._right
|
|
}
|
|
return length
|
|
}
|
|
|
|
[Symbol.iterator] () {
|
|
return {
|
|
next: function () {
|
|
while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
|
|
// item is deleted or itemElement does not exist (is deleted)
|
|
this._item = this._item._right
|
|
this._itemElement = 0
|
|
}
|
|
if (this._item === null) {
|
|
return {
|
|
done: true
|
|
}
|
|
}
|
|
let content
|
|
if (this._item instanceof Type) {
|
|
content = this._item
|
|
} else {
|
|
content = this._item._content[this._itemElement++]
|
|
}
|
|
return {
|
|
value: content,
|
|
done: false
|
|
}
|
|
},
|
|
_item: this._start,
|
|
_itemElement: 0,
|
|
_count: 0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes elements starting from an index.
|
|
*
|
|
* @param {number} index Index at which to start deleting elements
|
|
* @param {number} length The number of elements to remove. Defaults to 1.
|
|
*/
|
|
delete (index, length = 1) {
|
|
this._y.transact(() => {
|
|
let item = this._start
|
|
let count = 0
|
|
while (item !== null && length > 0) {
|
|
if (!item._deleted && item._countable) {
|
|
if (count <= index && index < count + item._length) {
|
|
const diffDel = index - count
|
|
item = item._splitAt(this._y, diffDel)
|
|
item._splitAt(this._y, length)
|
|
length -= item._length
|
|
item._delete(this._y)
|
|
count += diffDel
|
|
} else {
|
|
count += item._length
|
|
}
|
|
}
|
|
item = item._right
|
|
}
|
|
})
|
|
if (length > 0) {
|
|
throw new Error('Delete exceeds the range of the YArray')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inserts content after an element container.
|
|
*
|
|
* @private
|
|
* @param {Item} left The element container to use as a reference.
|
|
* @param {Array} content The Array of content to insert (see {@see insert})
|
|
*/
|
|
insertAfter (left, content) {
|
|
this._transact(y => {
|
|
let right
|
|
if (left === null) {
|
|
right = this._start
|
|
} else {
|
|
right = left._right
|
|
}
|
|
let prevJsonIns = null
|
|
for (let i = 0; i < content.length; i++) {
|
|
let c = content[i]
|
|
if (typeof c === 'function') {
|
|
c = new c() // eslint-disable-line new-cap
|
|
}
|
|
if (c instanceof Type) {
|
|
if (prevJsonIns !== null) {
|
|
if (y !== null) {
|
|
prevJsonIns._integrate(y)
|
|
}
|
|
left = prevJsonIns
|
|
prevJsonIns = null
|
|
}
|
|
c._origin = left
|
|
c._left = left
|
|
c._right = right
|
|
c._right_origin = right
|
|
c._parent = this
|
|
if (y !== null) {
|
|
c._integrate(y)
|
|
} else if (left === null) {
|
|
this._start = c
|
|
} else {
|
|
left._right = c
|
|
}
|
|
left = c
|
|
} else {
|
|
if (prevJsonIns === null) {
|
|
prevJsonIns = new ItemJSON()
|
|
prevJsonIns._origin = left
|
|
prevJsonIns._left = left
|
|
prevJsonIns._right = right
|
|
prevJsonIns._right_origin = right
|
|
prevJsonIns._parent = this
|
|
prevJsonIns._content = []
|
|
}
|
|
prevJsonIns._content.push(c)
|
|
}
|
|
}
|
|
if (prevJsonIns !== null) {
|
|
if (y !== null) {
|
|
prevJsonIns._integrate(y)
|
|
} else if (prevJsonIns._left === null) {
|
|
this._start = prevJsonIns
|
|
}
|
|
}
|
|
})
|
|
return content
|
|
}
|
|
|
|
/**
|
|
* Inserts new content at an index.
|
|
*
|
|
* Important: This function expects an array of content. Not just a content
|
|
* object. The reason for this "weirdness" is that inserting several elements
|
|
* is very efficient when it is done as a single operation.
|
|
*
|
|
* @example
|
|
* // Insert character 'a' at position 0
|
|
* yarray.insert(0, ['a'])
|
|
* // Insert numbers 1, 2 at position 1
|
|
* yarray.insert(2, [1, 2])
|
|
*
|
|
* @param {number} index The index to insert content at.
|
|
* @param {Array} content The array of content
|
|
*/
|
|
insert (index, content) {
|
|
this._transact(() => {
|
|
let left = null
|
|
let right = this._start
|
|
let count = 0
|
|
const y = this._y
|
|
while (right !== null) {
|
|
const rightLen = right._deleted ? 0 : (right._length - 1)
|
|
if (count <= index && index <= count + rightLen) {
|
|
const splitDiff = index - count
|
|
right = right._splitAt(y, splitDiff)
|
|
left = right._left
|
|
count += splitDiff
|
|
break
|
|
}
|
|
if (!right._deleted) {
|
|
count += right._length
|
|
}
|
|
left = right
|
|
right = right._right
|
|
}
|
|
if (index > count) {
|
|
throw new Error('Index exceeds array range!')
|
|
}
|
|
this.insertAfter(left, content)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Appends content to this YArray.
|
|
*
|
|
* @param {Array} content Array of content to append.
|
|
*/
|
|
push (content) {
|
|
let n = this._start
|
|
let lastUndeleted = null
|
|
while (n !== null) {
|
|
if (!n._deleted) {
|
|
lastUndeleted = n
|
|
}
|
|
n = n._right
|
|
}
|
|
this.insertAfter(lastUndeleted, content)
|
|
}
|
|
|
|
/**
|
|
* Transform this YXml Type to a readable format.
|
|
* Useful for logging as all Items and Delete implement this method.
|
|
*
|
|
* @private
|
|
*/
|
|
_logString () {
|
|
return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`)
|
|
}
|
|
}
|