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
  }
}

See Also

  • Undo/Redo - How transactions integrate with UndoManager
  • API Usage - Transaction-related mistakes