import { removeEventHandlerListener, callEventHandlerListeners, addEventHandlerListener, createEventHandler, getState, isVisible, ContentType, createID, ContentAny, ContentBinary, getItemCleanStart, AbstractUpdateEncoder, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line } from '../internals.js' import * as map from 'lib0/map.js' import * as iterator from 'lib0/iterator.js' import * as error from 'lib0/error.js' import * as math from 'lib0/math.js' const maxSearchMarker = 60 /** * A unique timestamp that identifies each marker. * * Time is relative,.. this is more like an ever-increasing clock. * * @type {number} */ let globalSearchMarkerTimestamp = 0 export class ArraySearchMarker { /** * @param {Item} p * @param {number} index */ constructor (p, index) { p.marker = true this.p = p this.index = index this.timestamp = globalSearchMarkerTimestamp++ } } /** * @param {ArraySearchMarker} marker */ const refreshMarkerTimestamp = marker => { marker.timestamp = globalSearchMarkerTimestamp++ } /** * This is rather complex so this function is the only thing that should overwrite a marker * * @param {ArraySearchMarker} marker * @param {Item} p * @param {number} index */ const overwriteMarker = (marker, p, index) => { marker.p.marker = false marker.p = p p.marker = true marker.index = index marker.timestamp = globalSearchMarkerTimestamp++ } /** * @param {Array} searchMarker * @param {Item} p * @param {number} index */ const markPosition = (searchMarker, p, index) => { if (searchMarker.length >= maxSearchMarker) { // override oldest marker (we don't want to create more objects) const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b) overwriteMarker(marker, p, index) return marker } else { // create new marker const pm = new ArraySearchMarker(p, index) searchMarker.push(pm) return pm } } /** * Search marker help us to find positions in the associative array faster. * * They speed up the process of finding a position without much bookkeeping. * * A maximum of `maxSearchMarker` objects are created. * * This function always returns a refreshed marker (updated timestamp) * * @param {AbstractType} yarray * @param {number} index */ export const findMarker = (yarray, index) => { if (yarray._start === null || index === 0 || yarray._searchMarker === null) { return null } const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b) let p = yarray._start let pindex = 0 if (marker !== null) { p = marker.p pindex = marker.index refreshMarkerTimestamp(marker) // we used it, we might need to use it again } // iterate to right if possible while (p.right !== null && pindex < index) { if (!p.deleted && p.countable) { if (index < pindex + p.length) { break } pindex += p.length } p = p.right } // iterate to left if necessary (might be that pindex > index) while (p.left !== null && pindex > index) { p = p.left if (!p.deleted && p.countable) { pindex -= p.length } } // we want to make sure that p can't be merged with left, because that would screw up everything // in that cas just return what we have (it is most likely the best marker anyway) // iterate to left until p can't be merged with left while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) { p = p.left if (!p.deleted && p.countable) { pindex -= p.length } } // @todo remove! // assure position // { // let start = yarray._start // let pos = 0 // while (start !== p) { // if (!start.deleted && start.countable) { // pos += start.length // } // start = /** @type {Item} */ (start.right) // } // if (pos !== pindex) { // debugger // throw new Error('Gotcha position fail!') // } // } // if (marker) { // if (window.lengthes == null) { // window.lengthes = [] // } // window.lengthes.push(marker.index - pindex) // console.log('distance', marker.index - pindex, 'len', p && p.parent.length) // } if (marker !== null && math.abs(marker.index - pindex) < 30) { // adjust existing marker overwriteMarker(marker, p, pindex) return marker } else { // create new marker return markPosition(yarray._searchMarker, p, pindex) } } /** * Update markers when a change happened. * * This should be called before doing a deletion! * * @param {Array} searchMarker * @param {number} index * @param {number} len If insertion, len is positive. If deletion, len is negative. */ export const updateMarkerChanges = (searchMarker, index, len) => { for (let i = searchMarker.length - 1; i >= 0; i--) { const m = searchMarker[i] if (len > 0) { /** * @type {Item|null} */ let p = m.p p.marker = false // Ideally we just want to do a simple position comparison, but this will only work if // search markers don't point to deleted items for formats. // Iterate marker to prev undeleted countable position so we know what to do when updating a position while (p && (p.deleted || !p.countable)) { p = p.left if (p && !p.deleted && p.countable) { // adjust position. the loop should break now m.index -= p.length } } if (p === null || p.marker === true) { // remove search marker if updated position is null or if position is already marked searchMarker.splice(i, 1) continue } m.p = p p.marker = true } if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice m.index = math.max(index, m.index + len) } } } /** * Accumulate all (list) children of a type and return them as an Array. * * @param {AbstractType} t * @return {Array} */ export const getTypeChildren = t => { let s = t._start const arr = [] while (s) { arr.push(s) s = s.right } return arr } /** * Call event listeners with an event. This will also add an event to all * parents (for `.observeDeep` handlers). * * @template EventType * @param {AbstractType} type * @param {Transaction} transaction * @param {EventType} event */ export const callTypeObservers = (type, transaction, event) => { const changedType = type const changedParentTypes = transaction.changedParentTypes while (true) { // @ts-ignore map.setIfUndefined(changedParentTypes, type, () => []).push(event) if (type._item === null) { break } type = /** @type {AbstractType} */ (type._item.parent) } callEventHandlerListeners(changedType._eH, event, transaction) } /** * @template EventType * Abstract Yjs Type class */ export class AbstractType { constructor () { /** * @type {Item|null} */ this._item = null /** * @type {Map} */ this._map = new Map() /** * @type {Item|null} */ this._start = null /** * @type {Doc|null} */ this.doc = null this._length = 0 /** * Event handlers * @type {EventHandler} */ this._eH = createEventHandler() /** * Deep event handlers * @type {EventHandler,Transaction>} */ this._dEH = createEventHandler() /** * @type {null | Array} */ this._searchMarker = null } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item|null} item */ _integrate (y, item) { this.doc = y this._item = item } /** * @return {AbstractType} */ _copy () { throw error.methodUnimplemented() } /** * @param {AbstractUpdateEncoder} encoder */ _write (encoder) { } /** * The first non-deleted item */ get _first () { let n = this._start while (n !== null && n.deleted) { n = n.right } return n } /** * Creates YEvent and calls all type observers. * Must be implemented by each type. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { if (!transaction.local && this._searchMarker) { this._searchMarker.length = 0 } } /** * Observe all events that are created on this type. * * @param {function(EventType, Transaction):void} f Observer function */ observe (f) { addEventHandlerListener(this._eH, f) } /** * Observe all events that are created by this type and its children. * * @param {function(Array,Transaction):void} f Observer function */ observeDeep (f) { addEventHandlerListener(this._dEH, f) } /** * Unregister an observer function. * * @param {function(EventType,Transaction):void} f Observer function */ unobserve (f) { removeEventHandlerListener(this._eH, f) } /** * Unregister an observer function. * * @param {function(Array,Transaction):void} f Observer function */ unobserveDeep (f) { removeEventHandlerListener(this._dEH, f) } /** * @abstract * @return {any} */ toJSON () {} } /** * @param {AbstractType} type * @return {Array} * * @private * @function */ export const typeListToArray = type => { const cs = [] let n = type._start while (n !== null) { if (n.countable && !n.deleted) { const c = n.content.getContent() for (let i = 0; i < c.length; i++) { cs.push(c[i]) } } n = n.right } return cs } /** * @param {AbstractType} type * @param {Snapshot} snapshot * @return {Array} * * @private * @function */ export const typeListToArraySnapshot = (type, snapshot) => { const cs = [] let n = type._start while (n !== null) { if (n.countable && isVisible(n, snapshot)) { const c = n.content.getContent() for (let i = 0; i < c.length; i++) { cs.push(c[i]) } } n = n.right } return cs } /** * Executes a provided function on once on overy element of this YArray. * * @param {AbstractType} type * @param {function(any,number,any):void} f A function to execute on every element of this YArray. * * @private * @function */ export const typeListForEach = (type, f) => { let index = 0 let n = type._start while (n !== null) { if (n.countable && !n.deleted) { const c = n.content.getContent() for (let i = 0; i < c.length; i++) { f(c[i], index++, type) } } n = n.right } } /** * @template C,R * @param {AbstractType} type * @param {function(C,number,AbstractType):R} f * @return {Array} * * @private * @function */ export const typeListMap = (type, f) => { /** * @type {Array} */ const result = [] typeListForEach(type, (c, i) => { result.push(f(c, i, type)) }) return result } /** * @param {AbstractType} type * @return {IterableIterator} * * @private * @function */ export const typeListCreateIterator = type => { let n = type._start /** * @type {Array|null} */ let currentContent = null let currentContentIndex = 0 return { [Symbol.iterator] () { return this }, next: () => { // find some content if (currentContent === null) { while (n !== null && n.deleted) { n = n.right } // check if we reached the end, no need to check currentContent, because it does not exist if (n === null) { return { done: true, value: undefined } } // we found n, so we can set currentContent currentContent = n.content.getContent() currentContentIndex = 0 n = n.right // we used the content of n, now iterate to next } const value = currentContent[currentContentIndex++] // check if we need to empty currentContent if (currentContent.length <= currentContentIndex) { currentContent = null } return { done: false, value } } } } /** * Executes a provided function on once on overy element of this YArray. * Operates on a snapshotted state of the document. * * @param {AbstractType} type * @param {function(any,number,AbstractType):void} f A function to execute on every element of this YArray. * @param {Snapshot} snapshot * * @private * @function */ export const typeListForEachSnapshot = (type, f, snapshot) => { let index = 0 let n = type._start while (n !== null) { if (n.countable && isVisible(n, snapshot)) { const c = n.content.getContent() for (let i = 0; i < c.length; i++) { f(c[i], index++, type) } } n = n.right } } /** * @param {AbstractType} type * @param {number} index * @return {any} * * @private * @function */ export const typeListGet = (type, index) => { const marker = findMarker(type, index) let n = type._start if (marker !== null) { n = marker.p index -= marker.index } for (; n !== null; n = n.right) { if (!n.deleted && n.countable) { if (index < n.length) { return n.content.getContent()[index] } index -= n.length } } } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {Item?} referenceItem * @param {Array|Array|boolean|number|string|Uint8Array>} content * * @private * @function */ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => { let left = referenceItem const doc = transaction.doc const ownClientId = doc.clientID const store = doc.store const right = referenceItem === null ? parent._start : referenceItem.right /** * @type {Array|number>} */ let jsonContent = [] const packJsonContent = () => { if (jsonContent.length > 0) { left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent)) left.integrate(transaction, 0) jsonContent = [] } } content.forEach(c => { switch (c.constructor) { case Number: case Object: case Boolean: case Array: case String: jsonContent.push(c) break default: packJsonContent() switch (c.constructor) { case Uint8Array: case ArrayBuffer: left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c)))) left.integrate(transaction, 0) break default: if (c instanceof AbstractType) { left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c)) left.integrate(transaction, 0) } else { throw new Error('Unexpected content type in insert operation') } } } }) packJsonContent() } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {number} index * @param {Array|Array|number|string|Uint8Array>} content * * @private * @function */ export const typeListInsertGenerics = (transaction, parent, index, content) => { if (index === 0) { if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, index, content.length) } return typeListInsertGenericsAfter(transaction, parent, null, content) } const startIndex = index const marker = findMarker(parent, index) let n = parent._start if (marker !== null) { n = marker.p index -= marker.index // we need to iterate one to the left so that the algorithm works if (index === 0) { // @todo refactor this as it actually doesn't consider formats n = n.prev // important! get the left undeleted item so that we can actually decrease index index += (n && n.countable && !n.deleted) ? n.length : 0 } } for (; n !== null; n = n.right) { if (!n.deleted && n.countable) { if (index <= n.length) { if (index < n.length) { // insert in-between getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)) } break } index -= n.length } } if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, startIndex, content.length) } return typeListInsertGenericsAfter(transaction, parent, n, content) } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {number} index * @param {number} length * * @private * @function */ export const typeListDelete = (transaction, parent, index, length) => { if (length === 0) { return } const startIndex = index const startLength = length const marker = findMarker(parent, index) let n = parent._start if (marker !== null) { n = marker.p index -= marker.index } // compute the first item to be deleted for (; n !== null && index > 0; n = n.right) { if (!n.deleted && n.countable) { if (index < n.length) { getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)) } index -= n.length } } // delete all items until done while (length > 0 && n !== null) { if (!n.deleted) { if (length < n.length) { getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length)) } n.delete(transaction) length -= n.length } n = n.right } if (length > 0) { throw error.create('array length exceeded') } if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */) } } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {string} key * * @private * @function */ export const typeMapDelete = (transaction, parent, key) => { const c = parent._map.get(key) if (c !== undefined) { c.delete(transaction) } } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {string} key * @param {Object|number|Array|string|Uint8Array|AbstractType} value * * @private * @function */ export const typeMapSet = (transaction, parent, key, value) => { const left = parent._map.get(key) || null const doc = transaction.doc const ownClientId = doc.clientID let content if (value == null) { content = new ContentAny([value]) } else { switch (value.constructor) { case Number: case Object: case Boolean: case Array: case String: content = new ContentAny([value]) break case Uint8Array: content = new ContentBinary(/** @type {Uint8Array} */ (value)) break default: if (value instanceof AbstractType) { content = new ContentType(value) } else { throw new Error('Unexpected content type') } } } new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0) } /** * @param {AbstractType} parent * @param {string} key * @return {Object|number|Array|string|Uint8Array|AbstractType|undefined} * * @private * @function */ export const typeMapGet = (parent, key) => { const val = parent._map.get(key) return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined } /** * @param {AbstractType} parent * @return {Object|number|Array|string|Uint8Array|AbstractType|undefined>} * * @private * @function */ export const typeMapGetAll = (parent) => { /** * @type {Object} */ const res = {} parent._map.forEach((value, key) => { if (!value.deleted) { res[key] = value.content.getContent()[value.length - 1] } }) return res } /** * @param {AbstractType} parent * @param {string} key * @return {boolean} * * @private * @function */ export const typeMapHas = (parent, key) => { const val = parent._map.get(key) return val !== undefined && !val.deleted } /** * @param {AbstractType} parent * @param {string} key * @param {Snapshot} snapshot * @return {Object|number|Array|string|Uint8Array|AbstractType|undefined} * * @private * @function */ export const typeMapGetSnapshot = (parent, key, snapshot) => { let v = parent._map.get(key) || null while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) { v = v.left } return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined } /** * @param {Map} map * @return {IterableIterator>} * * @private * @function */ export const createMapIterator = map => iterator.iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)