# Commands and history

A command represents an undoable edit, so gestures and application actions can share the engine's history instead of maintaining separate undo stacks.

Loading and editing are different intents: setup mutates the live model directly; user-facing edits execute commands. The framework binding renders that model, while the engine owns execution and history.

## Two intents, one live model

Use [`DiagramModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-diagrammodel#diagrammodel) for setup and [`DiagramEngine`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-engine#diagramengine) for editing:

| Intent | API | Result |
| --- | --- | --- |
| Seed a document | `diagram.addNode(node)` | Adds the node without recording a history entry. |
| Add a node on the user's behalf | `await engine.addNode(node)` | Executes the shipped add-node command and returns the live node. |
| Execute an explicit edit | `await engine.commandManager.execute(command)` | Executes and validates the command before recording an undoable entry. |
| Reverse or replay an edit | `await engine.undo()` / `await engine.redo()` | Changes the same model the canvas renders. |

```mermaid
flowchart LR
  Setup["Setup / import"] --> Model["Live diagram model"]
  Gesture["Canvas gesture"] --> Commands["Engine commands"]
  Action["Application action"] --> Commands
  Commands --> Model
  Commands --> History["One history stack"]
  History --> Reverse["Undo / redo"]
  Reverse --> Model
  Model --> Binding["Framework binding"]
  Binding --> Canvas["Rendered canvas"]
  Binding --> State["Controlled application state"]
```

Direct model additions are not recorded in history. Use an engine editing method or execute a command when a toolbar action must be undoable.

[`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance) has no `undo()` or `redo()`: reach history through `instance.getEngine().undo()` and `instance.getEngine().redo()`. For a flat instance facade, [`createDiagramApi`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-ext-functions#creatediagramapi) exposes asynchronous `execute()`, `undo()` and `redo()` and requests a repaint after each call.

## Bundle an application action into one undo step

Use the shipped [`AddNodeCommand`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-commands-classes-a-r#addnodecommand) rather than implementing node addition yourself. Wrap independent additions in [`BatchCommand`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-commands-classes-a-r#batchcommand) when they form one user action. The batch executes its children in order and undoes them in reverse order.

This browser example mounts Angular's [`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent#diagramcanvascomponent) with an engine, following its uncontrolled binding. It starts with a single node labelled **Loaded**. Select **Add pair** to add **Review** and **Ship** together; select **Undo** to remove both in one step. The loaded node remains. Drag a node, then undo: the drag joins that same history.

Install the packages in your Angular project:

```bash
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer rxjs @grafloria/element
```

Construct the data with [`NodeModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-nodemodel#nodemodel). Replace your root component with this file:

```ts title="app.component.ts"
import { Component, OnDestroy, signal } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { AddNodeCommand, BatchCommand, DiagramEngine, NodeModel } from '@grafloria/engine';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <button [disabled]="busy()" (click)="addPair()">Add pair</button>
    <button [disabled]="busy()" (click)="undo(canvas)">Undo</button>
    <button [disabled]="busy()" (click)="redo(canvas)">Redo</button>
    <p role="status">{{ status() }}</p>
    <grafloria-diagram-canvas #canvas [engine]="engine"
      style="display:block; height:400px" />
  `,
})
export class AppComponent implements OnDestroy {
  readonly engine = new DiagramEngine();
  readonly busy = signal(false);
  readonly status = signal('Loaded is setup data, not an undo step.');
  private nextPair = 0;

  constructor() {
    const diagram = this.engine.createDiagram('History example');
    const loaded = this.makeNode('loaded', 'Loaded', 60, 60);
    diagram.addNode(loaded);
  }

  private makeNode(id: string, label: string, x: number, y: number): NodeModel {
    const node = new NodeModel({
      id,
      type: 'default',
      position: { x, y },
      size: { width: 140, height: 50 },
    });
    node.setData('label', label);
    return node;
  }

  async addPair(): Promise<void> {
    const pair = ++this.nextPair;
    const y = 140 + (pair - 1) * 70;
    const review = this.makeNode(`review-${pair}`, 'Review', 60, y);
    const ship = this.makeNode(`ship-${pair}`, 'Ship', 260, y);
    await this.run(() => this.engine.commandManager.execute(
      new BatchCommand('Add review and ship', [
        new AddNodeCommand(review),
        new AddNodeCommand(ship),
      ]),
    ));
  }

  async undo(canvas: DiagramCanvasComponent): Promise<void> {
    await this.run(() => canvas.undo());
  }

  async redo(canvas: DiagramCanvasComponent): Promise<void> {
    await this.run(() => canvas.redo());
  }

  private async run(action: () => Promise<void>): Promise<void> {
    if (this.busy()) return;
    this.busy.set(true);
    try {
      await action();
      this.status.set(
        `Can undo: ${this.engine.canUndo()}; can redo: ${this.engine.canRedo()}`,
      );
    } catch (error) {
      this.status.set(error instanceof Error ? error.message : 'Command failed.');
    } finally {
      this.busy.set(false);
    }
  }

  ngOnDestroy(): void {
    this.engine.dispose();
  }
}
```

The canvas's `undo()` and `redo()` methods delegate to its active engine and schedule rendering. The status text reports history availability after a toolbar operation; it is not a subscription to every gesture. Use `engine.canUndo()` and `engine.canRedo()` to drive availability in your own controls.

Batching history is different from batching rendering. `instance.batchUpdate()` applies model mutations as one frame; it does not turn direct model writes into undoable commands.

## Await execution and handle refusal

[`Command`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-commands-classes-a-r#command) permits both synchronous and asynchronous `execute()` and `undo()` implementations. Its default `redo()` re-executes the command. The command manager awaits these operations, so await execution before reading the resulting model or issuing a dependent edit. `execute()`, `undo()` and `redo()` on the manager return `Promise<void>`, not a success flag.

History contains only successfully executed, validated commands that declare themselves undoable through `isUndoable()`. Refusal leaves no new entry:

- If `canExecute(context)` returns `false`, execution rejects before applying the command. For example, `AddNodeCommand` refuses an id already in the diagram. Catch the rejection and report it to the user.
- With real-time validation enabled and strict validation configured, an invalid result is undone and execution rejects. The failed command does not enter history.
- A read-only document refuses execution without throwing; the manager emits `command:refused` and returns. Promise resolution alone therefore does not prove that an edit occurred.

A batch checks every child's `canExecute()` against the pre-batch state. Do not put an action in a batch if it becomes executable only after an earlier child runs. The pair above works because both node ids are absent before execution.

## Keep application state on the same history

An undo changes the engine's live models, not a separate framework snapshot. In controlled mode, the binding returns those changes to your application: Angular writes through `[(nodes)]` and `[(edges)]`; Vue updates `v-model:nodes`; React calls `onNodesChange`.

Keep that return path connected rather than maintaining another undo stack over your specs. See [State and event flow](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/state-and-event-flow) for controlled bindings and [Instance and lifecycle](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/instance-and-lifecycle) for instance ownership.

For a gesture-driven example, open the [live interaction demos](https://grafloria.com/demos/#interaction), drag a node, then press ⌘Z or Ctrl+Z. One drag is one undo step, not one step per position update.

## Related

- [Specs and live models](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/specs-and-live-models) explains the data commands mutate.
- [Add editor controls](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/add-editor-controls) connects application controls to editing.
- [Synchronize editors](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/synchronize-editors) covers sharing document changes between peers.
