Transactions
Batching changes with yDoc.transact() for atomic operations and better performance.
Why Transactions
Transactions ensure that:
- All changes sync together (not partially)
- Observers fire once (not for each operation)
- Undo captures the entire transaction
// Without transaction: 2 syncs, 2 observer events
items.set('k8d2fn3m', { name: 'Task 1' })
order.push(['k8d2fn3m'])
// With transaction: 1 sync, 1 observer event
yDoc.transact(() => {
items.set('k8d2fn3m', { name: 'Task 1' })
order.push(['k8d2fn3m'])
})Atomic Sync
Without transactions, remote clients might see partial state:
// WRONG: order might sync before content
order.push(['k8d2fn3m'])
items.set('k8d2fn3m', data)
// CORRECT: atomic
yDoc.transact(() => {
items.set('k8d2fn3m', data)
order.push(['k8d2fn3m'])
})Transaction Origins
The second argument identifies the change source:
yDoc.transact(() => {
content.set('title', 'New Title')
}, 'user-action')With UndoManager
Filter which changes to track:
const undoManager = new UndoManager([content], {
trackedOrigins: new Set(['user-action'])
})
// Tracked for undo
yDoc.transact(() => content.set('title', 'New'), 'user-action')
// NOT tracked (remote sync, system updates)
yDoc.transact(() => metadata.set('modified', Date.now()), 'system')In Observers
items.observe((event, transaction) => {
if (transaction.origin === 'import') return // Skip re-render
renderItems()
})Nested Transactions
Nested transactions merge into the outermost:
function addItem(data) {
yDoc.transact(() => {
items.set(data.id, data)
order.push([data.id])
})
}
function addMultiple(dataArray) {
yDoc.transact(() => {
for (const data of dataArray) {
addItem(data) // Inner transact merges into outer
}
})
// Single sync, single observer event
}Anti-Patterns
Async inside transactions:
// WRONG: async breaks transaction
yDoc.transact(async () => {
items.set('a', { ... })
await saveToServer() // Transaction already ended!
items.set('b', { ... }) // NOT in same transaction
})
// CORRECT
yDoc.transact(() => {
items.set('a', { ... })
items.set('b', { ... })
})
await saveToServer()Over-large transactions:
// For huge imports, chunk to avoid blocking UI
async function importLarge(data: any[]) {
const CHUNK = 1000
for (let i = 0; i < data.length; i += CHUNK) {
yDoc.transact(() => {
data.slice(i, i + CHUNK).forEach(item => items.set(item.id, item))
}, 'import')
await new Promise(r => setTimeout(r, 0)) // Yield to UI
}
}