fixed most tests for delta v2 migration

This commit is contained in:
Kevin Jahns
2025-10-21 16:31:59 +02:00
parent 91384b54bf
commit c9829b0993
15 changed files with 1047 additions and 1471 deletions

View File

@@ -233,7 +233,7 @@ export const updateMarkerChanges = (searchMarker, index, len) => {
/** /**
* Accumulate all (list) children of a type and return them as an Array. * Accumulate all (list) children of a type and return them as an Array.
* *
* @param {AbstractType} t * @param {import('../utils/types.js').YType} t
* @return {Array<Item>} * @return {Array<Item>}
*/ */
export const getTypeChildren = t => { export const getTypeChildren = t => {
@@ -272,7 +272,7 @@ export const callTypeObservers = (type, transaction, event) => {
/** /**
* Abstract Yjs Type class * Abstract Yjs Type class
* @template {delta.Delta<any,any,any,any,any>} [EventDelta=delta.Delta<any,any,any,any,any>] * @template {delta.Delta<any,any,any,any,any>} [EventDelta=delta.Delta<any,any,any,any,any>]
* @template {YType_} [Self=any] * @template {AbstractType<any,any>} [Self=any]
*/ */
export class AbstractType { export class AbstractType {
constructor () { constructor () {
@@ -442,18 +442,19 @@ export class AbstractType {
* @param {import('../utils/IdSet.js').IdSet?} [opts.itemsToRender] * @param {import('../utils/IdSet.js').IdSet?} [opts.itemsToRender]
* @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions * @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions
* @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only * @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only
* @param {Set<string>?} [opts.renderAttrs] - if true, retain rendered+attributed deletes only * @param {Set<string>?} [opts.renderAttrs] - set of attrs to render. if null, render all attributes
* @param {boolean} [opts.renderChildren] - if true, retain rendered+attributed deletes only * @param {boolean} [opts.renderChildren] - if true, retain rendered+attributed deletes only
* @param {import('../utils/IdSet.js').IdSet?} [opts.deletedItems] - used for computing prevItem in attributes
* @return {EventDelta} The Delta representation of this type. * @return {EventDelta} The Delta representation of this type.
* *
* @public * @public
*/ */
getContent (am = noAttributionsManager, { itemsToRender = null, retainInserts = false, retainDeletes = false, renderAttrs = null, renderChildren = true } = {}) { getContent (am = noAttributionsManager, { itemsToRender = null, retainInserts = false, retainDeletes = false, renderAttrs = null, renderChildren = true, deletedItems = null } = {}) {
/** /**
* @type {EventDelta} * @type {EventDelta}
*/ */
const d = /** @type {any} */ (delta.create()) const d = /** @type {any} */ (delta.create())
typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am) typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am, deletedItems, itemsToRender)
if (renderChildren) { if (renderChildren) {
/** /**
* @type {delta.FormattingAttributes} * @type {delta.FormattingAttributes}
@@ -625,7 +626,7 @@ export class AbstractType {
* @type {import('../utils/AttributionManager.js').Attribution} * @type {import('../utils/AttributionManager.js').Attribution}
*/ */
const formattingAttribution = object.assign({}, d.usedAttribution) const formattingAttribution = object.assign({}, d.usedAttribution)
const changedAttributedAttributes = /** @type {{ [key: string]: Array<any> }} */ (formattingAttribution.attributes = object.assign({}, formattingAttribution.attributes ?? {})) const changedAttributedAttributes = /** @type {{ [key: string]: Array<any> }} */ (formattingAttribution.format = object.assign({}, formattingAttribution.format ?? {}))
if (attribution == null || equalAttrs(previousUnattributedAttributes[key], currentAttributes[key] ?? null)) { if (attribution == null || equalAttrs(previousUnattributedAttributes[key], currentAttributes[key] ?? null)) {
// an unattributed formatting attribute was found or an attributed formatting // an unattributed formatting attribute was found or an attributed formatting
// attribute was found that resets to the previous status // attribute was found that resets to the previous status
@@ -635,13 +636,13 @@ export class AbstractType {
const by = changedAttributedAttributes[key] = (changedAttributedAttributes[key]?.slice() ?? []) const by = changedAttributedAttributes[key] = (changedAttributedAttributes[key]?.slice() ?? [])
by.push(...((c.deleted ? attribution.delete : attribution.insert) ?? [])) by.push(...((c.deleted ? attribution.delete : attribution.insert) ?? []))
const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt) const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt)
if (attributedAt) formattingAttribution.attributedAt = attributedAt if (attributedAt) formattingAttribution.formatAt = attributedAt
} }
if (object.isEmpty(changedAttributedAttributes)) { if (object.isEmpty(changedAttributedAttributes)) {
d.useAttribution(null) d.useAttribution(null)
} else if (attribution != null) { } else if (attribution != null) {
const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt) const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt)
if (attributedAt != null) formattingAttribution.attributedAt = attributedAt if (attributedAt != null) formattingAttribution.formatAt = attributedAt
d.useAttribution(formattingAttribution) d.useAttribution(formattingAttribution)
} }
} }
@@ -651,7 +652,7 @@ export class AbstractType {
} }
} }
} }
return d return /** @type {any} */ (d.done())
} }
/** /**
@@ -678,7 +679,7 @@ export class AbstractType {
op.value = op.value.getContentDeep(am) op.value = op.value.getContentDeep(am)
} }
}) })
return /** @type {any} */ (d) return /** @type {any} */ (d.done())
} }
} }
@@ -768,47 +769,6 @@ export const typeListToArray = type => {
return cs return cs
} }
/**
* @todo this can be removed as this can be replaced by a generic function
* Render the difference to another ydoc (which can be empty) and highlight the differences with
* attributions.
*
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
* attribution `{ isDeleted: true, .. }`.
*
* @template {delta.Delta<any,any,any,any>} TypeDelta
* @param {TypeDelta} d
* @param {import('../utils/types.js').YType} type
* @param {import('../internals.js').AbstractAttributionManager} am
*
* @private
* @function
*/
export const typeListGetContent = (d, type, am) => {
type.doc ?? warnPrematureAccess()
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const cs = []
for (let item = type._start; item !== null; cs.length = 0) {
// populate cs
for (; item !== null && cs.length < 50; item = item.right) {
am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
}
for (let i = 0; i < cs.length; i++) {
const c = cs[i]
const attribution = createAttributionFromAttributionItems(c.attrs, c.deleted)
if (c.content.isCountable()) {
if (c.render || attribution != null) {
d.insert(c.content.getContent(), null, attribution)
} else if (!c.deleted) {
d.retain(c.content.getLength())
}
}
}
}
}
/** /**
* @param {AbstractType<any>} type * @param {AbstractType<any>} type
* @param {Snapshot} snapshot * @param {Snapshot} snapshot
@@ -1270,13 +1230,13 @@ export const typeMapGetAll = (parent) => {
* @param {YType_} parent * @param {YType_} parent
* @param {Set<string>?} attrsToRender * @param {Set<string>?} attrsToRender
* @param {import('../internals.js').AbstractAttributionManager} am * @param {import('../internals.js').AbstractAttributionManager} am
* @param {import('../utils/IdSet.js').IdSet?} [deletedItems]
* @param {import('../utils/IdSet.js').IdSet?} [itemsToRender]
* *
* @private * @private
* @function * @function
*/ */
export const typeMapGetDelta = (d, parent, attrsToRender, am) => { export const typeMapGetDelta = (d, parent, attrsToRender, am, deletedItems, itemsToRender) => {
parent.doc ?? warnPrematureAccess()
/** /**
* @param {Item} item * @param {Item} item
* @param {string} key * @param {string} key
@@ -1291,28 +1251,17 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am) => {
const c = array.last(content.getContent()) const c = array.last(content.getContent())
const attribution = createAttributionFromAttributionItems(attrs, deleted) const attribution = createAttributionFromAttributionItems(attrs, deleted)
if (deleted) { if (deleted) {
if (itemsToRender == null || itemsToRender.hasId(item.lastId)) {
d.unset(key, attribution, c) d.unset(key, attribution, c)
}
} else { } else {
/** // find prev content
* @type {Array<import('../internals.js').AttributedContent<any>>} let prevContentItem = item
*/ // this algorithm is problematic. should check all previous content using am.readcontent
let cs = [] for (; prevContentItem.left !== null && deletedItems?.hasId(prevContentItem.left.lastId); prevContentItem = prevContentItem.left) {
for (let prevItem = item.left; prevItem != null; prevItem = prevItem.left) { // nop
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const tmpcs = []
am.readContent(tmpcs, prevItem.id.client, prevItem.id.clock, prevItem.deleted, prevItem.content, 1)
cs = tmpcs.concat(cs)
if (cs.length === 0 || cs[0].attrs == null) {
cs.splice(0, cs.findIndex(c => c.attrs != null))
break
} }
if (cs.length > 0) { const prevValue = (prevContentItem !== item && itemsToRender?.hasId(prevContentItem.lastId)) ? array.last(prevContentItem.content.getContent()) : undefined
cs.length = 1
}
}
const prevValue = cs.length > 0 ? array.last(cs[0].content.getContent()) : undefined
d.set(key, c, attribution, prevValue) d.set(key, c, attribution, prevValue)
} }
} }

View File

@@ -17,7 +17,6 @@ import {
callTypeObservers, callTypeObservers,
transact, transact,
warnPrematureAccess, warnPrematureAccess,
typeListGetContent,
typeListSlice, typeListSlice,
noAttributionsManager, noAttributionsManager,
AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
@@ -108,7 +107,7 @@ export class YArray extends AbstractType {
*/ */
_callObserver (transaction, parentSubs) { _callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs) super._callObserver(transaction, parentSubs)
callTypeObservers(this, transaction, new YEvent(this, transaction, null)) callTypeObservers(this, transaction, new YEvent(this, transaction, parentSubs))
} }
/** /**
@@ -214,24 +213,6 @@ export class YArray extends AbstractType {
return super.getContentDeep(am) return super.getContentDeep(am)
} }
/**
* Render the difference to another ydoc (which can be empty) and highlight the differences with
* attributions.
*
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
* attribution `{ isDeleted: true, .. }`.
*
* @param {AbstractAttributionManager} am
* @return {delta.ArrayDelta<T>} The Delta representation of this type.
*
* @public
*/
getContent (am = noAttributionsManager) {
const d = this.change
typeListGetContent(d, this, am)
return d
}
/** /**
* Returns a portion of this YArray into a JavaScript Array selected * Returns a portion of this YArray into a JavaScript Array selected
* from start to end (end not included). * from start to end (end not included).

View File

@@ -758,9 +758,11 @@ export class YText extends AbstractType {
transact(this.doc, transaction => { transact(this.doc, transaction => {
const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), am) const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), am)
for (const op of d.children) { for (const op of d.children) {
if (delta.$insertOp.check(op)) { if (delta.$textOp.check(op)) {
if (op.insert.length > 0 || typeof op.insert !== 'string') {
insertText(transaction, this, currPos, op.insert, op.format || {}) insertText(transaction, this, currPos, op.insert, op.format || {})
} else if (delta.$insertOp.check(op)) {
for (let i = 0; i < op.insert.length; i++) {
insertText(transaction, this, currPos, op.insert[i], op.format || {})
} }
} else if (delta.$retainOp.check(op)) { } else if (delta.$retainOp.check(op)) {
currPos.formatText(transaction, this, op.retain, op.format || {}) currPos.formatText(transaction, this, op.retain, op.format || {})

View File

@@ -38,8 +38,8 @@ export const attributionJsonSchema = s.$object({
insertedAt: s.$number.optional, insertedAt: s.$number.optional,
delete: s.$array(s.$string).optional, delete: s.$array(s.$string).optional,
deletedAt: s.$number.optional, deletedAt: s.$number.optional,
attributes: s.$record(s.$string, s.$array(s.$string)).optional, format: s.$record(s.$string, s.$array(s.$string)).optional,
attributedAt: s.$number.optional formatAt: s.$number.optional
}) })
/** /**

View File

@@ -41,10 +41,6 @@ export class YEvent {
* @type {Transaction} * @type {Transaction}
*/ */
this.transaction = transaction this.transaction = transaction
/**
* @type {Object|null}
*/
this._changes = null
/** /**
* @type {null | Map<string, { action: 'add' | 'update' | 'delete', oldValue: any }>} * @type {null | Map<string, { action: 'add' | 'update' | 'delete', oldValue: any }>}
*/ */
@@ -183,7 +179,7 @@ export class YEvent {
*/ */
getDelta (am = noAttributionsManager) { getDelta (am = noAttributionsManager) {
const itemsToRender = mergeIdSets([diffIdSet(this.transaction.insertSet, this.transaction.deleteSet), diffIdSet(this.transaction.deleteSet, this.transaction.insertSet)]) const itemsToRender = mergeIdSets([diffIdSet(this.transaction.insertSet, this.transaction.deleteSet), diffIdSet(this.transaction.deleteSet, this.transaction.insertSet)])
return /** @type {any} */ (this.target.getContent(am, { itemsToRender, retainDeletes: true, renderAttrs: this.keysChanged, renderChildren: this.childListChanged })) return /** @type {any} */ (this.target.getContent(am, { itemsToRender, retainDeletes: true, renderAttrs: this.keysChanged, renderChildren: this.childListChanged, deletedItems: this.transaction.deleteSet }))
} }
/** /**

View File

@@ -1,15 +1,4 @@
/**
* @typedef {Object<string,any>|Array<any>|number|null|string|Uint8Array|BigInt
* |import('../index.js').Array<any>
* |import('../index.js').Map<any>
* |import('../index.js').Text<any>
* |import('../index.js').XmlElement<any>
* |import('../index.js').XmlFragment<any>
* |import('../index.js').XmlText
* |import('../index.js').XmlHook} YValue
*/
/** /**
* @typedef {import('../types/YArray.js').YArray<any> * @typedef {import('../types/YArray.js').YArray<any>
* | import('../types/YMap.js').YMap<any> * | import('../types/YMap.js').YMap<any>
@@ -17,15 +6,17 @@
* | import('../types/YXmlFragment.js').YXmlFragment<any,any> * | import('../types/YXmlFragment.js').YXmlFragment<any,any>
* | import('../types/YXmlElement.js').YXmlElement<any,any> * | import('../types/YXmlElement.js').YXmlElement<any,any>
* | import('../types/YXmlHook.js').YXmlHook * | import('../types/YXmlHook.js').YXmlHook
* | import('../types/YXmlText.js').YXmlText} YType * | import('../types/YXmlText.js').YXmlText} YValueType
*/ */
/** /**
* @typedef {typeof import('../types/YArray.js').YArray<any> * @typedef {Object<string,any>|Array<any>|number|null|string|Uint8Array|BigInt|YValueType} YValue
* | typeof import('../types/YMap.js').YMap<any> */
* | typeof import('../types/YText.js').YText<any>
* | typeof import('../types/YXmlFragment.js').YXmlFragment<any,any> /**
* | typeof import('../types/YXmlElement.js').YXmlElement<any,any> * @typedef {import('../types/AbstractType.js').AbstractType<any,any>} YType
* | typeof import('../types/YXmlHook.js').YXmlHook */
* | typeof import('../types/YXmlText.js').YXmlText} YTypeConstructors
/**
* @typedef {typeof import('../types/AbstractType.js').AbstractType<any,any>} YTypeConstructors
*/ */

View File

@@ -35,6 +35,7 @@
"lib0/conditions": "./node_modules/lib0/conditions.js", "lib0/conditions": "./node_modules/lib0/conditions.js",
"lib0/crypto/jwt": "./node_modules/lib0/crypto/jwt.js", "lib0/crypto/jwt": "./node_modules/lib0/crypto/jwt.js",
"lib0/crypto/aes-gcm": "./node_modules/lib0/crypto/aes-gcm.js", "lib0/crypto/aes-gcm": "./node_modules/lib0/crypto/aes-gcm.js",
"lib0/delta": "./node_modules/lib0/delta/d2.js",
"lib0/crypto/ecdsa": "./node_modules/lib0/crypto/ecdsa.js", "lib0/crypto/ecdsa": "./node_modules/lib0/crypto/ecdsa.js",
"lib0/crypto/rsa-oaep": "./node_modules/lib0/crypto/rsa-oaep.js", "lib0/crypto/rsa-oaep": "./node_modules/lib0/crypto/rsa-oaep.js",
"lib0/hash/rabin": "./node_modules/lib0/hash/rabin.js", "lib0/hash/rabin": "./node_modules/lib0/hash/rabin.js",
@@ -201,6 +202,7 @@
"lib0/conditions": "./node_modules/lib0/conditions.js", "lib0/conditions": "./node_modules/lib0/conditions.js",
"lib0/crypto/jwt": "./node_modules/lib0/crypto/jwt.js", "lib0/crypto/jwt": "./node_modules/lib0/crypto/jwt.js",
"lib0/crypto/aes-gcm": "./node_modules/lib0/crypto/aes-gcm.js", "lib0/crypto/aes-gcm": "./node_modules/lib0/crypto/aes-gcm.js",
"lib0/delta": "./node_modules/lib0/delta/d2.js",
"lib0/crypto/ecdsa": "./node_modules/lib0/crypto/ecdsa.js", "lib0/crypto/ecdsa": "./node_modules/lib0/crypto/ecdsa.js",
"lib0/crypto/rsa-oaep": "./node_modules/lib0/crypto/rsa-oaep.js", "lib0/crypto/rsa-oaep": "./node_modules/lib0/crypto/rsa-oaep.js",
"lib0/hash/rabin": "./node_modules/lib0/hash/rabin.js", "lib0/hash/rabin": "./node_modules/lib0/hash/rabin.js",

View File

@@ -29,7 +29,7 @@ export const testFindTypeInOtherDoc = _tc => {
const ydocClone = new Y.Doc() const ydocClone = new Y.Doc()
Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc)) Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc))
/** /**
* @template {Y.AbstractType<any>} Type * @template {import('../src/utils/types.js').YType} Type
* @param {Type} ytype * @param {Type} ytype
* @param {Y.Doc} otherYdoc * @param {Y.Doc} otherYdoc
* @return {Type} * @return {Type}
@@ -47,7 +47,7 @@ export const testFindTypeInOtherDoc = _tc => {
if (rootKey == null) { if (rootKey == null) {
throw new Error('type does not exist in other ydoc') throw new Error('type does not exist in other ydoc')
} }
return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {typeof Y.AbstractType<any>} */ (ytype.constructor))) return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {import('../src/utils/types.js').YTypeConstructors} */ (ytype.constructor)))
} else { } else {
/** /**
* If it is a sub type, we use the item id to find the history type. * If it is a sub type, we use the item id to find the history type.

View File

@@ -7,6 +7,8 @@ import * as object from 'lib0/object'
import * as map from 'lib0/map' import * as map from 'lib0/map'
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as math from 'lib0/math' import * as math from 'lib0/math'
import * as list from 'lib0/list'
import * as delta from 'lib0/delta'
import { import {
createIdSet, createIdMap, addToIdSet, encodeIdMap createIdSet, createIdMap, addToIdSet, encodeIdMap
} from '../src/internals.js' } from '../src/internals.js'
@@ -484,7 +486,7 @@ export const compare = users => {
t.compare(userArrayValues[i], userArrayValues[i + 1]) t.compare(userArrayValues[i], userArrayValues[i + 1])
t.compare(userMapValues[i], userMapValues[i + 1]) t.compare(userMapValues[i], userMapValues[i + 1])
t.compare(userXmlValues[i], userXmlValues[i + 1]) t.compare(userXmlValues[i], userXmlValues[i + 1])
t.compare(userTextValues[i].ops.map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length) t.compare(list.toArray(userTextValues[i].children).map(a => delta.$textOp.check(a) ? a.insert : ' ').join('').length, users[i].getText('text').length)
t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => { t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => {
if (a instanceof Y.AbstractType) { if (a instanceof Y.AbstractType) {
t.compare(a.toJSON(), b.toJSON()) t.compare(a.toJSON(), b.toJSON())

View File

@@ -1,7 +1,7 @@
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import { init } from './testHelper.js' // eslint-disable-line import { init } from './testHelper.js' // eslint-disable-line
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
import * as delta from '../src/utils/Delta.js' import * as delta from 'lib0/delta'
export const testInconsistentFormat = () => { export const testInconsistentFormat = () => {
/** /**
@@ -11,7 +11,7 @@ export const testInconsistentFormat = () => {
const content = /** @type {Y.XmlText} */ (ydoc.get('text', Y.XmlText)) const content = /** @type {Y.XmlText} */ (ydoc.get('text', Y.XmlText))
content.format(0, 6, { bold: null }) content.format(0, 6, { bold: null })
content.format(6, 4, { type: 'text' }) content.format(6, 4, { type: 'text' })
t.compare(content.getContent(), delta.createTextDelta().insert('Merge Test', { type: 'text' }).insert(' After', { type: 'text', italic: true })) t.compare(content.getContent(), delta.create().insert('Merge Test', { type: 'text' }).insert(' After', { type: 'text', italic: true }))
} }
const initializeYDoc = () => { const initializeYDoc = () => {
const yDoc = new Y.Doc({ gc: false }) const yDoc = new Y.Doc({ gc: false })
@@ -85,11 +85,11 @@ export const testUndoText = tc => {
t.assert(text0.toString() === 'bcxyz') t.assert(text0.toString() === 'bcxyz')
// test marks // test marks
text0.format(1, 3, { bold: true }) text0.format(1, 3, { bold: true })
t.compare(text0.getContent(), delta.fromJSON([{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])) t.compare(text0.getContent(), delta.create().insert('b').insert('cxy', { bold: true }).insert('z'))
undoManager.undo() undoManager.undo()
t.compare(text0.getContent(), delta.fromJSON([{ insert: 'bcxyz' }])) t.compare(text0.getContent(), delta.create().insert('bcxyz'))
undoManager.redo() undoManager.redo()
t.compare(text0.getContent(), delta.fromJSON([{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])) t.compare(text0.getContent(), delta.create().insert('b').insert('cxy', { bold: true }).insert('z'))
} }
/** /**
@@ -677,14 +677,10 @@ export const testUndoDeleteTextFormat = _tc => {
undoManager.undo() undoManager.undo()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
const expect = delta.fromJSON([ const expect = delta.create()
{ insert: 'Attack ships ' }, .insert('Attack ships ')
{ .insert('on fire', { bold: true })
insert: 'on fire', .insert(' off the shoulder of Orion.')
attributes: { bold: true }
},
{ insert: ' off the shoulder of Orion.' }
])
t.compare(text.getContent(), expect) t.compare(text.getContent(), expect)
t.compare(text2.getContent(), expect) t.compare(text2.getContent(), expect)
} }

View File

@@ -5,6 +5,7 @@ import { readStructSet, readIdSet, UpdateDecoderV2, UpdateEncoderV2, writeIdSet
import * as encoding from 'lib0/encoding' import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding' import * as decoding from 'lib0/decoding'
import * as object from 'lib0/object' import * as object from 'lib0/object'
import * as delta from 'lib0/delta'
/** /**
* @typedef {Object} Enc * @typedef {Object} Enc
@@ -126,7 +127,7 @@ export const testKeyEncoding = tc => {
const update = Y.encodeStateAsUpdateV2(users[0]) const update = Y.encodeStateAsUpdateV2(users[0])
Y.applyUpdateV2(users[1], update) Y.applyUpdateV2(users[1], update)
t.compare(text1.getContent().toJSON(), [{ insert: 'c', attributes: { italic: true } }, { insert: 'b' }, { insert: 'a', attributes: { italic: true } }]) t.compare(text1.getContent().toJSON().children, [{ insert: 'c', format: { italic: true } }, { insert: 'b' }, { insert: 'a', format: { italic: true } }])
compare(users) compare(users)
} }
@@ -207,7 +208,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
} }
const meta = enc.parseUpdateMeta(mergedUpdates) const meta = enc.parseUpdateMeta(mergedUpdates)
meta.from.forEach((clock, client) => t.assert(clock === 0)) meta.from.forEach((clock, _client) => t.assert(clock === 0))
meta.to.forEach((clock, client) => { meta.to.forEach((clock, client) => {
const structs = /** @type {Array<Y.Item>} */ (merged.store.clients.get(client)) const structs = /** @type {Array<Y.Item>} */ (merged.store.clients.get(client))
const lastStruct = structs[structs.length - 1] const lastStruct = structs[structs.length - 1]
@@ -237,10 +238,10 @@ export const testMergeUpdates1 = _tc => {
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} _tc
*/ */
export const testMergeUpdates2 = tc => { export const testMergeUpdates2 = _tc => {
encoders.forEach((enc, i) => { encoders.forEach((enc, _i) => {
t.info(`Using encoder: ${enc.description}`) t.info(`Using encoder: ${enc.description}`)
const ydoc = new Y.Doc({ gc: false }) const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array>} */ ([]) const updates = /** @type {Array<Uint8Array>} */ ([])
@@ -257,23 +258,23 @@ export const testMergeUpdates2 = tc => {
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} _tc
*/ */
export const testMergePendingUpdates = tc => { export const testMergePendingUpdates = _tc => {
const yDoc = new Y.Doc() const yDoc = new Y.Doc()
/** /**
* @type {Array<Uint8Array>} * @type {Array<Uint8Array>}
*/ */
const serverUpdates = [] const serverUpdates = []
yDoc.on('update', (update, origin, c) => { yDoc.on('update', (update, _origin, _c) => {
serverUpdates.splice(serverUpdates.length, 0, update) serverUpdates.splice(serverUpdates.length, 0, update)
}) })
const yText = yDoc.getText('textBlock') const yText = yDoc.getText('textBlock')
yText.applyDelta([{ insert: 'r' }]) yText.applyDelta(delta.create().insert('r'))
yText.applyDelta([{ insert: 'o' }]) yText.applyDelta(delta.create().insert('o'))
yText.applyDelta([{ insert: 'n' }]) yText.applyDelta(delta.create().insert('n'))
yText.applyDelta([{ insert: 'e' }]) yText.applyDelta(delta.create().insert('e'))
yText.applyDelta([{ insert: 'n' }]) yText.applyDelta(delta.create().insert('n'))
const yDoc1 = new Y.Doc() const yDoc1 = new Y.Doc()
Y.applyUpdate(yDoc1, serverUpdates[0]) Y.applyUpdate(yDoc1, serverUpdates[0])
@@ -297,8 +298,7 @@ export const testMergePendingUpdates = tc => {
const yDoc5 = new Y.Doc() const yDoc5 = new Y.Doc()
Y.applyUpdate(yDoc5, update4) Y.applyUpdate(yDoc5, update4)
Y.applyUpdate(yDoc5, serverUpdates[4]) Y.applyUpdate(yDoc5, serverUpdates[4])
// @ts-ignore Y.encodeStateAsUpdate(yDoc5)
const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const yText5 = yDoc5.getText('textBlock') const yText5 = yDoc5.getText('textBlock')
t.compareStrings(yText5.toString(), 'nenor') t.compareStrings(yText5.toString(), 'nenor')
@@ -313,7 +313,7 @@ export const testObfuscateUpdates = _tc => {
const ymap = ydoc.getMap('map') const ymap = ydoc.getMap('map')
const yarray = ydoc.getArray('array') const yarray = ydoc.getArray('array')
// test ytext // test ytext
ytext.applyDelta([{ insert: 'text', attributes: { bold: true } }, { insert: { href: 'supersecreturl' } }]) ytext.applyDelta(delta.create().insert('text', { bold: true }).insert([{ href: 'supersecreturl' }]))
// test ymap // test ymap
ymap.set('key', 'secret1') ymap.set('key', 'secret1')
ymap.set('key', 'secret2') ymap.set('key', 'secret2')
@@ -330,13 +330,14 @@ export const testObfuscateUpdates = _tc => {
const omap = odoc.getMap('map') const omap = odoc.getMap('map')
const oarray = odoc.getArray('array') const oarray = odoc.getArray('array')
// test ytext // test ytext
const delta = /** @type {Array<any>} */ (otext.getContent().toJSON()) const d = /** @type {any} */ (otext.getContent().toJSON().children)
t.assert(delta.length === 2) t.assert(d.length === 2)
t.assert(delta[0].insert !== 'text' && delta[0].insert.length === 4) const q = d[0]
t.assert(object.length(delta[0].attributes) === 1) t.assert(d[0].insert !== 'text' && d[0].insert.length === 4)
t.assert(!object.hasProperty(delta[0].attributes, 'bold')) t.assert(object.length(d[0].format) === 1)
t.assert(object.length(delta[1]) === 1) t.assert(!object.hasProperty(d[0].format, 'bold'))
t.assert(object.hasProperty(delta[1], 'insert')) t.assert(object.length(d[1]) === 1)
t.assert(object.hasProperty(d[1], 'insert'))
// test ymap // test ymap
t.assert(omap.size === 1) t.assert(omap.size === 1)
t.assert(!omap.has('key')) t.assert(!omap.has('key'))

View File

@@ -4,7 +4,7 @@ import * as t from 'lib0/testing'
import * as prng from 'lib0/prng' import * as prng from 'lib0/prng'
import * as math from 'lib0/math' import * as math from 'lib0/math'
import * as env from 'lib0/environment' import * as env from 'lib0/environment'
import * as delta from '../src/utils/Delta.js' import * as delta from 'lib0/delta'
const isDevMode = env.getVariable('node_env') === 'development' const isDevMode = env.getVariable('node_env') === 'development'
@@ -384,24 +384,22 @@ export const testObservedeepIndexes = _tc => {
export const testChangeEvent = tc => { export const testChangeEvent = tc => {
const { array0, users } = init(tc, { users: 2 }) const { array0, users } = init(tc, { users: 2 })
/** /**
* @type {any} * @type {delta.Delta<any,any,any,any,any>}
*/ */
let changes = null let d = delta.create()
array0.observe(e => { array0.observe(e => {
changes = e.changes d = e.delta
}) })
const newArr = new Y.Array() const newArr = new Y.Array()
array0.insert(0, [newArr, 4, 'dtrn']) array0.insert(0, [newArr, 4, 'dtrn'])
t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0) t.assert(d !== null && d.children.len === 1)
t.compare(changes.delta, [{ insert: [newArr, 4, 'dtrn'] }]) t.compare(d.toJSON().children, [{ insert: [newArr, 4, 'dtrn'] }])
changes = null
array0.delete(0, 2) array0.delete(0, 2)
t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2) t.assert(d !== null && d.children.len === 1)
t.compare(changes.delta, [{ delete: 2 }]) t.compare(d.toJSON().children, [{ delete: 2 }])
changes = null
array0.insert(1, [0.1]) array0.insert(1, [0.1])
t.assert(changes !== null && changes.added.size === 1 && changes.deleted.size === 0) t.assert(d !== null && d.children.len === 2)
t.compare(changes.delta, [{ retain: 1 }, { insert: [0.1] }]) t.compare(d.toJSON().children, [{ retain: 1 }, { insert: [0.1] }])
compare(users) compare(users)
} }
@@ -531,7 +529,7 @@ export const testAttributedContent = _tc => {
yarray.delete(0, 1) yarray.delete(0, 1)
yarray.insert(1, [42]) yarray.insert(1, [42])
}) })
const expectedContent = delta.createArrayDelta().insert([1], null, { delete: [] }).insert([2]).insert([42], null, { insert: [] }) const expectedContent = delta.create().insert([1], null, { delete: [] }).insert([2]).insert([42], null, { insert: [] })
const attributedContent = yarray.getContent(attributionManager) const attributedContent = yarray.getContent(attributionManager)
console.log(attributedContent.toJSON()) console.log(attributedContent.toJSON())
t.assert(attributedContent.equals(expectedContent)) t.assert(attributedContent.equals(expectedContent))

View File

@@ -4,11 +4,12 @@ import {
compareIDs, compareIDs,
noAttributionsManager, noAttributionsManager,
TwosetAttributionManager, TwosetAttributionManager,
createIdMapFromIdSet, createIdMapFromIdSet
mapDeltaJsonSchema
} from '../src/internals.js' } from '../src/internals.js'
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
import * as prng from 'lib0/prng' import * as prng from 'lib0/prng'
import * as delta from 'lib0/delta'
import * as s from 'lib0/schema'
/** /**
* @param {t.TestCase} _tc * @param {t.TestCase} _tc
@@ -490,45 +491,41 @@ export const testThrowsDeleteEventsOnClear = tc => {
export const testChangeEvent = tc => { export const testChangeEvent = tc => {
const { map0, users } = init(tc, { users: 2 }) const { map0, users } = init(tc, { users: 2 })
/** /**
* @type {any} * @type {delta.Delta<any,any,any,any>?}
*/ */
let changes = null let changes = delta.create()
/**
* @type {any}
*/
let keyChange = null
map0.observe(e => { map0.observe(e => {
changes = e.changes changes = e.delta
}) })
map0.set('a', 1) map0.set('a', 1)
keyChange = changes.keys.get('a') let keyChange = changes.attrs.get('a')
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
map0.set('a', 2) map0.set('a', 2)
keyChange = changes.keys.get('a') keyChange = changes.attrs.get('a')
t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 1) t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === 1)
users[0].transact(() => { users[0].transact(() => {
map0.set('a', 3) map0.set('a', 3)
map0.set('a', 4) map0.set('a', 4)
}) })
keyChange = changes.keys.get('a') keyChange = changes.attrs.get('a')
t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 2) t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === 2)
users[0].transact(() => { users[0].transact(() => {
map0.set('b', 1) map0.set('b', 1)
map0.set('b', 2) map0.set('b', 2)
}) })
keyChange = changes.keys.get('b') keyChange = changes.attrs.get('b')
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
users[0].transact(() => { users[0].transact(() => {
map0.set('c', 1) map0.set('c', 1)
map0.delete('c') map0.delete('c')
}) })
t.assert(changes !== null && changes.keys.size === 0) t.assert(changes !== null && changes.attrs.size === 0)
users[0].transact(() => { users[0].transact(() => {
map0.set('d', 1) map0.set('d', 1)
map0.set('d', 2) map0.set('d', 2)
}) })
keyChange = changes.keys.get('d') keyChange = changes.attrs.get('d')
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
compare(users) compare(users)
} }
@@ -631,24 +628,24 @@ export const testAttributedContent = _tc => {
}) })
t.group('initial value', () => { t.group('initial value', () => {
ymap.set('test', 42) ymap.set('test', 42)
const expectedContent = mapDeltaJsonSchema.ensure({ test: { type: 'insert', prevValue: undefined, value: 42, attribution: { insert: [] } } }) const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 42, attribution: { insert: [] } }) }
const attributedContent = ymap.getContent(attributionManager) const attributedContent = ymap.getContent(attributionManager)
console.log(attributedContent.toJSON()) console.log(attributedContent.toJSON())
t.compare(expectedContent, attributedContent.toJSON()) t.compare(expectedContent, attributedContent.toJSON().attrs)
}) })
t.group('overwrite value', () => { t.group('overwrite value', () => {
ymap.set('test', 'fourtytwo') ymap.set('test', 'fourtytwo')
const expectedContent = mapDeltaJsonSchema.ensure({ test: { type: 'insert', prevValue: 42, value: 'fourtytwo', attribution: { insert: [] } } }) const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', prevValue: 42, value: 'fourtytwo', attribution: { insert: [] } }) }
const attributedContent = ymap.getContent(attributionManager) const attributedContent = ymap.getContent(attributionManager)
console.log(attributedContent) console.log(attributedContent)
t.compare(expectedContent, attributedContent.toJSON()) t.compare(expectedContent, attributedContent.toJSON().attrs)
}) })
t.group('delete value', () => { t.group('delete value', () => {
ymap.delete('test') ymap.delete('test')
const expectedContent = mapDeltaJsonSchema.ensure({ test: { type: 'delete', prevValue: 'fourtytwo', attribution: { delete: [] } } }) const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'delete', prevValue: 'fourtytwo', attribution: { delete: [] } }) }
const attributedContent = ymap.getContent(attributionManager) const attributedContent = ymap.getContent(attributionManager)
console.log(attributedContent) console.log(attributedContent.toJSON())
t.compare(expectedContent, attributedContent.toJSON()) t.compare(expectedContent, attributedContent.toJSON().attrs)
}) })
} }

File diff suppressed because it is too large Load Diff

View File

@@ -20,5 +20,5 @@
} }
}, },
"include": ["./src/**/*.js", "./tests/**/*.js"], "include": ["./src/**/*.js", "./tests/**/*.js"],
"exclude": ["../lib0/**"] "exclude": ["./node_modules/**/*"]
} }