mirror of
https://github.com/yjs/yjs.git
synced 2025-12-16 11:47:46 +01:00
[AttributionManager] auto-updates on doc changes and can destroy itself
This commit is contained in:
@@ -5,7 +5,10 @@ import {
|
||||
createDeleteSetFromStructStore,
|
||||
createIdMapFromIdSet,
|
||||
ContentDeleted,
|
||||
Doc, Item, AbstractContent, IdMap // eslint-disable-line
|
||||
Doc, Item, AbstractContent, IdMap, // eslint-disable-line
|
||||
insertIntoIdMap,
|
||||
insertIntoIdSet,
|
||||
diffIdMap
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
@@ -146,16 +149,46 @@ export const noAttributionsManager = new NoAttributionsManager()
|
||||
*/
|
||||
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
|
||||
constructor (prevDoc, nextDoc) {
|
||||
const nextDocInserts = createInsertionSetFromStructStore(nextDoc.store)
|
||||
const prevDocInserts = createInsertionSetFromStructStore(prevDoc.store)
|
||||
const nextDocDeletes = createDeleteSetFromStructStore(nextDoc.store)
|
||||
const prevDocDeletes = createDeleteSetFromStructStore(prevDoc.store)
|
||||
this.inserts = createIdMapFromIdSet(diffIdSet(nextDocInserts, prevDocInserts), [])
|
||||
this.deletes = createIdMapFromIdSet(diffIdSet(nextDocDeletes, prevDocDeletes), [])
|
||||
|
||||
this._prevDoc = prevDoc
|
||||
this._prevDocStore = prevDoc.store
|
||||
this._nextDoc = nextDoc
|
||||
// update before observer calls fired
|
||||
this._nextBOH = nextDoc.on('beforeObserverCalls', tr => {
|
||||
// update inserts
|
||||
insertIntoIdSet(nextDocInserts, tr.insertSet)
|
||||
const diffInserts = diffIdSet(tr.insertSet, prevDocInserts)
|
||||
insertIntoIdMap(this.inserts, createIdMapFromIdSet(diffInserts, []))
|
||||
// update deletes
|
||||
insertIntoIdSet(nextDocDeletes, tr.deleteSet)
|
||||
const diffDeletes = diffIdSet(tr.deleteSet, prevDocDeletes)
|
||||
insertIntoIdMap(this.deletes, createIdMapFromIdSet(diffDeletes, []))
|
||||
// @todo fire update ranges on `diffInserts` and `diffDeletes`
|
||||
})
|
||||
this._prevBOH = prevDoc.on('beforeObserverCalls', tr => {
|
||||
this.inserts = diffIdMap(this.inserts, tr.insertSet)
|
||||
this.deletes = diffIdMap(this.deletes, tr.deleteSet)
|
||||
// @todo fire update ranges on `tr.insertSet` and `tr.deleteSet`
|
||||
})
|
||||
this._destroyHandler = nextDoc.on('destroy', this.destroy.bind(this))
|
||||
prevDoc.on('destroy', this._destroyHandler)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this._nextDoc.off('destroy', this._destroyHandler)
|
||||
this._prevDoc.off('destroy', this._destroyHandler)
|
||||
this._nextDoc.off('beforeObserverCalls', this._nextBOH)
|
||||
this._prevDoc.off('beforeObserverCalls', this._prevBOH)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,11 +231,4 @@ export class DiffAttributionManager {
|
||||
* @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)
|
||||
}
|
||||
export const createAttributionManagerFromDiff = (prevDoc, nextDoc) => new DiffAttributionManager(prevDoc, nextDoc)
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
findIndexInIdRanges,
|
||||
findRangeStartInIdRanges,
|
||||
_deleteRangeFromIdSet,
|
||||
DSDecoderV1, DSDecoderV2, IdSetEncoderV1, IdSetEncoderV2, IdSet, ID // eslint-disable-line
|
||||
DSDecoderV1, DSDecoderV2, IdSetEncoderV1, IdSetEncoderV2, IdSet, ID, // eslint-disable-line
|
||||
_insertIntoIdSet
|
||||
} from '../internals.js'
|
||||
|
||||
import * as array from 'lib0/array'
|
||||
@@ -139,6 +140,10 @@ export class AttrRanges {
|
||||
this._ids = ids
|
||||
}
|
||||
|
||||
copy () {
|
||||
return new AttrRanges(this._ids.slice())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} length
|
||||
@@ -572,6 +577,13 @@ const _ensureAttrs = (idmap, attrs) => attrs.map(attr =>
|
||||
|
||||
export const createIdMap = () => new IdMap()
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {IdMap<T>} dest
|
||||
* @param {IdMap<T>} src
|
||||
*/
|
||||
export const insertIntoIdMap = _insertIntoIdSet
|
||||
|
||||
/**
|
||||
* Remove all ranges from `exclude` from `ds`. The result is a fresh IdMap containing all ranges from `idSet` that are not
|
||||
* in `exclude`.
|
||||
|
||||
@@ -51,6 +51,10 @@ class IdRanges {
|
||||
this._ids = ids
|
||||
}
|
||||
|
||||
copy () {
|
||||
return new IdRanges(this._ids.slice())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} length
|
||||
@@ -162,13 +166,13 @@ export const _deleteRangeFromIdSet = (set, client, clock, len) => {
|
||||
if (index != null) {
|
||||
for (let r = ids[index]; index < ids.length && r.clock < clock + len; r = ids[++index]) {
|
||||
if (r.clock < clock) {
|
||||
ids[index] = r.copyWith(r.clock, clock-r.clock)
|
||||
ids[index] = r.copyWith(r.clock, clock - r.clock)
|
||||
if (clock + len < r.clock + r.len) {
|
||||
ids.splice(index + 1, 0, r.copyWith(clock + len, r.clock + r.len - clock - len))
|
||||
}
|
||||
} else if (clock + len < r.clock + r.len) {
|
||||
// need to retain end
|
||||
ids[index] = r.copyWith(clock + len , r.clock + r.len - clock - len)
|
||||
ids[index] = r.copyWith(clock + len, r.clock + r.len - clock - len)
|
||||
} else if (ids.length === 1) {
|
||||
set.clients.delete(client)
|
||||
return
|
||||
@@ -283,23 +287,30 @@ export const mergeIdSets = idSets => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} dest
|
||||
* @param {IdSet} src
|
||||
* @template {IdSet | IdMap<any>} S
|
||||
* @param {S} dest
|
||||
* @param {S} src
|
||||
*/
|
||||
export const insertIntoIdSet = (dest, src) => {
|
||||
export const _insertIntoIdSet = (dest, src) => {
|
||||
src.clients.forEach((srcRanges, client) => {
|
||||
const targetRanges = dest.clients.get(client)
|
||||
if (targetRanges) {
|
||||
array.appendTo(targetRanges.getIds(), srcRanges.getIds())
|
||||
targetRanges.sorted = false
|
||||
} else {
|
||||
const res = new IdRanges(srcRanges.getIds().slice())
|
||||
const res = srcRanges.copy()
|
||||
res.sorted = true
|
||||
dest.clients.set(client, res)
|
||||
dest.clients.set(client, /** @type {any} */ (res))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {IdSet} dest
|
||||
* @param {IdSet} src
|
||||
*/
|
||||
export const insertIntoIdSet = _insertIntoIdSet
|
||||
|
||||
/**
|
||||
* Remove all ranges from `exclude` from `ds`. The result is a fresh IdSet containing all ranges from `idSet` that are not
|
||||
* in `exclude`.
|
||||
@@ -373,10 +384,7 @@ export const _diffSet = (set, exclude) => {
|
||||
* 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} idSet
|
||||
* @param {IdSet | IdMap<any>} exclude
|
||||
* @return {Set}
|
||||
* @type {(idSet: IdSet, exclude: IdSet|IdMap<any>) => IdSet}
|
||||
*/
|
||||
export const diffIdSet = _diffSet
|
||||
|
||||
|
||||
@@ -309,36 +309,69 @@ export const testElementAttributedContentViaDiffer = _tc => {
|
||||
const yelement = ydoc.getXmlElement('p')
|
||||
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: [] })
|
||||
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', attributedContent.children.toJSON().ops)
|
||||
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.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')
|
||||
})
|
||||
t.assert(attributedContent.nodeName === 'UNDEFINED')
|
||||
})
|
||||
ydoc.transact(() => {
|
||||
elem3.insert(0, 'big')
|
||||
})
|
||||
t.group('test getContentDeep after some more updates', () => {
|
||||
t.info('expecting diffingAttributionManager to auto update itself')
|
||||
const expectedContent = delta.createArrayDelta().insert(
|
||||
[delta.createTextDelta().insert('hello', null, { delete: [] })],
|
||||
null,
|
||||
{ delete: [] }
|
||||
).insert([{ nodeName: 'span', children: delta.createArrayDelta(), attributes: {} }])
|
||||
.insert([
|
||||
delta.createTextDelta().insert('bigworld', 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')
|
||||
})
|
||||
Y.applyUpdate(ydocV1, Y.encodeStateAsUpdate(ydoc))
|
||||
t.group('test getContentDeep both docs synced', () => {
|
||||
t.info('expecting diffingAttributionManager to auto update itself')
|
||||
const expectedContent = delta.createArrayDelta().insert([{ nodeName: 'span', children: delta.createArrayDelta(), attributes: {} }]).insert([
|
||||
delta.createTextDelta().insert('bigworld')
|
||||
])
|
||||
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: null } })
|
||||
t.assert(attributedContent.nodeName === 'UNDEFINED')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user