From b2df311fce428460e3c376f913ccf15e47ef16ff Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Sun, 7 Dec 2025 01:21:19 +0100 Subject: [PATCH] fixed all tests & proper diff /w attribution in nested deltas --- package-lock.json | 8 ++++---- package.json | 2 +- src/types/AbstractType.js | 21 +++++++++++++-------- src/utils/YEvent.js | 31 +++++++++++++++++++++++++++++-- src/utils/delta-helpers.js | 2 +- test.html | 12 ++++-------- tests/attribution.tests.js | 9 ++++----- tests/y-map.tests.js | 31 +++++++++++++++++++++++++++++++ 8 files changed, 87 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a2fb393..bbd2c304 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "14.0.0-14", "license": "MIT", "dependencies": { - "lib0": "^0.2.115-4" + "lib0": "^0.2.115-6" }, "devDependencies": { "@types/node": "^22.14.1", @@ -3479,9 +3479,9 @@ } }, "node_modules/lib0": { - "version": "0.2.115-4", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.115-4.tgz", - "integrity": "sha512-6/oPO1T3Hsl7YAIuCga+iGpIMwY3/6osNCZbMNkBGIdHrhEm+fdW2I4x0UNNL52BvyFTGJslJimj/iOJ+NqABA==", + "version": "0.2.115-6", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.115-6.tgz", + "integrity": "sha512-Xv2aEEvDYOzWiJEYZy5rM4WpKb1ujJYe70Wds/kDpxEP5wMbS/zWHFnveekx1aG3o8anq8FGrb9CCenGWA1BYg==", "license": "MIT", "dependencies": { "isomorphic.js": "^0.2.4" diff --git a/package.json b/package.json index 50c3fe26..0a1e5cfd 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ }, "homepage": "https://docs.yjs.dev", "dependencies": { - "lib0": "^0.2.115-4" + "lib0": "^0.2.115-6" }, "devDependencies": { "@types/node": "^22.14.1", diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index c884dcf0..f450837d 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -464,22 +464,22 @@ export class AbstractType { * @param {import('../utils/IdSet.js').IdSet?} [opts.itemsToRender] * @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions * @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only - * @param {Set?} [opts.renderAttrs] - set of attrs to render. if null, render all attributes - * @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 - * @param {Set|Map|null} [opts.modified] - set of types that should be rendered as modified children + * @param {Map>|null} [opts.modified] - set of types that should be rendered as modified children * @param {Deep} [opts.deep] - render child types as delta * @return {Deep extends true ? ToDeepEventDelta : EventDelta} The Delta representation of this type. * * @public */ getContent (am = noAttributionsManager, opts = {}) { - const { itemsToRender = null, retainInserts = false, retainDeletes = false, renderAttrs = null, renderChildren = true, deletedItems = null, modified = null, deep = false } = opts + const { itemsToRender = null, retainInserts = false, retainDeletes = false, deletedItems = null, modified = null, deep = false } = opts + const renderAttrs = modified?.get(this) || null + const renderChildren = (modified == null || opts.modified.get(this)?.has(null)) /** * @type {EventDelta extends delta.Delta ? delta.DeltaBuilder : never} */ const d = /** @type {any} */ (delta.create(/** @type {any} */ (this).nodeName || null)) - typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am, deep, modified, deletedItems, itemsToRender) + typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am, deep, modified, deletedItems, itemsToRender, opts) if (renderChildren) { /** * @type {delta.FormattingAttributes} @@ -571,7 +571,7 @@ export class AbstractType { if (c.deleted ? retainDeletes : retainInserts) { d.retain(c.content.getLength(), null, attribution ?? {}) } else if (deep && c.content.constructor === ContentType) { - d.insert([/** @type {any} */(c.content).type.getContent(am, { ...opts, renderChildren: true, renderAttrs: null })], null, attribution) + d.insert([/** @type {any} */(c.content).type.getContent(am, opts)], null, attribution) } else { d.insert(c.content.getContent(), null, attribution) } @@ -1296,6 +1296,8 @@ export const typeMapGetAll = (parent) => { } /** + * @todo move this to getContent/getDelta + * * Render the difference to another ydoc (which can be empty) and highlight the differences with * attributions. * @@ -1305,17 +1307,18 @@ export const typeMapGetAll = (parent) => { * @template {delta.DeltaBuilder} TypeDelta * @param {TypeDelta} d * @param {YType_} parent - * @param {Set?} attrsToRender + * @param {Set?} attrsToRender * @param {import('../internals.js').AbstractAttributionManager} am * @param {boolean} deep * @param {Set|Map|null} [modified] - set of types that should be rendered as modified children * @param {import('../utils/IdSet.js').IdSet?} [deletedItems] * @param {import('../utils/IdSet.js').IdSet?} [itemsToRender] + * @param {any} [opts] * * @private * @function */ -export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, deletedItems, itemsToRender) => { +export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, deletedItems, itemsToRender, opts) => { // @todo support modified ops! /** * @param {Item} item @@ -1334,6 +1337,8 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, de if (itemsToRender == null || itemsToRender.hasId(item.lastId)) { d.unset(key, attribution, c) } + } else if (deep && c instanceof AbstractType && modified?.has(c)) { + d.update(key, c.getContent(am, opts)) } else { // find prev content let prevContentItem = item diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index 0a367d02..3f4bbacf 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -5,7 +5,9 @@ import { AbstractAttributionManager, Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line } from '../internals.js' +import * as map from 'lib0/map' import * as delta from 'lib0/delta' // eslint-disable-line +import * as set from 'lib0/set' /** * @typedef {import('./types.js').YType} _YType @@ -121,8 +123,33 @@ export class YEvent { */ getDelta (am = noAttributionsManager, { deep } = {}) { const itemsToRender = mergeIdSets([diffIdSet(this.transaction.insertSet, this.transaction.deleteSet), diffIdSet(this.transaction.deleteSet, this.transaction.insertSet)]) - const modified = deep ? this.transaction.changedParentTypes : null - return /** @type {any} */ (this.target.getContent(am, { itemsToRender, retainDeletes: true, renderAttrs: this.keysChanged, renderChildren: deep || this.childListChanged, deletedItems: this.transaction.deleteSet, deep: !!deep, modified })) + /** + * @todo this should be done only one in the transaction step + * + * @type {Map>|null} + */ + let modified = this.transaction.changed + if (deep) { + // need to add deep changes to copy of modified + const dchanged = new Map() + modified.forEach((attrs, type) => { + dchanged.set(type, new Set(attrs)) + }) + for (let m of modified.keys()) { + while (m._item != null) { + const item = m._item + const ms = map.setIfUndefined(dchanged, item?.parent, set.create) + if (item && !ms.has(item.parentSub)) { + ms.add(item.parentSub) + m = /** @type {any} */ (item.parent) + } else { + break + } + } + } + modified = dchanged + } + return /** @type {any} */ (this.target.getContent(am, { itemsToRender, retainDeletes: true, deletedItems: this.transaction.deleteSet, deep: !!deep, modified })) } /** diff --git a/src/utils/delta-helpers.js b/src/utils/delta-helpers.js index 5790e6f7..5c4c5c66 100644 --- a/src/utils/delta-helpers.js +++ b/src/utils/delta-helpers.js @@ -44,7 +44,7 @@ export const diffDocsToDelta = (v1, v2, { am = createAttributionManagerFromDiff( if (typeConf) { // @ts-ignore const shareDelta = type.getContent(am, { - itemsToRender, retainDeletes: true, renderAttrs: /** @type {Set} */ (changedTypes.get(type)), renderChildren: typeConf.has(null), deletedItems: deletesOnly, modified: changedTypes, deep: true + itemsToRender, retainDeletes: true, deletedItems: deletesOnly, modified: changedTypes, deep: true }) d.update(typename, shareDelta) } diff --git a/test.html b/test.html index c1dc0dfa..f19aa0da 100644 --- a/test.html +++ b/test.html @@ -35,7 +35,7 @@ "lib0/conditions": "./node_modules/lib0/conditions.js", "lib0/crypto/jwt": "./node_modules/lib0/crypto/jwt.js", "lib0/crypto/aes-gcm": "./node_modules/lib0/crypto/aes-gcm.js", - "lib0/delta": "./node_modules/lib0/delta/d2.js", + "lib0/delta": "./node_modules/lib0/delta/delta.js", "lib0/crypto/ecdsa": "./node_modules/lib0/crypto/ecdsa.js", "lib0/crypto/rsa-oaep": "./node_modules/lib0/crypto/rsa-oaep.js", "lib0/hash/rabin": "./node_modules/lib0/hash/rabin.js", @@ -137,9 +137,7 @@ "lib0/symbol.js": "./node_modules/lib0/symbol.js", "lib0/dist/symbol.cjs": "./node_modules/lib0/dist/symbol.cjs", "lib0/symbol": "./node_modules/lib0/symbol.js", - "lib0/traits.js": "./node_modules/lib0/traits.js", - "lib0/dist/traits.cjs": "./node_modules/lib0/dist/traits.cjs", - "lib0/traits": "./node_modules/lib0/traits.js", + "lib0/traits": "./node_modules/lib0/trait/traits.js", "lib0/testing.js": "./node_modules/lib0/testing.js", "lib0/dist/testing.cjs": "./node_modules/lib0/dist/testing.cjs", "lib0/testing": "./node_modules/lib0/testing.js", @@ -202,7 +200,7 @@ "lib0/conditions": "./node_modules/lib0/conditions.js", "lib0/crypto/jwt": "./node_modules/lib0/crypto/jwt.js", "lib0/crypto/aes-gcm": "./node_modules/lib0/crypto/aes-gcm.js", - "lib0/delta": "./node_modules/lib0/delta/d2.js", + "lib0/delta": "./node_modules/lib0/delta/delta.js", "lib0/crypto/ecdsa": "./node_modules/lib0/crypto/ecdsa.js", "lib0/crypto/rsa-oaep": "./node_modules/lib0/crypto/rsa-oaep.js", "lib0/hash/rabin": "./node_modules/lib0/hash/rabin.js", @@ -304,9 +302,7 @@ "lib0/symbol.js": "./node_modules/lib0/symbol.js", "lib0/dist/symbol.cjs": "./node_modules/lib0/dist/symbol.cjs", "lib0/symbol": "./node_modules/lib0/symbol.js", - "lib0/traits.js": "./node_modules/lib0/traits.js", - "lib0/dist/traits.cjs": "./node_modules/lib0/dist/traits.cjs", - "lib0/traits": "./node_modules/lib0/traits.js", + "lib0/traits": "./node_modules/lib0/trait/traits.js", "lib0/testing.js": "./node_modules/lib0/testing.js", "lib0/dist/testing.cjs": "./node_modules/lib0/dist/testing.cjs", "lib0/testing": "./node_modules/lib0/testing.js", diff --git a/tests/attribution.tests.js b/tests/attribution.tests.js index ba2cdee2..c8892c1e 100644 --- a/tests/attribution.tests.js +++ b/tests/attribution.tests.js @@ -100,11 +100,10 @@ export const testYdocDiff = () => { ydocUpdated.getMap('map').get('nested').insert(0, [1]) // @todo add custom attribution const d = Y.diffDocsToDelta(ydocStart, ydocUpdated) + console.log('calculated diff', d.toJSON()) t.compare(d, delta.create() - .update('text', delta.create().retain(5).insert('world')) - .update('array', delta.create().retain(1).insert(['x'])) - .update('map', delta.create().set('newk', 42).update('nested', delta.create().insert([1]))) + .update('text', delta.create().retain(5).insert(' world', null, { insert: [] })) + .update('array', delta.create().retain(1).insert(['x'], null, { insert: [] })) + .update('map', delta.create().set('newk', 42, { insert: [] }).update('nested', delta.create().insert([1], null, { insert: [] }))) ) - console.log(d.toJSON()) - debugger } diff --git a/tests/y-map.tests.js b/tests/y-map.tests.js index 4fa5e964..a1acc9f4 100644 --- a/tests/y-map.tests.js +++ b/tests/y-map.tests.js @@ -37,6 +37,37 @@ export const testIterators = _tc => { console.log(vals, entries, keys) } +export const testNestedMapEvent = () => { + const ydoc = new Y.Doc() + const ymap = ydoc.getMap() + const ymapNested = ymap.set('nested', new Y.Map()) + let called = 0 + ymap.observeDeep((events, tr) => { + const event = events.find(event => event.target === ymap) || new Y.YEvent(ymap, tr, new Set()) + const d = event.deltaDeep + called++ + t.compare(d, delta.create().update('nested', delta.create().set('k', 'v'))) + }) + ymapNested.set('k', 'v') + t.assert(called === 1) +} + +export const testNestedMapEvent2 = () => { + const ydoc = new Y.Doc() + const yarr = ydoc.getArray() + const ymapNested = new Y.Map() + yarr.insert(0, [ymapNested]) + let called = 0 + yarr.observeDeep((events, tr) => { + const event = events.find(event => event.target === yarr) || new Y.YEvent(yarr, tr, new Set()) + const d = event.deltaDeep + called++ + t.compare(d, delta.create().modify(delta.create().set('k', 'v'))) + }) + ymapNested.set('k', 'v') + t.assert(called === 1) +} + /** * Computing event changes after transaction should result in an error. See yjs#539 *