diff --git a/src/internals.js b/src/internals.js index 889c050b..c1229bfc 100644 --- a/src/internals.js +++ b/src/internals.js @@ -40,6 +40,5 @@ export * from './structs/ContentJSON.js' export * from './structs/ContentAny.js' export * from './structs/ContentString.js' export * from './structs/ContentType.js' -export * from './structs/ContentLink.js' export * from './structs/Item.js' export * from './structs/Skip.js' diff --git a/src/structs/ContentLink.js b/src/structs/ContentLink.js deleted file mode 100644 index 25d6e6fb..00000000 --- a/src/structs/ContentLink.js +++ /dev/null @@ -1,183 +0,0 @@ -import { decoding, encoding, error } from 'lib0' -import { - UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore, // eslint-disable-line - YWeakLink, - AbstractType, - getItemCleanStart, - createID, - getItemCleanEnd - } from '../internals.js' - - export class ContentLink { - /** - * @param {YWeakLink} link - */ - constructor (link) { - this.link = link - /** - * @type {Item|null} - */ - this._item = null - } - - /** - * @return {number} - */ - getLength () { - return 1 - } - - /** - * @return {Array} - */ - getContent () { - return [this.link] - } - - /** - * @return {boolean} - */ - isCountable () { - return true - } - - /** - * @return {ContentLink} - */ - copy () { - return new ContentLink(this.link) - } - - /** - * @param {number} offset - * @return {ContentLink} - */ - splice (offset) { - throw error.methodUnimplemented() - } - - /** - * @param {ContentLink} right - * @return {boolean} - */ - mergeWith (right) { - return false - } - - /** - * @param {Transaction} transaction - * @param {Item} item - */ - integrate (transaction, item) { - let sourceItem = this.link.item !== null ? this.link.item : getItemCleanStart(transaction, this.link.id) - if (sourceItem.constructor === Item && sourceItem.parentSub !== null) { - // for maps, advance to most recent item - while (sourceItem.right !== null) { - sourceItem = sourceItem.right - } - } - if (!sourceItem.deleted && sourceItem.length > 1) { - sourceItem = getItemCleanEnd(transaction, transaction.doc.store, createID(sourceItem.id.client, sourceItem.id.clock + 1)) - } - this.link.item = sourceItem - this._item = item - if (!sourceItem.deleted) { - const src = /** @type {Item} */ (sourceItem) - if (src.linkedBy === null) { - src.linkedBy = new Set() - } - src.linkedBy.add(item) - } - } - - /** - * @param {Transaction} transaction - */ - delete (transaction) { - if (this._item !== null && this.link !== null && this.link.item !== null && !this.link.item.deleted) { - const item = /** @type {Item} */ (this.link.item) - if (item.linkedBy !== null) { - item.linkedBy.delete(this._item) - } - this.link.item = null - } - } - - /** - * @param {StructStore} store - */ - gc (store) {} - - /** - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder - * @param {number} offset - */ - write (encoder, offset) { - const flags = 0 // flags that could be used in the future - encoding.writeUint8(encoder.restEncoder, flags) - encoder.writeLeftID(this.link.id) - } - - /** - * @return {number} - */ - getRef () { - return 11 - } - } - - /** - * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder - * @return {ContentLink} - */ - export const readContentWeakLink = decoder => { - const flags = decoding.readUint8(decoder.restDecoder) - const id = decoder.readLeftID() - return new ContentLink(new YWeakLink(id, null)) - } - -const lengthExceeded = error.create('Length exceeded!') - -/** - * Returns a {WeakLink} to an YArray element at given index. - * - * @param {Transaction} transaction - * @param {AbstractType} parent - * @param {number} index - * @return {YWeakLink} - */ -export const arrayWeakLink = (transaction, parent, index) => { - let item = parent._start - for (; item !== null; item = item.right) { - if (!item.deleted && item.countable) { - if (index < item.length) { - if (index > 0) { - item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + index)) - } - if (item.length > 1) { - item = getItemCleanEnd(transaction, transaction.doc.store, createID(item.id.client, item.id.clock + 1)) - } - return new YWeakLink(item.id, item) - } - index -= item.length - } - } - - throw lengthExceeded -} - -/** - * Returns a {WeakLink} to an YMap element at given key. - * - * @param {AbstractType} parent - * @param {string} key - * @return {YWeakLink|undefined} - */ -export const mapWeakLink = (parent, key) => { - const item = parent._map.get(key) - if (item !== undefined) { - return new YWeakLink(item.id, item) - } else { - return undefined - } -} \ No newline at end of file diff --git a/src/structs/ContentType.js b/src/structs/ContentType.js index e9c11de1..479ead2c 100644 --- a/src/structs/ContentType.js +++ b/src/structs/ContentType.js @@ -7,7 +7,8 @@ import { readYXmlFragment, readYXmlHook, readYXmlText, - UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line + UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType, // eslint-disable-line + readYWeakLink } from '../internals.js' import * as error from 'lib0/error' @@ -23,7 +24,8 @@ export const typeRefs = [ readYXmlElement, readYXmlFragment, readYXmlHook, - readYXmlText + readYXmlText, + readYWeakLink ] export const YArrayRefID = 0 @@ -33,6 +35,7 @@ export const YXmlElementRefID = 3 export const YXmlFragmentRefID = 4 export const YXmlHookRefID = 5 export const YXmlTextRefID = 6 +export const YWeakLinkRefID = 7 /** * @private @@ -104,6 +107,7 @@ export class ContentType { * @param {Transaction} transaction */ delete (transaction) { + this.type._delete(transaction) // call custom destructor on AbstractType let item = this.type._start while (item !== null) { if (!item.deleted) { diff --git a/src/structs/Item.js b/src/structs/Item.js index f7e8658a..deaea0ae 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -18,15 +18,13 @@ import { readContentString, readContentEmbed, readContentDoc, - readContentWeakLink, createID, readContentFormat, readContentType, addChangedTypeToTransaction, isDeleted, StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction, // eslint-disable-line - YWeakLink, - ContentLink + YWeakLink } from '../internals.js' import * as error from 'lib0/error' @@ -302,7 +300,7 @@ export class Item extends AbstractStruct { * If this item was referenced by other weak links, here we keep the references * to these weak refs. * - * @type {Set | null} + * @type {Set> | null} */ this.linkedBy = null /** @@ -386,10 +384,10 @@ export class Item extends AbstractStruct { 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 } - if (this.content.constructor === ContentLink) { - const content = /** @type {ContentLink} */ (this.content) - if (content.link.id.client !== this.id.client) { - return content.link.id.client + if (this.content.constructor === ContentType && /** @type {ContentType} */ (this.content).type.constructor === YWeakLink) { + const content = /** @type {any} */ (this.content).type + if (content._id.client !== this.id.client) { + return content._id.client } } @@ -540,7 +538,7 @@ export class Item extends AbstractStruct { addChangedTypeToTransaction(transaction, /** @type {AbstractType} */ (this.parent), this.parentSub) if (this.linkedBy !== null) { for (let link of this.linkedBy) { - addChangedTypeToTransaction(transaction, /** @type {AbstractType} */ (link.parent), link.parentSub) + addChangedTypeToTransaction(transaction, link, this.parentSub) } } if ((/** @type {AbstractType} */ (this.parent)._item !== null && /** @type {AbstractType} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) { @@ -647,7 +645,12 @@ export class Item extends AbstractStruct { addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length) addChangedTypeToTransaction(transaction, parent, this.parentSub) this.content.delete(transaction) - this.linkedBy = null + if (this.linkedBy !== null) { + for (let link of this.linkedBy) { + addChangedTypeToTransaction(transaction, link, this.parentSub) + } + this.linkedBy = null + } } } @@ -744,8 +747,7 @@ export const contentRefs = [ readContentType, // 7 readContentAny, // 8 readContentDoc, // 9 - () => { error.unexpectedCase() }, // 10 - Skip is not ItemContent - readContentWeakLink // 11 + () => { error.unexpectedCase() } // 10 - Skip is not ItemContent ] /** diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 472297a6..4d09ff68 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -11,7 +11,7 @@ import { ContentAny, ContentBinary, getItemCleanStart, - ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, YWeakLink, ContentLink, // eslint-disable-line + ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, YWeakLink, // eslint-disable-line } from '../internals.js' import * as map from 'lib0/map' @@ -309,6 +309,11 @@ export class AbstractType { this._item = item } + /** + * @param {Transaction} transaction + */ + _delete (transaction) { } + /** * @return {AbstractType} */ @@ -669,10 +674,6 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c))) left.integrate(transaction, 0) break - case YWeakLink: - left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentLink(/** @type {YWeakLink} */ (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)) @@ -855,9 +856,6 @@ export const typeMapSet = (transaction, parent, key, value) => { case Doc: content = new ContentDoc(/** @type {Doc} */ (value)) break - case YWeakLink: - content = new ContentLink(/** @type {YWeakLink} */ (value)) - break; default: if (value instanceof AbstractType) { content = new ContentType(value) diff --git a/src/types/YWeakLink.js b/src/types/YWeakLink.js index 978d920a..76b319f8 100644 --- a/src/types/YWeakLink.js +++ b/src/types/YWeakLink.js @@ -1,4 +1,13 @@ -import { AbstractType, GC, ID, Item, Transaction, YEvent } from "yjs" +import { decoding, encoding, error } from "lib0" +import { + YEvent, Transaction, ID, GC, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, + transact, + getItemCleanEnd, + createID, + getItemCleanStart, + callTypeObservers, + YWeakLinkRefID +} from "../internals.js" /** * @template T extends AbstractType @@ -9,27 +18,27 @@ export class YWeakLinkEvent extends YEvent { /** * @param {YWeakLink} ylink The YWeakLink to which this event was propagated to. * @param {Transaction} transaction - * @param {YEvent} source Source event that has been propagated to ylink. */ - constructor (ylink, transaction, source) { + constructor (ylink, transaction) { super(ylink, transaction) - this.source = source } } /** * @template T + * @extends AbstractType> * * Weak link to another value stored somewhere in the document. */ -export class YWeakLink { +export class YWeakLink extends AbstractType { /** * @param {ID} id * @param {Item|GC|null} item */ constructor(id, item) { - this.id = id - this.item = item + super() + this._id = id + this._linkedItem = item } /** @@ -38,14 +47,14 @@ export class YWeakLink { * @return {T|undefined} */ deref() { - if (this.item !== null && this.item.constructor === Item) { - let item = this.item + if (this._linkedItem !== null && this._linkedItem.constructor === Item) { + let item = this._linkedItem if (item.parentSub !== null) { // for map types go to the most recent one while (item.right !== null) { item = item.right } - this.item = item + this._linkedItem = item } if (!item.deleted) { return item.content.getContent()[0] @@ -53,4 +62,155 @@ export class YWeakLink { } return undefined; } + + /** + * 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) { + super._integrate(y, item) + if (item !== null) { + transact(y, (transaction) => { + let sourceItem = this._linkedItem !== null ? this._linkedItem : getItemCleanStart(transaction, this._id) + if (sourceItem.constructor === Item && sourceItem.parentSub !== null) { + // for maps, advance to most recent item + while (sourceItem.right !== null) { + sourceItem = sourceItem.right + } + } + if (!sourceItem.deleted && sourceItem.length > 1) { + sourceItem = getItemCleanEnd(transaction, transaction.doc.store, createID(sourceItem.id.client, sourceItem.id.clock + 1)) + } + this._linkedItem = sourceItem + if (!sourceItem.deleted) { + const src = /** @type {Item} */ (sourceItem) + if (src.linkedBy === null) { + src.linkedBy = new Set() + } + src.linkedBy.add(this) + } + }) + } + } + + /** + * @param {Transaction} transaction + */ + _delete (transaction) { + if (this._item !== null && this._linkedItem !== null && !this._linkedItem.deleted) { + const item = /** @type {Item} */ (this._linkedItem) + if (item.linkedBy !== null) { + item.linkedBy.delete(this) + } + this._linkedItem = null + } + } + + /** + * @return {YWeakLink} + */ + _copy () { + return new YWeakLink(this._id, this._linkedItem) + } + + /** + * @return {YWeakLink} + */ + clone () { + return new YWeakLink(this._id, this._linkedItem) + } + + /** + * Creates YWeakLinkEvent and calls observers. + * + * @param {Transaction} transaction + * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. + */ + _callObserver (transaction, parentSubs) { + super._callObserver(transaction, parentSubs) + callTypeObservers(this, transaction, new YWeakLinkEvent(this, transaction)) + } + + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + */ + _write (encoder) { + encoder.writeTypeRef(YWeakLinkRefID) + const flags = 0 // flags that could be used in the future + encoding.writeUint8(encoder.restEncoder, flags) + encoder.writeLeftID(this._id) + } } + + +/** + * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder + * @return {YWeakLink} + */ +export const readYWeakLink = decoder => { + const flags = decoding.readUint8(decoder.restDecoder) + const id = decoder.readLeftID() + return new YWeakLink(id, null) +} + +const lengthExceeded = error.create('Length exceeded!') + +/** + * Returns a {WeakLink} to an YArray element at given index. + * + * @param {Transaction} transaction + * @param {AbstractType} parent + * @param {number} index + * @return {YWeakLink} + */ +export const arrayWeakLink = (transaction, parent, index) => { + let item = parent._start + for (; item !== null; item = item.right) { + if (!item.deleted && item.countable) { + if (index < item.length) { + if (index > 0) { + item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + index)) + } + if (item.length > 1) { + item = getItemCleanEnd(transaction, transaction.doc.store, createID(item.id.client, item.id.clock)) + } + const link = new YWeakLink(item.id, item) + if (item.linkedBy === null) { + item.linkedBy = new Set() + } + item.linkedBy.add(link) + return link + } + index -= item.length + } + } + + throw lengthExceeded +} + +/** + * Returns a {WeakLink} to an YMap element at given key. + * + * @param {AbstractType} parent + * @param {string} key + * @return {YWeakLink|undefined} + */ +export const mapWeakLink = (parent, key) => { + const item = parent._map.get(key) + if (item !== undefined) { + const link = new YWeakLink(item.id, item) + if (item.linkedBy === null) { + item.linkedBy = new Set() + } + item.linkedBy.add(link) + return link + } else { + return undefined + } +} \ No newline at end of file diff --git a/tests/y-weak-links.tests.js b/tests/y-weak-links.tests.js index df7d8b91..83f8955d 100644 --- a/tests/y-weak-links.tests.js +++ b/tests/y-weak-links.tests.js @@ -118,55 +118,69 @@ export const testDeleteSource = tc => { /** * @param {t.TestCase} tc */ -export const testObserveMapLinkArrayRemove = tc => { - const doc = new Y.Doc() - const map = doc.getMap('map') - const array = doc.getArray('array') - - array.insert(0, [1]) - const link = array.link(0) - map.set('key', link) +export const testObserveMapUpdate = tc => { + const { testConnector, users, map0, map1 } = init(tc, { users: 2 }) + map0.set('a', 'value') + const link0 = /** @type {Y.WeakLink} */ (map0.link('a')) /** * @type {any} */ - let keys = null - map.observe((e) => { - console.log('map received event', e) - keys = e.keys - }) + let target0 + link0.observe((e) => target0 = e.target) + map0.set('b', link0) - array.delete(0) + testConnector.flushAllMessages() - t.compare(keys.get('key'), { action:'delete', oldValue: 1, newValue: null }) -} - -/** - * @param {t.TestCase} tc - */ -export const testObserveMapLinkMapUpdate = tc => { - const doc = new Y.Doc() - const map1 = doc.getMap('map1') - const map2 = doc.getMap('map2') + let link1 = /** @type {Y.WeakLink} */ (map1.get('b')) + t.compare(link1.deref(), 'value') /** - * @type {Map} + * @type {any} */ - let keys - map1.observe((e) => keys = e.keys) + let target1 + link1.observe((e) => target1 = e.target) - map2.set('key', 'value1') - const link = map2.link('key') - map1.set('other-key', link) + map0.set('a', 'value2') + t.compare(target0.deref(), 'value2') - keys = /** @type {any} */ (null) - map2.set('key', 'value2') - - t.compare(keys.get('key'), { action:'update', oldValue: 'value1', newValue: 'value2' }) + testConnector.flushAllMessages() + t.compare(target1.deref(), 'value2') } /** * @param {t.TestCase} tc */ -export const testObserveMapLinkMapRemove = tc => { +export const testObserveMapDelete = tc => { + const { testConnector, users, map0, map1 } = init(tc, { users: 2 }) + map0.set('a', 'value') + const link0 = /** @type {Y.WeakLink} */ (map0.link('a')) + /** + * @type {any} + */ + let target0 + link0.observe((e) => target0 = e.target) + map0.set('b', link0) + + testConnector.flushAllMessages() + + let link1 = /** @type {Y.WeakLink} */ (map1.get('b')) + t.compare(link1.deref(), 'value') + /** + * @type {any} + */ + let target1 + link1.observe((e) => target1 = e.target) + + map0.delete('a') + t.compare(target0.deref(), undefined) + + testConnector.flushAllMessages() + t.compare(target1.deref(), undefined) +} + +/** + * @param {t.TestCase} tc + */ + const testObserveMapLinkMapRemove = tc => { const doc = new Y.Doc() const map1 = doc.getMap('map1') const map2 = doc.getMap('map2') @@ -189,7 +203,7 @@ export const testObserveMapLinkMapRemove = tc => { /** * @param {t.TestCase} tc */ -export const testObserveArrayLinkMapRemove = tc => { + const testObserveArrayLinkMapRemove = tc => { const doc = new Y.Doc() const array = doc.getArray('array') const map = doc.getMap('map') @@ -212,7 +226,7 @@ export const testObserveArrayLinkMapRemove = tc => { /** * @param {t.TestCase} tc */ -export const testObserveArrayLinkMapUpdate = tc => { + const testObserveArrayLinkMapUpdate = tc => { const doc = new Y.Doc() const array = doc.getArray('array') const map = doc.getMap('map') @@ -235,7 +249,7 @@ export const testObserveArrayLinkMapUpdate = tc => { /** * @param {t.TestCase} tc */ -export const testObserveTransitive = tc => { + const testObserveTransitive = tc => { // test observers in a face of linked chains of values const doc = new Y.Doc() const map1 = doc.getMap('map1') @@ -262,7 +276,7 @@ export const testObserveTransitive = tc => { /** * @param {t.TestCase} tc */ -export const testDeepObserveMap = tc => { + const testDeepObserveMap = tc => { // test observers in a face of linked chains of values const doc = new Y.Doc() const map = doc.getMap('map') @@ -297,7 +311,7 @@ export const testDeepObserveMap = tc => { /** * @param {t.TestCase} tc */ -export const testDeepObserveArray = tc => { + const testDeepObserveArray = tc => { // test observers in a face of linked chains of values const doc = new Y.Doc() const map = doc.getMap('map') @@ -332,7 +346,7 @@ export const testDeepObserveArray = tc => { /** * @param {t.TestCase} tc */ -export const testDeepObserveRecursive = tc => { + const testDeepObserveRecursive = tc => { // test observers in a face of linked chains of values const doc = new Y.Doc() const root = doc.getArray('array')