import { getState, createID, writeStructsFromTransaction, writeDeleteSet, DeleteSet, sortAndMergeDeleteSet, getStates, findIndexSS, callEventHandlerListeners, AbstractItem, ItemDeleted, ID, AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line } from '../internals.js' import * as encoding from 'lib0/encoding.js' import * as map from 'lib0/map.js' import * as math from 'lib0/math.js' /** * A transaction is created for every change on the Yjs model. It is possible * to bundle changes on the Yjs model in a single transaction to * minimize the number on messages sent and the number of observer calls. * If possible the user of this library should bundle as many changes as * possible. Here is an example to illustrate the advantages of bundling: * * @example * const map = y.define('map', YMap) * // Log content when change is triggered * map.observe(() => { * console.log('change triggered') * }) * // Each change on the map type triggers a log message: * map.set('a', 0) // => "change triggered" * map.set('b', 0) // => "change triggered" * // When put in a transaction, it will trigger the log after the transaction: * y.transact(() => { * map.set('a', 1) * map.set('b', 1) * }) // => "change triggered" * * @public */ export class Transaction { /** * @param {Y} y */ constructor (y) { /** * The Yjs instance. * @type {Y} */ this.y = y /** * Describes the set of deleted items by ids * @type {DeleteSet} */ this.deleteSet = new DeleteSet() /** * Holds the state before the transaction started. * @type {Map} */ this.beforeState = getStates(y.store) /** * Holds the state after the transaction. * @type {Map} */ this.afterState = new Map() /** * All types that were directly modified (property added or child * inserted/deleted). New types are not included in this Set. * Maps from type to parentSubs (`item._parentSub = null` for YArray) * @type {Map,Set>} */ this.changed = new Map() /** * Stores the events for the types that observe also child elements. * It is mainly used by `observeDeep`. * @type {Map,Array>} */ this.changedParentTypes = new Map() /** * @type {encoding.Encoder|null} * @private */ this._updateMessage = null /** * @type {Set} * @private */ this._mergeStructs = new Set() } /** * @type {encoding.Encoder|null} * @public */ get updateMessage () { // only create if content was added in transaction (state or ds changed) if (this._updateMessage === null && (this.deleteSet.clients.size > 0 || map.any(this.afterState, (clock, client) => this.beforeState.get(client) !== clock))) { const encoder = encoding.createEncoder() sortAndMergeDeleteSet(this.deleteSet) writeStructsFromTransaction(encoder, this) writeDeleteSet(encoder, this.deleteSet) this._updateMessage = encoder } return this._updateMessage } } /** * @param {Transaction} transaction * * @private * @function */ export const nextID = transaction => { const y = transaction.y return createID(y.clientID, getState(y.store, y.clientID)) } /** * Implements the functionality of `y.transact(()=>{..})` * * @param {Y} y * @param {function(Transaction):void} f * * @private * @function */ export const transact = (y, f) => { let initialCall = false if (y._transaction === null) { initialCall = true y._transaction = new Transaction(y) y.emit('beforeTransaction', [y._transaction, y]) } const transaction = y._transaction try { f(transaction) } finally { if (initialCall) { y._transaction = null y.emit('beforeObserverCalls', [transaction, y]) // emit change events on changed types transaction.changed.forEach((subs, itemtype) => { itemtype._callObserver(transaction, subs) }) transaction.changedParentTypes.forEach((events, type) => { events = events .filter(event => event.target._item === null || !event.target._item.deleted ) events .forEach(event => { event.currentTarget = type }) // we don't need to check for events.length // because we know it has at least one element callEventHandlerListeners(type._dEH, events, transaction) }) // only call afterTransaction listeners if anything changed transaction.afterState = getStates(transaction.y.store) // when all changes & events are processed, emit afterTransaction event // transaction cleanup const store = transaction.y.store const ds = transaction.deleteSet // replace deleted items with ItemDeleted / GC sortAndMergeDeleteSet(ds) y.emit('afterTransaction', [transaction, y]) for (const [client, deleteItems] of ds.clients) { /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(client) for (let di = 0; di < deleteItems.length; di++) { const deleteItem = deleteItems[di] for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) { const struct = structs[si] if (deleteItem.clock + deleteItem.len <= struct.id.clock) { break } if (struct.deleted && struct instanceof AbstractItem) { if (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted)) { // check if we can GC struct.gc(transaction, store) } else { // otherwise only gc children (if there are any) struct.gcChildren(transaction, store) } } } } } /** * @param {Array} structs * @param {number} pos */ const tryToMergeWithLeft = (structs, pos) => { const left = structs[pos - 1] const right = structs[pos] if (left.deleted === right.deleted && left.constructor === right.constructor) { if (left.mergeWith(right)) { structs.splice(pos, 1) if (right instanceof AbstractItem && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) { // @ts-ignore we already did a constructor check above right.parent._map.set(right.parentSub, left) } } } } // on all affected store.clients props, try to merge for (const [client, clock] of transaction.afterState) { const beforeClock = transaction.beforeState.get(client) || 0 if (beforeClock !== clock) { /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(client) // we iterate from right to left so we can safely remove entries const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1) for (let i = structs.length - 1; i >= firstChangePos; i--) { tryToMergeWithLeft(structs, i) } } } // try to merge mergeStructs for (const mid of transaction._mergeStructs) { const client = mid.client const clock = mid.clock /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(client) const replacedStructPos = findIndexSS(structs, clock) if (replacedStructPos + 1 < structs.length) { tryToMergeWithLeft(structs, replacedStructPos + 1) } if (replacedStructPos > 0) { tryToMergeWithLeft(structs, replacedStructPos) } } y.emit('afterTransactionCleanup', [transaction, y]) } } }