mirror of
https://github.com/yjs/yjs.git
synced 2025-12-16 19:57:45 +01:00
implement createAttributionsManagerFromDiff that automatically handles gc
This commit is contained in:
@@ -104,7 +104,7 @@ export {
|
|||||||
createDeleteSetFromStructStore,
|
createDeleteSetFromStructStore,
|
||||||
IdMap,
|
IdMap,
|
||||||
createIdMap,
|
createIdMap,
|
||||||
createAttribution,
|
createAttributionItem,
|
||||||
createInsertionSetFromStructStore,
|
createInsertionSetFromStructStore,
|
||||||
diffIdMap,
|
diffIdMap,
|
||||||
diffIdSet,
|
diffIdSet,
|
||||||
@@ -112,7 +112,8 @@ export {
|
|||||||
encodeIdMap,
|
encodeIdMap,
|
||||||
createIdMapFromIdSet,
|
createIdMapFromIdSet,
|
||||||
TwosetAttributionManager,
|
TwosetAttributionManager,
|
||||||
noAttributionsManager
|
noAttributionsManager,
|
||||||
|
createAttributionManagerFromDiff
|
||||||
} from './internals.js'
|
} from './internals.js'
|
||||||
|
|
||||||
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
ContentAny,
|
ContentAny,
|
||||||
ContentBinary,
|
ContentBinary,
|
||||||
getItemCleanStart,
|
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'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as delta from '../utils/Delta.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++) {
|
for (let i = 0; i < cs.length; i++) {
|
||||||
const { content, deleted, attrs } = cs[i]
|
const { content, deleted, attrs } = cs[i]
|
||||||
const attribution = createAttributionFromAttrs(attrs, deleted)
|
const attribution = createAttributionFromAttributionItems(attrs, deleted)
|
||||||
d.insert(content.getContent(), null, attribution)
|
d.insert(content.getContent(), null, attribution)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1006,7 +1006,7 @@ export const typeMapGetContent = (parent, am) => {
|
|||||||
am.readContent(cs, item)
|
am.readContent(cs, item)
|
||||||
const { deleted, attrs, content } = cs[cs.length - 1]
|
const { deleted, attrs, content } = cs[cs.length - 1]
|
||||||
const c = array.last(content.getContent())
|
const c = array.last(content.getContent())
|
||||||
const attribution = createAttributionFromAttrs(attrs, deleted)
|
const attribution = createAttributionFromAttributionItems(attrs, deleted)
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
mapcontent[key] = { prevValue: c, value: undefined, attribution }
|
mapcontent[key] = { prevValue: c, value: undefined, attribution }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
ContentType,
|
ContentType,
|
||||||
warnPrematureAccess,
|
warnPrematureAccess,
|
||||||
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, // eslint-disable-line
|
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, // eslint-disable-line
|
||||||
createAttributionFromAttrs
|
createAttributionFromAttributionItems
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as delta from '../utils/Delta.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++) {
|
for (let i = 0; i < cs.length; i++) {
|
||||||
const { content, deleted, attrs } = cs[i]
|
const { content, deleted, attrs } = cs[i]
|
||||||
const attribution = createAttributionFromAttrs(attrs, deleted)
|
const attribution = createAttributionFromAttributionItems(attrs, deleted)
|
||||||
switch (content.constructor) {
|
switch (content.constructor) {
|
||||||
case ContentString: {
|
case ContentString: {
|
||||||
d.insert(/** @type {ContentString} */ (content).str, null, attribution)
|
d.insert(/** @type {ContentString} */ (content).str, null, attribution)
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
getContent (am = noAttributionsManager) {
|
getContent (am = noAttributionsManager) {
|
||||||
const attributes = typeMapGetContent(this, am)
|
const attributes = typeMapGetContent(this, am)
|
||||||
const { children } = super.getContent(am)
|
const { children } = super.getContent(am)
|
||||||
return { children, attributes }
|
return { nodeName: this.nodeName, children, attributes }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
Item, AbstractContent, IdMap // eslint-disable-line
|
getItem,
|
||||||
|
diffIdSet,
|
||||||
|
createInsertionSetFromStructStore,
|
||||||
|
createDeleteSetFromStructStore,
|
||||||
|
createIdMapFromIdSet,
|
||||||
|
ContentDeleted,
|
||||||
|
Doc, Item, AbstractContent, IdMap, // eslint-disable-line
|
||||||
|
findIndexCleanStart
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error'
|
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
|
* @param {boolean} deleted - whether the attributed item is deleted
|
||||||
* @return {Attribution?}
|
* @return {Attribution?}
|
||||||
*/
|
*/
|
||||||
export const createAttributionFromAttrs = (attrs, deleted) => {
|
export const createAttributionFromAttributionItems = (attrs, deleted) => {
|
||||||
/**
|
/**
|
||||||
* @type {Attribution?}
|
* @type {Attribution?}
|
||||||
*/
|
*/
|
||||||
@@ -84,8 +91,6 @@ export class AbstractAttributionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for associating Attributions to content / changes
|
|
||||||
*
|
|
||||||
* @implements AbstractAttributionManager
|
* @implements AbstractAttributionManager
|
||||||
*/
|
*/
|
||||||
export class TwosetAttributionManager {
|
export class TwosetAttributionManager {
|
||||||
@@ -136,3 +141,71 @@ export class NoAttributionsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const noAttributionsManager = new 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
|
* @param {V} val
|
||||||
* @return {AttributionItem<V>}
|
* @return {AttributionItem<V>}
|
||||||
*/
|
*/
|
||||||
export const createAttribution = (name, val) => new AttributionItem(name, val)
|
export const createAttributionItem = (name, val) => new AttributionItem(name, val)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @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`.
|
* in `exclude`.
|
||||||
*
|
*
|
||||||
* @template {IdSet} Set
|
* @template {IdSet} Set
|
||||||
* @param {Set} set
|
* @param {Set} idSet
|
||||||
* @param {IdSet | IdMap<any>} exclude
|
* @param {IdSet | IdMap<any>} exclude
|
||||||
* @return {Set}
|
* @return {Set}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
import * as idmap from '../src/utils/IdMap.js'
|
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'
|
import * as YY from '../src/internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,7 +10,7 @@ import * as YY from '../src/internals.js'
|
|||||||
const simpleConstructAttrs = ops => {
|
const simpleConstructAttrs = ops => {
|
||||||
const attrs = createIdMap()
|
const attrs = createIdMap()
|
||||||
ops.forEach(op => {
|
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
|
return attrs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ export const createRandomIdMap = (gen, clients, clockRange, attrChoices) => {
|
|||||||
attrs.push(a)
|
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}`)
|
t.info(`Created IdMap with ${numOfOps} ranges and ${attrChoices.length} different attributes. Encoded size: ${encodeIdMap(idMap).byteLength}`)
|
||||||
return idMap
|
return idMap
|
||||||
|
|||||||
@@ -284,9 +284,54 @@ export const testElementAttributedContent = _tc => {
|
|||||||
null,
|
null,
|
||||||
{ delete: [] }
|
{ delete: [] }
|
||||||
).insert([{ nodeName: 'span', children: delta.createArrayDelta(), attributes: {} }])
|
).insert([{ nodeName: 'span', children: delta.createArrayDelta(), attributes: {} }])
|
||||||
.insert([
|
.insert([
|
||||||
delta.createTextDelta().insert('world', null, { insert: [] })
|
delta.createTextDelta().insert('world', null, { insert: [] })
|
||||||
], 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)
|
const attributedContent = yelement.getContentDeep(attributionManager)
|
||||||
console.log('children', JSON.stringify(attributedContent.children.toJSON().ops, null, 2))
|
console.log('children', JSON.stringify(attributedContent.children.toJSON().ops, null, 2))
|
||||||
console.log('cs expec', JSON.stringify(expectedContent.toJSON().ops, null, 2))
|
console.log('cs expec', JSON.stringify(expectedContent.toJSON().ops, null, 2))
|
||||||
|
|||||||
Reference in New Issue
Block a user