prevent infinite loops when deepObserve is called over links with circular references

This commit is contained in:
Bartosz Sypytkowski 2023-06-15 09:50:06 +02:00
parent cc3ca51d6a
commit 6e95c19b3e
4 changed files with 39 additions and 28 deletions

View File

@ -1,4 +1,5 @@
import { WeakLink } from 'yjs'
import {
readYArray,
readYMap,
@ -107,7 +108,19 @@ export class ContentType {
* @param {Transaction} transaction
*/
delete (transaction) {
this.type._delete(transaction) // call custom destructor on AbstractType
if (this.type.constructor === WeakLink) {
// when removing weak links, remove references to them
// from type they're pointing to
const type = /** @type {WeakLink<any>} */ (this.type);
if (type._linkedItem !== null && !type._linkedItem.deleted) {
const item = /** @type {Item} */ (type._linkedItem)
if (item.linkedBy !== null) {
item.linkedBy.delete(type)
}
type._linkedItem = null
}
}
let item = this.type._start
while (item !== null) {
if (!item.deleted) {

View File

@ -233,8 +233,9 @@ export const getTypeChildren = t => {
* @param {AbstractType<EventType>} type
* @param {Transaction} transaction
* @param {EventType} event
* @param {Set<YWeakLink<any>>|null} visitedLinks
*/
export const callTypeObservers = (type, transaction, event) => {
export const callTypeObservers = (type, transaction, event, visitedLinks = null) => {
const changedType = type
const changedParentTypes = transaction.changedParentTypes
while (true) {
@ -243,9 +244,15 @@ export const callTypeObservers = (type, transaction, event) => {
if (type._item === null) {
break
} else if (type._item.linkedBy !== null) {
if (visitedLinks === null) {
visitedLinks = new Set()
}
for (let link of type._item.linkedBy) {
// recursive call
callTypeObservers(link, transaction, /** @type {any} */ (event))
if (!visitedLinks.has(link)) {
visitedLinks.add(link)
// recursive call
callTypeObservers(link, transaction, /** @type {any} */ (event), visitedLinks)
}
}
}
type = /** @type {AbstractType<any>} */ (type._item.parent)
@ -314,11 +321,6 @@ export class AbstractType {
this._item = item
}
/**
* @param {Transaction} transaction
*/
_delete (transaction) { }
/**
* @return {AbstractType<EventType>}
*/

View File

@ -98,19 +98,6 @@ export class YWeakLink extends AbstractType {
})
}
}
/**
* @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>}

View File

@ -347,7 +347,7 @@ export const testDeepObserveArray = tc => {
/**
* @param {t.TestCase} tc
*/
const testDeepObserveRecursive = tc => {
export const testDeepObserveRecursive = tc => {
// test observers in a face of linked chains of values
const doc = new Y.Doc()
const root = doc.getArray('array')
@ -365,9 +365,9 @@ export const testDeepObserveArray = tc => {
const l2 = root.link(2)
// create cyclic reference between links
m0.set('k1', m1)
m1.set('k2', m2)
m2.set('k0', m0)
m0.set('k1', l1)
m1.set('k2', l2)
m2.set('k0', l0)
/**
* @type {Array<any>}
@ -376,9 +376,18 @@ export const testDeepObserveArray = tc => {
m0.observeDeep((e) => events = e)
m1.set('test-key1', 'value1')
t.compare(events, [{}]) //TODO
t.compare(events.length, 1)
t.compare(events[0].target, m1)
t.compare(events[0].keys, new Map([['test-key1', {action:'add', oldValue: undefined}]]))
events = []
m2.set('test-key2', 'value2')
t.compare(events, [{}]) //TODO
t.compare(events.length, 1)
t.compare(events[0].target, m2)
t.compare(events[0].keys, new Map([['test-key2', {action:'add', oldValue: undefined}]]))
m1.delete('test-key1')
t.compare(events.length, 1)
t.compare(events[0].target, m1)
t.compare(events[0].keys, new Map([['test-key1', {action:'delete', oldValue: undefined}]]))
}