# Transactions

Transactions let many live scene updates render immediately while still committing a single durable history entry.

Typical use case:

- AI streaming emits many `NEVER` updates for real-time feedback.
- Those updates advance the live scene and store snapshot, but do not create undo history.
- A transaction records the intended logical delta separately, then commits one synthetic durable increment at the end.

## Key Files

### `packages/excalidraw/transaction/`

- `transaction.ts`
  - `Transaction` is the per-tx instance.
  - `updateScene()` applies live updates with `captureUpdate: NEVER`.
  - `updateElements()` is a typed partial-patch helper over `updateScene()`.
  - `commit()` builds synthetic snapshots and commits one durable entry.
  - `cancel()` ends the tx without committing history.
- `ledger.ts`
  - `TransactionLedger` keeps per-element baseline, target, and touched props across tx steps.
  - `buildSyntheticSnapshots()` reconciles tx intent with the live scene using `live-wins-per-prop`.
- `undoOverridePlanner.ts`
  - Tracks tx intermediate values.
  - Produces undo override candidates for regular user history entries that were recorded while a tx was active.
- `manager.ts`
  - Owns tx lifecycle state and active-tx priority.
  - Injects tx undo markers into durable deltas at record time.
  - Resolves effective history deltas at undo/redo time.
- `diff.ts` / `types.ts`
  - Shared diff helpers and transaction-specific types.

### `packages/element/src/store.ts`

- `store.commitSyntheticIncrement()`
  - Builds `StoreSnapshot -> StoreChange -> StoreDelta` from logical before/after snapshots.
  - Commits the synthetic durable increment immediately through an isolated path.
  - Does not flush unrelated pending micro actions.
- `StoreDelta.markers`
  - Carries extra history markers such as `txUndoOverrides`.

### `packages/excalidraw/history.ts`

- `onBeforeRecord()`
  - Lets the transaction manager attach markers before a durable delta becomes a history entry.
- `setEffectiveDeltaResolver()`
  - Lets the transaction manager rewrite a history delta at undo/redo time before it is applied.

## Flow

1. Caller creates a transaction.
2. `tx.updateScene()` applies live updates with `NEVER`.
3. The ledger records net element changes; appState intent is accumulated separately.
4. Regular user durable edits that happen while the tx is active may receive tx undo markers.
5. `tx.commit()` asks the ledger for logical before/after snapshots and calls `store.commitSyntheticIncrement()`.
6. Undo/redo later uses the manager's effective-delta resolver to decide whether ended tx markers should patch the baseline of a regular history entry.

Result:

- live rendering stays immediate;
- one tx still produces one durable history entry;
- interleaved regular edits can undo back to the correct pre-tx baseline.

## Undo Examples

### Interleaved Regular Edit

1. Baseline: `rect = (x: 0, stroke: black)`
2. Tx updates it live to `(x: 200, stroke: red)` with `NEVER`
3. User makes a regular durable edit: `(x: 200, stroke: purple)`
4. Tx commits; live stays purple

The user history entry was recorded while the tx was active, so its local baseline is `red`.
If undo later runs after the tx has ended, restoring `red` is wrong: the correct pre-tx baseline is `black`.

To fix this:

- record-time attaches a tx undo marker to the user history entry
- perform-time checks whether the tx has already ended
- if ended, the effective delta is patched so undo restores `black` instead of `red`

### Active Tx vs Ended Tx

Using the same example:

- if the user undoes while the tx is still active, the marker stays inactive and undo restores the tx intermediate value (`red`)
- if the user undoes after the tx has ended, the marker becomes active and undo restores the pre-tx baseline (`black`)

So markers are recorded early, but applied conditionally.

## AppState

Elements use ledger snapshots. AppState does not.

The transaction instead keeps:

- `initialAppState`: observed appState at tx start
- `accumulatedAppState`: merged caller intent from `tx.updateScene({ appState })`

At commit, the transaction computes the final appState patch and passes it into `store.commitSyntheticIncrement()`.

## Example

```ts
const tx = app.transactionManager.create();

tx.updateScene({ elements: updatedElementsA });
tx.updateScene({ elements: updatedElementsB });
tx.updateElements({
  elements: [
    { id: "rect-1", type: "rectangle", updates: { strokeColor: "#f00" } },
    { id: "rect-2", type: "rectangle", updates: { x: 120 } },
  ],
});

tx.commit();
```

## Notes

- `commit()` and `cancel()` are idempotent.
- `updateScene()` and `updateElements()` throw after the tx has ended.
- Conflict handling inside synthetic snapshots is fixed to `live-wins-per-prop`.
- Multi-tx interleaving is supported through manager-owned lifecycle tracking and active-tx priority.
