From c7539b028a981bc7f041cb4557cce80762642ba2 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Mon, 29 May 2023 10:17:38 +0200 Subject: [PATCH] draft for WeakLink API --- src/index.js | 1 + src/internals.js | 2 + src/structs/ContentLink.js | 99 +++++++++++++++++++++ src/structs/Item.js | 2 + src/types/WeakLink.js | 17 ++++ src/types/YArray.js | 13 ++- src/types/YMap.js | 13 ++- tests/index.js | 3 +- tests/weakLinks.tests.js | 176 +++++++++++++++++++++++++++++++++++++ 9 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 src/structs/ContentLink.js create mode 100644 src/types/WeakLink.js create mode 100644 tests/weakLinks.tests.js diff --git a/src/index.js b/src/index.js index d23c37f5..3d9c535d 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ export { YXmlHook as XmlHook, YXmlElement as XmlElement, YXmlFragment as XmlFragment, + WeakLink, YXmlEvent, YMapEvent, YArrayEvent, diff --git a/src/internals.js b/src/internals.js index bc386f0a..4861c591 100644 --- a/src/internals.js +++ b/src/internals.js @@ -27,6 +27,7 @@ export * from './types/YXmlElement.js' export * from './types/YXmlEvent.js' export * from './types/YXmlHook.js' export * from './types/YXmlText.js' +export * from './types/WeakLink.js' export * from './structs/AbstractStruct.js' export * from './structs/GC.js' @@ -39,5 +40,6 @@ 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 new file mode 100644 index 00000000..6ec2170e --- /dev/null +++ b/src/structs/ContentLink.js @@ -0,0 +1,99 @@ +import { + UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line + } from '../internals.js' + + export class ContentLink { + /** + * @param {Item} item + */ + constructor (item) { + /** + * @type {Item} + */ + this.item = item + } + + /** + * @return {number} + */ + getLength () { + return this.item.length + } + + /** + * @return {Array} + */ + getContent () { + throw new Error('not implemented') + } + + /** + * @return {boolean} + */ + isCountable () { + return true + } + + /** + * @return {ContentLink} + */ + copy () { + return new ContentLink(this.item) + } + + /** + * @param {number} offset + * @return {ContentLink} + */ + splice (offset) { + throw new Error('not implemented') + } + + /** + * @param {ContentLink} right + * @return {boolean} + */ + mergeWith (right) { + throw new Error('not implemented') + } + + /** + * @param {Transaction} transaction + * @param {Item} item + */ + integrate (transaction, item) {} + + /** + * @param {Transaction} transaction + */ + delete (transaction) {} + + /** + * @param {StructStore} store + */ + gc (store) {} + + /** + * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder + * @param {number} offset + */ + write (encoder, offset) { + throw new Error('not implemented') + } + + /** + * @return {number} + */ + getRef () { + return 10 + } + } + + /** + * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder + * @return {ContentLink} + */ + export const readContentWeakLink = decoder => { + throw new Error('not implemented') + } + \ No newline at end of file diff --git a/src/structs/Item.js b/src/structs/Item.js index 7e1bc92c..e696e753 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -18,6 +18,7 @@ import { readContentString, readContentEmbed, readContentDoc, + readContentWeakLink, createID, readContentFormat, readContentType, @@ -719,6 +720,7 @@ export const contentRefs = [ readContentType, // 7 readContentAny, // 8 readContentDoc, // 9 + readContentWeakLink, // 10 () => { error.unexpectedCase() } // 10 - Skip is not ItemContent ] diff --git a/src/types/WeakLink.js b/src/types/WeakLink.js new file mode 100644 index 00000000..c6ef6e60 --- /dev/null +++ b/src/types/WeakLink.js @@ -0,0 +1,17 @@ + +/** + * @template T + * + * Weak link to another value stored somewhere in the document. + */ +export class WeakLink { + + /** + * Returns a reference to an underlying value existing somewhere on in the document. + * + * @return {T|undefined} + */ + deref() { + throw new Error('not implemented') + } +} \ No newline at end of file diff --git a/src/types/YArray.js b/src/types/YArray.js index a895274e..59bf0901 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -16,7 +16,8 @@ import { YArrayRefID, callTypeObservers, transact, - ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line + ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line + WeakLink } from '../internals.js' import { typeListSlice } from './AbstractType.js' @@ -201,6 +202,16 @@ export class YArray extends AbstractType { return typeListGet(this, index) } + /** + * Returns the weak link to i-th element from a YArray. + * + * @param {number} index The index of the element to return from the YArray + * @return {WeakLink} + */ + link(index) { + throw new Error('Method not implemented.') + } + /** * Transforms this YArray to a JavaScript Array. * diff --git a/src/types/YMap.js b/src/types/YMap.js index e2dd7a49..b4895792 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -14,7 +14,8 @@ import { YMapRefID, callTypeObservers, transact, - UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line + UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line + WeakLink } from '../internals.js' import * as iterator from 'lib0/iterator' @@ -233,6 +234,16 @@ export class YMap extends AbstractType { return /** @type {any} */ (typeMapGet(this, key)) } + /** + * Returns a weak reference link to another element stored in the same document. + * + * @param {string} key + * @return {WeakLink|undefined} + */ + link(key) { + throw new Error('Method not implemented.') + } + /** * Returns a boolean indicating whether the specified key exists or not. * diff --git a/tests/index.js b/tests/index.js index ec22ed05..945452fe 100644 --- a/tests/index.js +++ b/tests/index.js @@ -11,6 +11,7 @@ import * as doc from './doc.tests.js' import * as snapshot from './snapshot.tests.js' import * as updates from './updates.tests.js' import * as relativePositions from './relativePositions.tests.js' +import * as weakLinks from './weakLinks.tests.js' import { runTests } from 'lib0/testing' import { isBrowser, isNode } from 'lib0/environment' @@ -20,7 +21,7 @@ if (isBrowser) { log.createVConsole(document.body) } runTests({ - doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions + doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions, weakLinks }).then(success => { /* istanbul ignore next */ if (isNode) { diff --git a/tests/weakLinks.tests.js b/tests/weakLinks.tests.js new file mode 100644 index 00000000..cb412dc2 --- /dev/null +++ b/tests/weakLinks.tests.js @@ -0,0 +1,176 @@ +import * as Y from '../src/index.js' +import * as t from 'lib0/testing' +import { init, compare } from './testHelper.js' + +/** + * @param {t.TestCase} tc + */ +export const testBasic = tc => { + const doc = new Y.Doc() + const map = doc.getMap('map') + + const nested = new Y.Map() + nested.set('a1', 'hello') + map.set('a', nested) + const link = nested.link('a') + map.set('b', link) + + const nested2 = map.get('b') + t.compare(nested2.toJSON(), nested.toJSON()) +} + +/** + * @param {t.TestCase} tc + */ +export const testUpdate = tc => { + const { testConnector, users, map0, map1 } = init(tc, { users: 2 }) + map0.set('a', new Y.Map([['a1', 'hello']])) + const link0 = /** @type {Y.WeakLink>} */ (map0.link('a')) + map0.set('b', link0) + + testConnector.flushAllMessages() + const link1 = /** @type {Y.WeakLink>} */ (map1.get('b')) + let l1 = /** @type {Y.Map} */ (link1.deref()) + let l0 = /** @type {Y.Map} */ (link0.deref()) + t.compare(l1.get('a1'), l0.get('a1')) + + map1.get('a').set('a2', 'world') + + testConnector.flushAllMessages() + + l1 = /** @type {Y.Map} */ (link1.deref()) + l0 = /** @type {Y.Map} */ (link0.deref()) + t.compare(l1.get('a2'), l0.get('a2')) + + compare(users) +} + +/** + * @param {t.TestCase} tc + */ +export const testDeleteWeakLink = tc => { + const { testConnector, users, map0, map1 } = init(tc, { users: 2 }) + map0.set('a', new Y.Map([['a1', 'hello']])) + const link0 = /** @type {Y.WeakLink>} */ (map0.link('a')) + map0.set('b', link0) + + testConnector.flushAllMessages() + const link1 = /** @type {Y.WeakLink} */ map1.get('b') + const l0 = /** @type {Y.Map} */ (link0.deref()) + t.compare(link1.ref.get('a1'), l0.get('a1')) + + map1.delete('b') // delete links + + testConnector.flushAllMessages() + + // since links have been deleted, they no longer refer to any content + t.compare(link0.deref(), undefined) + t.compare(link1.deref(), undefined) + + compare(users) +} + +/** + * @param {t.TestCase} tc + */ +export const testDeleteSource = tc => { + const { testConnector, users, map0, map1 } = init(tc, { users: 2 }) + map0.set('a', new Y.Map([['a1', 'hello']])) + const link0 = /** @type {Y.WeakLink>} */ (map0.link('a')) + map0.set('b', link0) + + testConnector.flushAllMessages() + const link1 = /** @type {Y.WeakLink>} */ (map1.get('b')) + let l1 = /** @type {Y.Map} */ (link1.deref()) + let l0 = /** @type {Y.Map} */ (link0.deref()) + t.compare(l1.get('a1'), l0.get('a1')) + + map1.delete('a') // delete source of the link + + testConnector.flushAllMessages() + + // since source have been deleted, links no longer refer to any content + t.compare(link0.deref(), undefined) + t.compare(link1.deref(), undefined) + + compare(users) +} + +/** + * @param {t.TestCase} tc + */ +export const testObserve = tc => { + const doc = new Y.Doc() + const map = doc.getMap('map') + const array = doc.getArray('array') + /** + * @type {Array} + */ + let delta + array.observe((e) => delta = e.changes.delta) + + map.set('key', 'value1') + const link = map.link('key') + array.insert(0, [link]) + + delta = [] + + map.set('key', 'value2') + t.compare(delta, [{ delete: 1 }, { insert: 'value2' }]) +} + +/** + * @param {t.TestCase} tc + */ +export const testDeepObserve = tc => { + const doc = new Y.Doc() + const map = doc.getMap('map') + const array = doc.getArray('array') + /** + * @type {Array} + */ + let events + array.observeDeep((e) => events = e) + + const nested = new Y.Map([['key', 'value']]) + map.set('key', nested) + const link = map.link('key') + array.insert(0, [link]) + + events = [] + + nested.set('key', 'value2') + for (let i = 0; i < events.length; i++) { + let e = events[i] + throw new Error('todo') + } +} + +/** + * @param {t.TestCase} tc + */ +export const testObserveRecursive = tc => { + const doc = new Y.Doc() + const map = doc.getMap('map') + const array = doc.getArray('array') + /** + * @type {any} + */ + let arrayChanges + array.observe((e) => arrayChanges = e.changes) + /** + * @type {any} + */ + let mapChanges + map.observe((e) => mapChanges = e.changes) + + Y.transact(doc, () => { + map.set('key', 'map-value') + array.insert(0, [map.link('key')]) + map.set('key2', array.link(0)) + }) + t.compare(arrayChanges.delta, [{ insert: 'map-value' }]) + t.compare(mapChanges.keys.get('key2'), [{ action: 'insert', oldValue: 'map-value' }]) + + t.compare(map.get('key2').deref().deref(), 'map-value') +} \ No newline at end of file