250 lines
7.8 KiB
JavaScript
250 lines
7.8 KiB
JavaScript
|
|
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<Number,Number>}
|
|
*/
|
|
this.beforeState = getStates(y.store)
|
|
/**
|
|
* Holds the state after the transaction.
|
|
* @type {Map<Number,Number>}
|
|
*/
|
|
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<AbstractType<YEvent>,Set<String|null>>}
|
|
*/
|
|
this.changed = new Map()
|
|
/**
|
|
* Stores the events for the types that observe also child elements.
|
|
* It is mainly used by `observeDeep`.
|
|
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
|
|
*/
|
|
this.changedParentTypes = new Map()
|
|
/**
|
|
* @type {encoding.Encoder|null}
|
|
* @private
|
|
*/
|
|
this._updateMessage = null
|
|
/**
|
|
* @type {Set<ID>}
|
|
* @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<AbstractStruct>}
|
|
*/
|
|
// @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<AbstractStruct>} 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<AbstractStruct>}
|
|
*/
|
|
// @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<AbstractStruct>}
|
|
*/
|
|
// @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])
|
|
}
|
|
}
|
|
}
|