draft for WeakLink API

This commit is contained in:
Bartosz Sypytkowski 2023-05-29 10:17:38 +02:00
parent eeae74decf
commit 740598cef2
9 changed files with 323 additions and 3 deletions

View File

@ -10,6 +10,7 @@ export {
YXmlHook as XmlHook,
YXmlElement as XmlElement,
YXmlFragment as XmlFragment,
WeakLink,
YXmlEvent,
YMapEvent,
YArrayEvent,

View File

@ -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'

View File

@ -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<any>}
*/
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')
}

View File

@ -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
]

17
src/types/WeakLink.js Normal file
View File

@ -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')
}
}

View File

@ -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<T>}
*/
link(index) {
throw new Error('Method not implemented.')
}
/**
* Transforms this YArray to a JavaScript Array.
*

View File

@ -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<MapType>|undefined}
*/
link(key) {
throw new Error('Method not implemented.')
}
/**
* Returns a boolean indicating whether the specified key exists or not.
*

View File

@ -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) {

176
tests/weakLinks.tests.js Normal file
View File

@ -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<Y.Map<any>>} */ (map0.link('a'))
map0.set('b', link0)
testConnector.flushAllMessages()
const link1 = /** @type {Y.WeakLink<Y.Map<any>>} */ (map1.get('b'))
let l1 = /** @type {Y.Map<any>} */ (link1.deref())
let l0 = /** @type {Y.Map<any>} */ (link0.deref())
t.compare(l1.get('a1'), l0.get('a1'))
map1.get('a').set('a2', 'world')
testConnector.flushAllMessages()
l1 = /** @type {Y.Map<any>} */ (link1.deref())
l0 = /** @type {Y.Map<any>} */ (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<Y.Map<any>>} */ (map0.link('a'))
map0.set('b', link0)
testConnector.flushAllMessages()
const link1 = /** @type {Y.WeakLink<Y.Map>} */ map1.get('b')
const l0 = /** @type {Y.Map<any>} */ (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<Y.Map<any>>} */ (map0.link('a'))
map0.set('b', link0)
testConnector.flushAllMessages()
const link1 = /** @type {Y.WeakLink<Y.Map<any>>} */ (map1.get('b'))
let l1 = /** @type {Y.Map<any>} */ (link1.deref())
let l0 = /** @type {Y.Map<any>} */ (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<any>}
*/
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<any>}
*/
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')
}