diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 445260cb..d8783159 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -257,7 +257,7 @@ const cleanupTransactions = (transactionCleanups, i) => { // the type-rooted deep delta of this transaction (a nested `modify` chain for ancestors) const change = /** @type {any} */ (deepEventHandler.getDelta({ renderer: type._renderer, deep: true }).done()) type._delta?.apply(change) // keep the cache current (incl. ancestors and diff-renderer attributions) - if (hasDeltaListeners) type.emit('delta', [change]) + if (hasDeltaListeners) type.emit('delta', [change, transaction.origin]) } }) }) diff --git a/src/ytype.js b/src/ytype.js index f76cb2db..a018ba33 100644 --- a/src/ytype.js +++ b/src/ytype.js @@ -636,7 +636,8 @@ export const callTypeObservers = (type, transaction, event) => { * Abstract Yjs Type class. * * A `YType` is a {@link https://github.com/dmonad/lib0 lib0} `RDT` ("replicated data type", see - * `lib0/delta/rdt.js`): it emits a `'delta'` event whenever its state changes, accepts foreign + * `lib0/delta/rdt.js`): it emits a `'delta'` event whenever its state changes (carrying the change + * and the origin of the transaction that caused it), accepts foreign * changes via {@link YType#applyDelta}, exposes its delta {@link YType#$delta schema}, and can be * torn down via {@link YType#destroy}. This lets a `YType` be `bind()`-ed to any other RDT (another * `YType`, an in-memory delta, a DOM subtree, …). The legacy {@link YType#observe `observe`} / @@ -644,7 +645,7 @@ export const callTypeObservers = (type, transaction, event) => { * channel. * * @template {delta.DeltaConf} [DConf=any] - * @extends {ObservableV2<{ delta: (delta: delta.Delta) => void, destroy: (type: YType) => void }>} + * @extends {ObservableV2<{ delta: (delta: delta.Delta, origin: any) => void, destroy: (type: YType) => void }>} */ export class YType extends ObservableV2 { /** @@ -731,6 +732,11 @@ export class YType extends ObservableV2 { * (e.g. `type.delta.clone()`) if you need a stable snapshot, and call {@link YType#clearCache} to * drop the cache. * + * Consider the returned delta **done** — it must not be edited from the outside. It is + * deliberately typed as a `Delta` (not a `DeltaBuilder`) so the mutating builder API is not + * reachable; editing it anyway would corrupt the cache without changing the CRDT. The proper way + * to change this type is {@link YType#applyDelta}. + * * @type {delta.Delta} */ get delta () { @@ -772,7 +778,8 @@ export class YType extends ObservableV2 { * * If the deep-delta cache ({@link YType#delta}) is being maintained, or a `'delta'` listener is * attached, the content is re-rendered with the new renderer and the difference is emitted on the - * `'delta'` channel only (a renderer switch is not a CRDT change, so no `YEvent` is produced). + * `'delta'` channel only (a renderer switch is not a CRDT change, so no `YEvent` is produced, and + * the emitted origin is `null` as no transaction is involved). * * @param {AbstractRenderer} renderer * @return {this} @@ -787,7 +794,7 @@ export class YType extends ObservableV2 { if (this._delta !== null) this._delta = newState if (hasDeltaListeners) { const d = /** @type {any} */ (delta.diff(/** @type {any} */ (oldState), /** @type {any} */ (newState))) - if (!d.isEmpty()) this.emit('delta', [d]) + if (!d.isEmpty()) this.emit('delta', [d, null]) } } else { this._renderer = renderer diff --git a/tests/attribution.tests.js b/tests/attribution.tests.js index 038c17fe..87d81344 100644 --- a/tests/attribution.tests.js +++ b/tests/attribution.tests.js @@ -256,6 +256,28 @@ export const testRdtDeltaEvent = () => { t.compare(captured, delta.create().insert('hello').done()) } +/** + * The `'delta'` event carries the transaction origin as its second argument. + */ +export const testRdtDeltaEventOrigin = () => { + const ydoc = new Y.Doc() + const ytext = ydoc.get() + /** + * @type {any} + */ + let capturedOrigin = null + ytext.on('delta', (_d, origin) => { capturedOrigin = origin }) + const myOrigin = {} + ydoc.transact(() => { + ytext.insert(0, 'hello') + }, myOrigin) + t.assert(capturedOrigin === myOrigin) + // without an explicit origin, `null` is emitted + capturedOrigin = myOrigin + ytext.insert(5, ' world') + t.assert(capturedOrigin === null) +} + /** * `useRenderer` changes the default renderer used by `toDelta` (and friends). Calling `toDelta()` * with no argument afterwards is equivalent to passing the renderer explicitly.