# Transactions ## Transaction + Ledger System This system lets many canvas mutations over a time window (for example AI streaming patch/action/create) render immediately, while still producing a single undoable durable history entry. - AI streaming typically emits many `NEVER` updates (for real-time visual feedback). - `NEVER` updates advance Store snapshots, but do not write undo/redo history. - If we only do a final "one IMMEDIATELY at the end", we often miss the intended delta because snapshots may already be aligned. - Transaction + Ledger creates synthetic logical before/after snapshots, then commits one durable increment with correct undo semantics. ## Key Files ### `packages/excalidraw/transaction.ts` - `TransactionLedger`: keeps net mutation state across steps. - `recordStep()`: folds multiple updates into per-element baseline/target/touchedProps. - `buildSyntheticSnapshots()`: reconciles ledger state with live scene using fixed `live-wins-per-prop` behavior and returns `elementsBefore` / `elementsAfter` for history commit. - `TransactionManager`: thin factory holding the app reference. `create()` returns a `Transaction`. - `Transaction` (class): - `tx.updateScene(data)`: wraps `app.updateScene` with `captureUpdate: NEVER`. Snapshots elements before/after and records the diff into the ledger. Accumulates appState intent. - `tx.updateElements({ elements })`: convenience API for partial updates (`[{ id, strokeColor }, { id, x, y }]`). It materializes a next elements array and delegates to `tx.updateScene(...)`. - `tx.commit()`: builds synthetic before/after snapshots from the ledger, calls `store.commitSyntheticIncrement()`, returns a summary. - `tx.cancel()`: finalizes without committing history. ### `packages/element/src/store.ts` - `store.commitSyntheticIncrement()`: builds `StoreSnapshot -> StoreChange -> StoreDelta` from logical before/after element maps and optional appState patches. Schedules a micro IMMEDIATELY so the delta is flushed on the next `componentDidUpdate -> store.commit()`. ## How They Work Together 1. Caller creates a transaction (`excalidrawAPI.createTransaction()` or `app.transactionManager.create()`). 2. `tx.updateScene()` applies mutations with `NEVER` for real-time visual feedback. 3. Ledger records element diffs on each `updateScene()` call; appState intent is auto-accumulated. 4. On `tx.commit()`: - Ledger builds `logicalBefore/logicalAfter` reconciled against live scene - `store.commitSyntheticIncrement()` computes change/delta and schedules micro IMMEDIATELY 5. Next `componentDidUpdate -> store.commit()` flushes the micro and emits a durable increment to the undo stack. Result: - streaming visuals stay immediate; - history remains one entry per transaction; - synthetic durable commit is scheduled via micro IMMEDIATELY, flushed naturally by the next commit cycle. ## AppState Strategy The ledger tracks elements per-id per-property. AppState cannot use the same snapshot approach because `setState` is batched. Instead, the transaction auto-accumulates caller intent: each `tx.updateScene({ appState })` call merges the passed `appState` patch into `accumulatedAppState`. The transaction saves `initialAppState` at creation time. At commit: `appStateBefore = initialAppState`, `appStateAfter = initialAppState + accumulatedAppState`. ## Sample Usage ### Short Session ```ts const tx = app.transactionManager.create(); tx.updateScene({ elements: updatedElementsA }); tx.updateScene({ elements: updatedElementsB }); tx.updateElements({ elements: [{ id: "rect-1", strokeColor: "#f00" }, { id: "rect-2", x: 120 }], }); tx.commit(); ``` ### Long Session (multi-step with error handling) ```ts const tx = app.transactionManager.create(); try { tx.updateScene({ elements: step1Elements }); tx.updateScene({ elements: step2Elements }); tx.updateScene({ elements: step3Elements, appState: { selectedElementIds: nextSelectedIds }, }); tx.commit(); } catch (error) { tx.cancel(); throw error; } ``` ## Notes - `commit()` and `cancel()` are idempotent (repeated calls return the same summary). - `updateScene()` / `updateElements()` throw after `commit()` or `cancel()`. - Element conflict handling is fixed to `live-wins-per-prop`. - Multi-transaction concurrency works naturally — each tx has its own ledger.