implement createAttributionsManagerFromDiff that automatically handles gc

This commit is contained in:
Kevin Jahns
2025-04-29 22:42:56 +02:00
parent 1722c8a36f
commit 527e382f8a
10 changed files with 140 additions and 21 deletions

View File

@@ -104,7 +104,7 @@ export {
createDeleteSetFromStructStore,
IdMap,
createIdMap,
createAttribution,
createAttributionItem,
createInsertionSetFromStructStore,
diffIdMap,
diffIdSet,
@@ -112,7 +112,8 @@ export {
encodeIdMap,
createIdMapFromIdSet,
TwosetAttributionManager,
noAttributionsManager
noAttributionsManager,
createAttributionManagerFromDiff
} from './internals.js'
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'

View File

@@ -10,7 +10,7 @@ import {
ContentAny,
ContentBinary,
getItemCleanStart,
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttrs, AbstractAttributionManager, // eslint-disable-line
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttributionItems, AbstractAttributionManager, // eslint-disable-line
} from '../internals.js'
import * as delta from '../utils/Delta.js'
@@ -518,7 +518,7 @@ export const typeListGetContent = (type, am) => {
}
for (let i = 0; i < cs.length; i++) {
const { content, deleted, attrs } = cs[i]
const attribution = createAttributionFromAttrs(attrs, deleted)
const attribution = createAttributionFromAttributionItems(attrs, deleted)
d.insert(content.getContent(), null, attribution)
}
}
@@ -1006,7 +1006,7 @@ export const typeMapGetContent = (parent, am) => {
am.readContent(cs, item)
const { deleted, attrs, content } = cs[cs.length - 1]
const c = array.last(content.getContent())
const attribution = createAttributionFromAttrs(attrs, deleted)
const attribution = createAttributionFromAttributionItems(attrs, deleted)
if (deleted) {
mapcontent[key] = { prevValue: c, value: undefined, attribution }
} else {

View File

@@ -27,7 +27,7 @@ import {
ContentType,
warnPrematureAccess,
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, // eslint-disable-line
createAttributionFromAttrs
createAttributionFromAttributionItems
} from '../internals.js'
import * as delta from '../utils/Delta.js'
@@ -1038,7 +1038,7 @@ export class YText extends AbstractType {
}
for (let i = 0; i < cs.length; i++) {
const { content, deleted, attrs } = cs[i]
const attribution = createAttributionFromAttrs(attrs, deleted)
const attribution = createAttributionFromAttributionItems(attrs, deleted)
switch (content.constructor) {
case ContentString: {
d.insert(/** @type {ContentString} */ (content).str, null, attribution)

View File

@@ -254,7 +254,7 @@ export class YXmlElement extends YXmlFragment {
getContent (am = noAttributionsManager) {
const attributes = typeMapGetContent(this, am)
const { children } = super.getContent(am)
return { children, attributes }
return { nodeName: this.nodeName, children, attributes }
}
/**

View File

@@ -1,5 +1,12 @@
import {
Item, AbstractContent, IdMap // eslint-disable-line
getItem,
diffIdSet,
createInsertionSetFromStructStore,
createDeleteSetFromStructStore,
createIdMapFromIdSet,
ContentDeleted,
Doc, Item, AbstractContent, IdMap, // eslint-disable-line
findIndexCleanStart
} from '../internals.js'
import * as error from 'lib0/error'
@@ -21,7 +28,7 @@ import * as error from 'lib0/error'
* @param {boolean} deleted - whether the attributed item is deleted
* @return {Attribution?}
*/
export const createAttributionFromAttrs = (attrs, deleted) => {
export const createAttributionFromAttributionItems = (attrs, deleted) => {
/**
* @type {Attribution?}
*/
@@ -84,8 +91,6 @@ export class AbstractAttributionManager {
}
/**
* Abstract class for associating Attributions to content / changes
*
* @implements AbstractAttributionManager
*/
export class TwosetAttributionManager {
@@ -136,3 +141,71 @@ export class NoAttributionsManager {
}
export const noAttributionsManager = new NoAttributionsManager()
/**
* @implements AbstractAttributionManager
*/
export class DiffAttributionManager {
/**
* @param {IdMap<any>} inserts
* @param {IdMap<any>} deletes
* @param {Doc} prevDoc
* @param {Doc} nextDoc
*/
constructor (inserts, deletes, prevDoc, nextDoc) {
this.inserts = inserts
this.deletes = deletes
this._prevDocStore = prevDoc.store
this._nextDoc = nextDoc
}
/**
* @param {Array<AttributedContent<any>>} contents
* @param {Item} item
*/
readContent (contents, item) {
const deleted = item.deleted || /** @type {any} */ (item.parent).doc !== this._nextDoc
const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length)
let content = slice.length === 1 ? item.content : item.content.copy()
if (content instanceof ContentDeleted && slice[0].attrs != null) {
// Retrieved item is never more fragmented than the newer item.
const prevItem = getItem(this._prevDocStore, item.id)
content = prevItem.length > 1 ? prevItem.content.copy() : prevItem.content
// trim itemContent to the correct size.
const diffStart = prevItem.id.clock - item.id.clock
const diffEnd = prevItem.id.clock + prevItem.length - item.id.clock - item.length
if (diffStart > 0) {
content = content.splice(diffStart)
}
if (diffEnd > 0) {
content.splice(content.getLength() - diffEnd)
}
}
slice.forEach(s => {
const c = content
if (s.len < c.getLength()) {
content = c.splice(s.len)
}
if (!deleted || s.attrs != null) {
contents.push(new AttributedContent(c, deleted, s.attrs))
}
})
}
}
/**
* Attribute changes from ydoc1 to ydoc2.
*
* @param {Doc} prevDoc
* @param {Doc} nextDoc
*/
export const createAttributionManagerFromDiff = (prevDoc, nextDoc) => {
const inserts = diffIdSet(createInsertionSetFromStructStore(nextDoc.store), createInsertionSetFromStructStore(prevDoc.store))
const deletes = diffIdSet(createDeleteSetFromStructStore(nextDoc.store), createDeleteSetFromStructStore(prevDoc.store))
const insertMap = createIdMapFromIdSet(inserts, [])
const deleteMap = createIdMapFromIdSet(deletes, [])
// @todo, get deletes from the older doc
return new DiffAttributionManager(insertMap, deleteMap, prevDoc, nextDoc)
}

View File

@@ -49,7 +49,7 @@ const _hashAttribution = attr => {
* @param {V} val
* @return {AttributionItem<V>}
*/
export const createAttribution = (name, val) => new AttributionItem(name, val)
export const createAttributionItem = (name, val) => new AttributionItem(name, val)
/**
* @template T

View File

@@ -312,11 +312,11 @@ export const _diffSet = (set, exclude) => {
}
/**
* Remove all ranges from `exclude` from `ds`. The result is a fresh IdSet containing all ranges from `idSet` that are not
* Remove all ranges from `exclude` from `idSet`. The result is a fresh IdSet containing all ranges from `idSet` that are not
* in `exclude`.
*
* @template {IdSet} Set
* @param {Set} set
* @param {Set} idSet
* @param {IdSet | IdMap<any>} exclude
* @return {Set}
*/

View File

@@ -1,6 +1,6 @@
import * as t from 'lib0/testing'
import * as idmap from '../src/utils/IdMap.js'
import { compareIdmaps, createIdMap, ID, createRandomIdSet, createRandomIdMap, createAttribution } from './testHelper.js'
import { compareIdmaps, createIdMap, ID, createRandomIdSet, createRandomIdMap, createAttributionItem } from './testHelper.js'
import * as YY from '../src/internals.js'
/**
@@ -10,7 +10,7 @@ import * as YY from '../src/internals.js'
const simpleConstructAttrs = ops => {
const attrs = createIdMap()
ops.forEach(op => {
attrs.add(op[0], op[1], op[2], op[3].map(v => createAttribution('', v)))
attrs.add(op[0], op[1], op[2], op[3].map(v => createAttributionItem('', v)))
})
return attrs
}

View File

@@ -436,7 +436,7 @@ export const createRandomIdMap = (gen, clients, clockRange, attrChoices) => {
attrs.push(a)
}
}
idMap.add(client, clockStart, len, attrs.map(v => Y.createAttribution('', v)))
idMap.add(client, clockStart, len, attrs.map(v => Y.createAttributionItem('', v)))
}
t.info(`Created IdMap with ${numOfOps} ranges and ${attrChoices.length} different attributes. Encoded size: ${encodeIdMap(idMap).byteLength}`)
return idMap

View File

@@ -297,3 +297,48 @@ export const testElementAttributedContent = _tc => {
})
})
}
/**
* @param {t.TestCase} _tc
*/
export const testElementAttributedContentViaDiffer = _tc => {
const ydocV1 = new Y.Doc()
ydocV1.getXmlElement('p').insert(0, [new Y.XmlText('hello'), new Y.XmlElement('span')])
const ydoc = new Y.Doc()
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(ydocV1))
const yelement = ydoc.getXmlElement('p')
const elem1 = yelement.get(0) // new Y.XmlText('hello')
const elem2 = yelement.get(1) // new Y.XmlElement('span')
const elem3 = new Y.XmlText('world')
t.group('insert / delete', () => {
ydoc.transact(() => {
yelement.delete(0, 1)
yelement.insert(1, [elem3])
yelement.setAttribute('key', '42')
})
const attributionManager = Y.createAttributionManagerFromDiff(ydocV1, ydoc)
const expectedContent = delta.createArrayDelta().insert([delta.createTextDelta().insert('hello', null, { delete: [] })], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.createTextDelta().insert('world', null, { insert: [] })], null, { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', attributedContent.children.toJSON().ops)
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes, { key: { prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.group('test getContentDeep', () => {
const expectedContent = delta.createArrayDelta().insert(
[delta.createTextDelta().insert('hello', null, { delete: [] })],
null,
{ delete: [] }
).insert([{ nodeName: 'span', children: delta.createArrayDelta(), attributes: {} }])
.insert([
delta.createTextDelta().insert('world', null, { insert: [] })
], null, { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.children.toJSON().ops, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON().ops, null, 2))
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes, { key: { prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.assert(attributedContent.nodeName === 'UNDEFINED')
})
})
}