mirror of
https://github.com/yjs/yjs.git
synced 2025-12-16 03:37:50 +01:00
implement createAttributionsManagerFromDiff that automatically handles gc
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -284,9 +284,54 @@ export const testElementAttributedContent = _tc => {
|
||||
null,
|
||||
{ delete: [] }
|
||||
).insert([{ nodeName: 'span', children: delta.createArrayDelta(), attributes: {} }])
|
||||
.insert([
|
||||
delta.createTextDelta().insert('world', null, { insert: [] })
|
||||
], null, { insert: [] })
|
||||
.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')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @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))
|
||||
|
||||
Reference in New Issue
Block a user