reimplemented WeakLink as an AbstractType

This commit is contained in:
Bartosz Sypytkowski 2023-06-09 15:55:25 +02:00
parent 6f9db68f9a
commit e50db9e123
7 changed files with 251 additions and 257 deletions

View File

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

View File

@ -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<any>} link
*/
constructor (link) {
this.link = link
/**
* @type {Item|null}
*/
this._item = null
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
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<any>} parent
* @param {number} index
* @return {YWeakLink<any>}
*/
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<any>} parent
* @param {string} key
* @return {YWeakLink<any>|undefined}
*/
export const mapWeakLink = (parent, key) => {
const item = parent._map.get(key)
if (item !== undefined) {
return new YWeakLink(item.id, item)
} else {
return undefined
}
}

View File

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

View File

@ -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<Item> | null}
* @type {Set<YWeakLink<any>> | 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<any>} */ (this.parent), this.parentSub)
if (this.linkedBy !== null) {
for (let link of this.linkedBy) {
addChangedTypeToTransaction(transaction, /** @type {AbstractType<any>} */ (link.parent), link.parentSub)
addChangedTypeToTransaction(transaction, link, this.parentSub)
}
}
if ((/** @type {AbstractType<any>} */ (this.parent)._item !== null && /** @type {AbstractType<any>} */ (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
]
/**

View File

@ -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<EventType>}
*/
@ -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<any>} */ (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<any>} */ (value))
break;
default:
if (value instanceof AbstractType) {
content = new ContentType(value)

View File

@ -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<any>
@ -9,27 +18,27 @@ export class YWeakLinkEvent extends YEvent {
/**
* @param {YWeakLink<T>} ylink The YWeakLink to which this event was propagated to.
* @param {Transaction} transaction
* @param {YEvent<any>} 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<YWeakLinkEvent<T>>
*
* 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<T>}
*/
_copy () {
return new YWeakLink(this._id, this._linkedItem)
}
/**
* @return {YWeakLink<T>}
*/
clone () {
return new YWeakLink(this._id, this._linkedItem)
}
/**
* Creates YWeakLinkEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} 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<any>}
*/
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<any>} parent
* @param {number} index
* @return {YWeakLink<any>}
*/
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<any>} parent
* @param {string} key
* @return {YWeakLink<any>|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
}
}

View File

@ -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<String>} */ (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<String>} */ (map1.get('b'))
t.compare(link1.deref(), 'value')
/**
* @type {Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
* @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<String>} */ (map0.link('a'))
/**
* @type {any}
*/
let target0
link0.observe((e) => target0 = e.target)
map0.set('b', link0)
testConnector.flushAllMessages()
let link1 = /** @type {Y.WeakLink<String>} */ (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')