From 52ec69863596a0fa6088af9e7cddb3868f0a0569 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 9 Apr 2019 04:01:37 +0200 Subject: [PATCH] implement some of the commented todos --- examples/codemirror.js | 2 +- examples/textarea.js | 3 +- rollup.config.js | 11 ++- src/index.js | 2 + src/structs/AbstractItem.js | 15 +---- src/structs/GC.js | 1 - src/structs/ItemBinary.js | 2 - src/structs/ItemDeleted.js | 7 +- src/structs/ItemJSON.js | 5 +- src/structs/ItemString.js | 7 +- src/structs/ItemType.js | 4 +- src/types/AbstractType.js | 55 +++++++-------- src/types/YArray.js | 8 ++- src/types/YMap.js | 8 ++- src/types/YText.js | 17 ++--- src/types/YXmlElement.js | 12 ++-- src/utils/DeleteSet.js | 4 +- src/utils/ID.js | 26 ++----- src/utils/StructStore.js | 4 +- src/utils/Transaction.js | 121 +++++++++++++++++++++++++++++++++ src/utils/UndoManager.js | 6 +- src/utils/Y.js | 123 +--------------------------------- src/utils/relativePosition.js | 28 ++++++-- tests/testHelper.js | 5 +- 24 files changed, 233 insertions(+), 243 deletions(-) diff --git a/examples/codemirror.js b/examples/codemirror.js index 05c4dd59..96f804fc 100644 --- a/examples/codemirror.js +++ b/examples/codemirror.js @@ -9,7 +9,7 @@ import 'codemirror/mode/javascript/javascript.js' const provider = new WebsocketProvider(conf.serverAddress) const ydocument = provider.get('codemirror') -const ytext = ydocument.define('codemirror', Y.Text) +const ytext = ydocument.getText('codemirror') const editor = new CodeMirror(document.querySelector('#container'), { mode: 'javascript', diff --git a/examples/textarea.js b/examples/textarea.js index ec9cf0d3..585f232f 100644 --- a/examples/textarea.js +++ b/examples/textarea.js @@ -1,4 +1,3 @@ -import * as Y from 'yjs' import { WebsocketProvider } from 'y-websocket' import { TextareaBinding } from 'y-textarea' @@ -6,7 +5,7 @@ import * as conf from './exampleConfig.js' const provider = new WebsocketProvider(conf.serverAddress) const ydocument = provider.get('textarea') -const type = ydocument.define('textarea', Y.Text) +const type = ydocument.getText('textarea') const textarea = document.querySelector('textarea') const binding = new TextareaBinding(type, textarea) diff --git a/rollup.config.js b/rollup.config.js index 5077c554..d54ba55f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -56,8 +56,8 @@ export default [{ format: 'cjs', sourcemap: true, paths: path => { - if (/^funlib\//.test(path)) { - return `lib0/dist${path.slice(6)}` + if (/^lib0\//.test(path)) { + return `lib0/dist/${path.slice(5)}` } return path } @@ -85,9 +85,8 @@ export default [{ }), commonjs() ] -} -/* { - input: ['./examples/codemirror.js', './examples/textarea.js', './examples/quill.js', './examples/dom.js', './examples/prosemirror.js'], // fs.readdirSync('./examples').filter(file => /(? './examples/' + file), +}, { + input: ['./examples/codemirror.js', './examples/textarea.js'/*, './examples/quill.js', './examples/dom.js', './examples/prosemirror.js'*/], // fs.readdirSync('./examples').filter(file => /(? './examples/' + file), output: { dir: 'examples/build', format: 'esm', @@ -103,4 +102,4 @@ export default [{ commonjs(), ...minificationPlugins ] -} */] +}] diff --git a/src/index.js b/src/index.js index 281b92bb..a995c3d5 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,8 @@ export { compareRelativePositions, writeRelativePosition, readRelativePosition, + createRelativePositionFromJSON, + toAbsolutePosition, AbsolutePosition, RelativePosition, ID, diff --git a/src/structs/AbstractItem.js b/src/structs/AbstractItem.js index 75bd466e..09b82c00 100644 --- a/src/structs/AbstractItem.js +++ b/src/structs/AbstractItem.js @@ -29,14 +29,11 @@ import * as binary from 'lib0/binary.js' /** * Split leftItem into two items - * @param {StructStore} store * @param {AbstractItem} leftItem * @param {number} diff * @return {AbstractItem} - * - * @todo remove store param0 */ -export const splitItem = (store, leftItem, diff) => { +export const splitItem = (leftItem, diff) => { const id = leftItem.id // create rightItem const rightItem = leftItem.copy( @@ -182,7 +179,7 @@ export class AbstractItem extends AbstractStruct { if (o.id.client < id.client) { this.left = o conflictingItems.clear() - } // TODO: verify break else? + } } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) { // case 2 if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) { @@ -192,9 +189,6 @@ export class AbstractItem extends AbstractStruct { } else { break } - // TODO: experiment with rightOrigin. - // 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 @@ -340,8 +334,6 @@ export class AbstractItem extends AbstractStruct { /** * Computes the last content address of this Item. - * TODO: do still need this? - * @private */ get lastId () { return createID(this.id.client, this.id.clock + this.length - 1) @@ -378,11 +370,10 @@ export class AbstractItem extends AbstractStruct { * * This method should only be cally by StructStore. * - * @param {StructStore} store * @param {number} diff * @return {AbstractItem} */ - splitAt (store, diff) { + splitAt (diff) { throw new Error('unimplemented') } diff --git a/src/structs/GC.js b/src/structs/GC.js index a2af4af1..fb5693d1 100644 --- a/src/structs/GC.js +++ b/src/structs/GC.js @@ -14,7 +14,6 @@ import * as encoding from 'lib0/encoding.js' export const structGCRefNumber = 0 -// TODO should have the same base class as Item export class GC extends AbstractStruct { /** * @param {ID} id diff --git a/src/structs/ItemBinary.js b/src/structs/ItemBinary.js index 96b67bc8..645f2c9a 100644 --- a/src/structs/ItemBinary.js +++ b/src/structs/ItemBinary.js @@ -2,8 +2,6 @@ * @module structs */ -// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency - import { AbstractItem, AbstractItemRef, diff --git a/src/structs/ItemDeleted.js b/src/structs/ItemDeleted.js index eb8c1655..7cd1e89f 100644 --- a/src/structs/ItemDeleted.js +++ b/src/structs/ItemDeleted.js @@ -2,8 +2,6 @@ * @module structs */ -// TODO: ItemBinary should be able to merge with right (similar to other items). Or the other items (ItemJSON) should not be able to merge - extra byte + consistency - import { AbstractItem, AbstractItemRef, @@ -59,15 +57,14 @@ export class ItemDeleted extends AbstractItem { addToDeleteSet(transaction.deleteSet, this.id, this.length) } /** - * @param {StructStore} store * @param {number} diff */ - splitAt (store, diff) { + splitAt (diff) { /** * @type {ItemDeleted} */ // @ts-ignore - const right = splitItem(store, this, diff) + const right = splitItem(this, diff) right._len -= diff this._len = diff return right diff --git a/src/structs/ItemJSON.js b/src/structs/ItemJSON.js index 4bdccb71..aadb71e7 100644 --- a/src/structs/ItemJSON.js +++ b/src/structs/ItemJSON.js @@ -54,15 +54,14 @@ export class ItemJSON extends AbstractItem { return this.content } /** - * @param {StructStore} store * @param {number} diff */ - splitAt (store, diff) { + splitAt (diff) { /** * @type {ItemJSON} */ // @ts-ignore - const right = splitItem(store, this, diff) + const right = splitItem(this, diff) right.content = this.content.splice(diff) return right } diff --git a/src/structs/ItemString.js b/src/structs/ItemString.js index 5179ab69..b0373c91 100644 --- a/src/structs/ItemString.js +++ b/src/structs/ItemString.js @@ -15,7 +15,7 @@ import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' export const structStringRefNumber = 6 -// TODO: we can probably try to omit rightOrigin. We can just use .right + export class ItemString extends AbstractItem { /** * @param {ID} id @@ -53,16 +53,15 @@ export class ItemString extends AbstractItem { return this.string.length } /** - * @param {StructStore} store * @param {number} diff * @return {ItemString} */ - splitAt (store, diff) { + splitAt (diff) { /** * @type {ItemString} */ // @ts-ignore - const right = splitItem(store, this, diff) + const right = splitItem(this, diff) right.string = this.string.slice(diff) this.string = this.string.slice(0, diff) return right diff --git a/src/structs/ItemType.js b/src/structs/ItemType.js index 2e546c43..53d76c99 100644 --- a/src/structs/ItemType.js +++ b/src/structs/ItemType.js @@ -15,7 +15,7 @@ import { readYXmlFragment, readYXmlHook, readYXmlText, - StructStore, Y, GC, ItemDeleted, Transaction, ID, AbstractType // eslint-disable-line + StructStore, Y, GC, Transaction, ID, AbstractType // eslint-disable-line } from '../internals.js' import * as encoding from 'lib0/encoding.js' // eslint-disable-line @@ -83,7 +83,7 @@ export class ItemType extends AbstractItem { * @param {ID | null} rightOrigin * @param {AbstractType} parent * @param {string | null} parentSub - * @return {AbstractItem} TODO, returns itemtype + * @return {ItemType} */ copy (id, left, origin, right, rightOrigin, parent, parentSub) { return new ItemType(id, left, origin, right, rightOrigin, parent, parentSub, this.type._copy()) diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 3fe77940..5260ff67 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -22,6 +22,29 @@ import * as iterator from 'lib0/iterator.js' import * as error from 'lib0/error.js' import * as encoding from 'lib0/encoding.js' // eslint-disable-line +/** + * Call event listeners with an event. This will also add an event to all + * parents (for `.observeDeep` handlers). + * @private + * + * @template EventType + * @param {AbstractType} type + * @param {Transaction} transaction + * @param {EventType} event + */ +export const callTypeObservers = (type, transaction, event) => { + callEventHandlerListeners(type._eH, [event, transaction]) + const changedParentTypes = transaction.changedParentTypes + while (true) { + // @ts-ignore + map.setIfUndefined(changedParentTypes, type, () => []).push(event) + if (type._item === null) { + break + } + type = type._item.parent + } +} + /** * @template EventType * Abstract Yjs Type class @@ -100,42 +123,14 @@ export class AbstractType { } /** - * Creates YEvent and calls _callEventHandler. + * Creates YEvent and calls all type observers. * Must be implemented by each type. - * @todo Rename to _createEvent * @private * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ - _callObserver (transaction, parentSubs) { - throw error.methodUnimplemented() - } - - /** - * Call event listeners with an event. This will also add an event to all - * parents (for `.observeDeep` handlers). - * @private - * - * @param {Transaction} transaction - * @param {any} event - */ - _callEventHandler (transaction, event) { - callEventHandlerListeners(this._eH, [event, transaction]) - const changedParentTypes = transaction.changedParentTypes - /** - * @type {AbstractType} - */ - let type = this - while (true) { - // @ts-ignore - map.setIfUndefined(changedParentTypes, type, () => []).push(event) - if (type._item === null) { - break - } - type = type._item.parent - } - } + _callObserver (transaction, parentSubs) { /* skip if no type is specified */ } /** * Observe all events that are created on this type. diff --git a/src/types/YArray.js b/src/types/YArray.js index e10c7f0f..3156ad4b 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -13,6 +13,8 @@ import { typeArrayDelete, typeArrayMap, YArrayRefID, + callTypeObservers, + transact, Y, Transaction, ItemType, // eslint-disable-line } from '../internals.js' @@ -75,7 +77,7 @@ export class YArray extends AbstractType { * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { - this._callEventHandler(transaction, new YArrayEvent(this, transaction)) + callTypeObservers(this, transaction, new YArrayEvent(this, transaction)) } /** @@ -141,7 +143,7 @@ export class YArray extends AbstractType { */ delete (index, length = 1) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { typeArrayDelete(transaction, this, index, length) }) } else { @@ -168,7 +170,7 @@ export class YArray extends AbstractType { */ insert (index, content) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { typeArrayInsertGenerics(transaction, this, index, content) }) } else { diff --git a/src/types/YMap.js b/src/types/YMap.js index eda6f192..5206d6f9 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -11,6 +11,8 @@ import { typeMapHas, createMapIterator, YMapRefID, + callTypeObservers, + transact, Y, Transaction, ItemType, // eslint-disable-line } from '../internals.js' @@ -75,7 +77,7 @@ export class YMap extends AbstractType { * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { - this._callEventHandler(transaction, new YMapEvent(this, transaction, parentSubs)) + callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs)) } /** @@ -125,7 +127,7 @@ export class YMap extends AbstractType { */ delete (key) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { typeMapDelete(transaction, this, key) }) } else { @@ -142,7 +144,7 @@ export class YMap extends AbstractType { */ set (key, value) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { typeMapSet(transaction, this, key, value) }) } else { diff --git a/src/types/YText.js b/src/types/YText.js index dd5500c2..0c397960 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -13,6 +13,8 @@ import { getItemCleanStart, isVisible, YTextRefID, + callTypeObservers, + transact, Y, ItemType, AbstractItem, Snapshot, StructStore, Transaction // eslint-disable-line } from '../internals.js' @@ -295,7 +297,6 @@ const deleteText = (transaction, parent, left, right, currentAttributes, length) return { left, right } } -// TODO: In the quill delta representation we should also use the format {ops:[..]} /** * The Quill Delta format represents changes on a text document with * formatting information. For mor information visit {@link https://quilljs.com/docs/delta/|Quill Delta} @@ -354,7 +355,7 @@ class YTextEvent extends YEvent { if (this._delta === null) { const y = this.target._y // @ts-ignore - y.transact(transaction => { + transact(y, transaction => { /** * @type {Array<{delete:number|undefined,retain:number|undefined,insert:string|undefined,attributes:Object}>} */ @@ -586,7 +587,7 @@ export class YText extends AbstractType { * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { - this._callEventHandler(transaction, new YTextEvent(this, transaction)) + callTypeObservers(this, transaction, new YTextEvent(this, transaction)) } toDom () { @@ -657,7 +658,7 @@ export class YText extends AbstractType { */ applyDelta (delta) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { /** * @type {{left:AbstractItem|null,right:AbstractItem|null}} */ @@ -772,7 +773,7 @@ export class YText extends AbstractType { } const y = this._y if (y !== null) { - y.transact(transaction => { + transact(y, transaction => { const {left, right, currentAttributes} = findPosition(transaction, y.store, this, index) insertText(transaction, this, left, right, currentAttributes, text, attributes) }) @@ -795,7 +796,7 @@ export class YText extends AbstractType { } const y = this._y if (y !== null) { - y.transact(transaction => { + transact(y, transaction => { const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) insertText(transaction, this, left, right, currentAttributes, embed, attributes) }) @@ -816,7 +817,7 @@ export class YText extends AbstractType { } const y = this._y if (y !== null) { - y.transact(transaction => { + transact(y, transaction => { const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) deleteText(transaction, this, left, right, currentAttributes, length) }) @@ -836,7 +837,7 @@ export class YText extends AbstractType { format (index, length, attributes) { const y = this._y if (y !== null) { - y.transact(transaction => { + transact(y, transaction => { let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) if (right === null) { return diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js index 45f2eedd..7c7c0a37 100644 --- a/src/types/YXmlElement.js +++ b/src/types/YXmlElement.js @@ -14,6 +14,8 @@ import { typeMapSet, typeMapDelete, YXmlElementRefID, + callTypeObservers, + transact, Y, Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line } from '../internals.js' @@ -192,7 +194,7 @@ export class YXmlFragment extends AbstractType { * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { - this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, transaction)) + callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction)) } toString () { @@ -329,7 +331,7 @@ export class YXmlElement extends YXmlFragment { */ removeAttribute (attributeName) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { typeMapDelete(transaction, this, attributeName) }) } else { @@ -348,7 +350,7 @@ export class YXmlElement extends YXmlFragment { */ setAttribute (attributeName, attributeValue) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { typeMapSet(transaction, this, attributeName, attributeValue) }) } else { @@ -395,7 +397,7 @@ export class YXmlElement extends YXmlFragment { */ insert (index, content) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { typeArrayInsertGenerics(transaction, this, index, content) }) } else { @@ -412,7 +414,7 @@ export class YXmlElement extends YXmlFragment { */ delete (index, length = 1) { if (this._y !== null) { - this._y.transact(transaction => { + transact(this._y, transaction => { typeArrayDelete(transaction, this, index, length) }) } else { diff --git a/src/utils/DeleteSet.js b/src/utils/DeleteSet.js index 2940ea7b..03c52c54 100644 --- a/src/utils/DeleteSet.js +++ b/src/utils/DeleteSet.js @@ -193,7 +193,7 @@ export const readDeleteSet = (decoder, transaction, store) => { let struct = structs[index] // split the first item if necessary if (!struct.deleted && struct.id.clock < clock) { - structs.splice(index + 1, 0, struct.splitAt(store, clock - struct.id.clock)) + structs.splice(index + 1, 0, struct.splitAt(clock - struct.id.clock)) index++ // increase we now want to use the next struct } while (index < structs.length) { @@ -202,7 +202,7 @@ export const readDeleteSet = (decoder, transaction, store) => { if (struct.id.clock < clock + len) { if (!struct.deleted) { if (clock + len < struct.id.clock + struct.length) { - structs.splice(index, 0, struct.splitAt(store, clock + len - struct.id.clock)) + structs.splice(index, 0, struct.splitAt(clock + len - struct.id.clock)) } struct.delete(transaction) } diff --git a/src/utils/ID.js b/src/utils/ID.js index 133fb449..2cefe8a6 100644 --- a/src/utils/ID.js +++ b/src/utils/ID.js @@ -24,27 +24,13 @@ export class ID { this.clock = clock } /** - * @return {ID} + * @deprecated + * @todo remove and adapt relative position implementation */ - clone () { - return new ID(this.client, this.clock) - } - /** - * @param {ID} id - * @return {boolean} - */ - equals (id) { - return id !== null && id.client === this.client && id.clock === this.clock - } - /** - * @param {ID} id - * @return {boolean} - */ - lessThan (id) { - if (id.constructor === ID) { - return this.client < id.client || (this.client === id.client && this.clock < id.clock) - } else { - return false + toJSON () { + return { + client: this.client, + clock: this.clock } } } diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js index d28cece1..db72b970 100644 --- a/src/utils/StructStore.js +++ b/src/utils/StructStore.js @@ -175,7 +175,7 @@ export const getItemCleanStart = (store, id) => { */ let struct = structs[index] if (struct.id.clock < id.clock && struct.constructor !== GC) { - struct = struct.splitAt(store, id.clock - struct.id.clock) + struct = struct.splitAt(id.clock - struct.id.clock) structs.splice(index + 1, 0, struct) } return struct @@ -198,7 +198,7 @@ export const getItemCleanEnd = (store, id) => { const index = findIndexSS(structs, id.clock) const struct = structs[index] if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) { - structs.splice(index + 1, 0, struct.splitAt(store, id.clock - struct.id.clock + 1)) + structs.splice(index + 1, 0, struct.splitAt(id.clock - struct.id.clock + 1)) } return struct } diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 0076484c..bf691de7 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -10,11 +10,16 @@ import { DeleteSet, sortAndMergeDeleteSet, getStates, + findIndexSS, + callEventHandlerListeners, + AbstractItem, + ItemDeleted, AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line } from '../internals.js' import * as encoding from 'lib0/encoding.js' import * as map from 'lib0/map.js' +import * as math from 'lib0/math.js' /** * A transaction is created for every change on the Yjs model. It is possible @@ -108,3 +113,119 @@ export const nextID = transaction => { const y = transaction.y return createID(y.clientID, getState(y.store, y.clientID)) } + +/** + * @param {Y} y + * @param {function(Transaction):void} f + */ +export const transact = (y, f) => { + let initialCall = false + if (y._transaction === null) { + initialCall = true + y._transaction = new Transaction(y) + y.emit('beforeTransaction', [y, y._transaction]) + } + const transaction = y._transaction + try { + f(transaction) + } finally { + if (initialCall) { + y._transaction = null + y.emit('beforeObserverCalls', [y, y._transaction]) + // emit change events on changed types + transaction.changed.forEach((subs, itemtype) => { + itemtype._callObserver(transaction, subs) + }) + transaction.changedParentTypes.forEach((events, type) => { + events = events + .filter(event => + event.target._item === null || !event.target._item.deleted + ) + events + .forEach(event => { + event.currentTarget = type + }) + // we don't need to check for events.length + // because we know it has at least one element + callEventHandlerListeners(type._dEH, [events, transaction]) + }) + // only call afterTransaction listeners if anything changed + transaction.afterState = getStates(transaction.y.store) + // when all changes & events are processed, emit afterTransaction event + // transaction cleanup + const store = transaction.y.store + const ds = transaction.deleteSet + // replace deleted items with ItemDeleted / GC + sortAndMergeDeleteSet(ds) + y.emit('afterTransaction', [y, transaction]) + for (const [client, deleteItems] of ds.clients) { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(client) + for (let di = 0; di < deleteItems.length; di++) { + const deleteItem = deleteItems[di] + for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) { + const struct = structs[si] + if (deleteItem.clock + deleteItem.len < struct.id.clock) { + break + } + if (struct.deleted && struct instanceof AbstractItem && (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted))) { + // check if we can GC + struct.gc(transaction, store) + } + } + } + } + /** + * @param {Array} structs + * @param {number} pos + */ + const tryToMergeWithLeft = (structs, pos) => { + const left = structs[pos - 1] + const right = structs[pos] + if (left.deleted === right.deleted && left.constructor === right.constructor) { + if (left.mergeWith(right)) { + structs.splice(pos, 1) + } + } + } + // on all affected store.clients props, try to merge + for (const [client, clock] of transaction.afterState) { + const beforeClock = transaction.beforeState.get(client) || 0 + if (beforeClock !== clock) { + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(client) + // we iterate from right to left so we can safely remove entries + const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1) + for (let i = structs.length - 1; i >= firstChangePos; i--) { + tryToMergeWithLeft(structs, i) + } + } + } + // try to merge replacedItems + for (const replacedItem of transaction._replacedItems) { + const id = replacedItem.id + const client = id.client + const clock = id.clock + /** + * @type {Array} + */ + // @ts-ignore + const structs = store.clients.get(client) + const replacedStructPos = findIndexSS(structs, clock) + if (replacedStructPos + 1 < structs.length) { + tryToMergeWithLeft(structs, replacedStructPos + 1) + } + if (replacedStructPos > 0) { + tryToMergeWithLeft(structs, replacedStructPos) + } + } + y.emit('afterTransactionCleanup', [y, transaction]) + } + } +} diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index 69a72894..926602ed 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -2,7 +2,8 @@ import { isParentOf, - createID + createID, + transact } from '../internals.js' class ReverseOperation { @@ -33,7 +34,7 @@ class ReverseOperation { function applyReverseOperation (y, scope, reverseBuffer) { let performedUndo = false let undoOp = null - y.transact(() => { + transact(y, () => { while (!performedUndo && reverseBuffer.length > 0) { undoOp = reverseBuffer.pop() // make sure that it is possible to iterate {from}-{to} @@ -107,7 +108,6 @@ export class UndoManager { this._lastTransactionWasUndo = false const y = scope._y this.y = y - y._hasUndoManager = true let bindingInfos y.on('beforeTransaction', (y, transaction, remote) => { if (!remote) { diff --git a/src/utils/Y.js b/src/utils/Y.js index e9a4e700..05ad8812 100644 --- a/src/utils/Y.js +++ b/src/utils/Y.js @@ -1,26 +1,18 @@ -import { getStates } from './StructStore.js' - import { - callEventHandlerListeners, - sortAndMergeDeleteSet, StructStore, - findIndexSS, - Transaction, AbstractType, - AbstractItem, YArray, YText, YMap, YXmlFragment, - ItemDeleted, - YEvent, GC, AbstractStruct // eslint-disable-line + transact, + Transaction, YEvent // eslint-disable-line } from '../internals.js' import { Observable } from 'lib0/observable.js' import * as error from 'lib0/error.js' import * as random from 'lib0/random.js' import * as map from 'lib0/map.js' -import * as math from 'lib0/math.js' /** * A Yjs instance handles the state of shared data. @@ -43,7 +35,6 @@ export class Y extends Observable { * @type {Transaction | null} */ this._transaction = null - this._hasUndoManager = false } /** * @type {Transaction} @@ -66,115 +57,7 @@ export class Y extends Observable { * @todo separate this into a separate function */ transact (f) { - let initialCall = false - if (this._transaction === null) { - initialCall = true - this._transaction = new Transaction(this) - this.emit('beforeTransaction', [this, this._transaction]) - } - try { - f(this._transaction) - } finally { - if (initialCall) { - const transaction = this._transaction - this._transaction = null - this.emit('beforeObserverCalls', [this, this._transaction]) - // emit change events on changed types - transaction.changed.forEach((subs, itemtype) => { - itemtype._callObserver(transaction, subs) - }) - transaction.changedParentTypes.forEach((events, type) => { - events = events - .filter(event => - event.target._item === null || !event.target._item.deleted - ) - events - .forEach(event => { - event.currentTarget = type - }) - // we don't need to check for events.length - // because we know it has at least one element - callEventHandlerListeners(type._dEH, [events, transaction]) - }) - // only call afterTransaction listeners if anything changed - transaction.afterState = getStates(transaction.y.store) - // when all changes & events are processed, emit afterTransaction event - // transaction cleanup - const store = transaction.y.store - const ds = transaction.deleteSet - // replace deleted items with ItemDeleted / GC - sortAndMergeDeleteSet(ds) - this.emit('afterTransaction', [this, transaction]) - for (const [client, deleteItems] of ds.clients) { - /** - * @type {Array} - */ - // @ts-ignore - const structs = store.clients.get(client) - for (let di = 0; di < deleteItems.length; di++) { - const deleteItem = deleteItems[di] - for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) { - const struct = structs[si] - if (deleteItem.clock + deleteItem.len < struct.id.clock) { - break - } - if (struct.deleted && struct instanceof AbstractItem && (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted))) { - // check if we can GC - struct.gc(transaction, store) - } - } - } - } - /** - * @param {Array} structs - * @param {number} pos - */ - const tryToMergeWithLeft = (structs, pos) => { - const left = structs[pos - 1] - const right = structs[pos] - if (left.deleted === right.deleted && left.constructor === right.constructor) { - if (left.mergeWith(right)) { - structs.splice(pos, 1) - } - } - } - // on all affected store.clients props, try to merge - for (const [client, clock] of transaction.afterState) { - const beforeClock = transaction.beforeState.get(client) || 0 - if (beforeClock !== clock) { - /** - * @type {Array} - */ - // @ts-ignore - const structs = store.clients.get(client) - // we iterate from right to left so we can safely remove entries - const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1) - for (let i = structs.length - 1; i >= firstChangePos; i--) { - tryToMergeWithLeft(structs, i) - } - } - } - // try to merge replacedItems - for (const replacedItem of transaction._replacedItems) { - const id = replacedItem.id - const client = id.client - const clock = id.clock - /** - * @type {Array} - */ - // @ts-ignore - const structs = store.clients.get(client) - const replacedStructPos = findIndexSS(structs, clock) - if (replacedStructPos + 1 < structs.length) { - tryToMergeWithLeft(structs, replacedStructPos + 1) - } - if (replacedStructPos > 0) { - tryToMergeWithLeft(structs, replacedStructPos) - } - } - this.emit('afterTransactionCleanup', [this, transaction]) - } - } + transact(this, f) } /** * Define a shared data type. diff --git a/src/utils/relativePosition.js b/src/utils/relativePosition.js index cf1c992a..2d511619 100644 --- a/src/utils/relativePosition.js +++ b/src/utils/relativePosition.js @@ -61,8 +61,27 @@ export class RelativePosition { */ this.item = item } + toJSON () { + const json = {} + if (this.type !== null) { + json.type = this.type.toJSON() + } + if (this.tname !== null) { + json.tname = this.tname + } + if (this.item !== null) { + json.item = this.item.toJSON() + } + return json + } } +/** + * @param {Object} json + * @return {RelativePosition} + */ +export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock)) + export class AbsolutePosition { /** * @param {AbstractType} type @@ -175,11 +194,11 @@ export const readRelativePosition = (decoder, y, store) => { /** * @param {RelativePosition} rpos - * @param {StructStore} store * @param {Y} y * @return {AbsolutePosition|null} */ -export const toAbsolutePosition = (rpos, store, y) => { +export const toAbsolutePosition = (rpos, y) => { + const store = y.store const rightID = rpos.item const typeID = rpos.type const tname = rpos.tname @@ -193,7 +212,7 @@ export const toAbsolutePosition = (rpos, store, y) => { if (!(right instanceof AbstractItem)) { return null } - offset = right.deleted ? 0 : rightID.clock - right.id.clock + offset = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock let n = right.left while (n !== null) { if (!n.deleted && n.countable) { @@ -251,9 +270,8 @@ export const toRelativePosition = (apos, y) => { * @param {RelativePosition|null} b */ export const compareRelativePositions = (a, b) => a === b || ( - a !== null && b !== null && ( + a !== null && b !== null && a.tname === b.tname && ( (a.item !== null && b.item !== null && compareIDs(a.item, b.item)) || - (a.tname !== null && a.tname === b.tname) || (a.type !== null && b.type !== null && compareIDs(a.type, b.type)) ) ) diff --git a/tests/testHelper.js b/tests/testHelper.js index ab11a8a7..44b94883 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -381,11 +381,8 @@ export const applyRandomTests = (tc, mods, iterations) => { testConnector.reconnectRandom() } } else if (prng.int31(gen, 0, 100) <= 1) { - // 1% chance to flush all & garbagecollect - // TODO: We do not gc all users as this does not work yet - // await garbageCollectUsers(t, users) + // 1% chance to flush all testConnector.flushAllMessages() - // await users[0].db.emptyGarbageCollector() // TODO: reintroduce GC tests! } else if (prng.int31(gen, 0, 100) <= 50) { // 50% chance to flush a random message testConnector.flushRandomMessage()