← Command lesson
Lab

Command

type Card = { name: string; x: number };
type Change = { card: Card; inverse: Command };

// A request as a value: it knows how to apply itself to a card.
interface Command {
  label: string;
  // The changed card and the command that reverses it, or null: no change.
  execute(card: Card): Change | null;
}

function rename(name: string): Command {
  return {
    label: 'Rename',
    execute: (card) =>
      card.name === name
        ? null
        : { card: { ...card, name }, inverse: rename(card.name) },
  };
}

function moveTo(x: number): Command {
  return {
    label: 'Move',
    execute: (card) =>
      card.x === x ? null : { card: { ...card, x }, inverse: moveTo(card.x) },
  };
}

type Entry = { forward: Command; inverse: Command };

// The invoker: it runs every request and owns the card and its history.
class Editor {
  card: Card;
  past: Entry[] = [];
  future: Entry[] = [];

  constructor(card: Card) {
    this.card = card;
  }

  execute(command: Command): boolean {
    const change = command.execute(this.card);
    if (!change) return false;
    this.card = change.card;
    this.past.push({ forward: command, inverse: change.inverse });
    this.future = [];
    return true;
  }

  undo(): boolean {
    const entry = this.past.pop();
    if (!entry) return false;
    this.card = entry.inverse.execute(this.card)?.card ?? this.card;
    this.future.push(entry);
    return true;
  }
}

const editor = new Editor({ name: 'Draft', x: 20 });

// The rename dialog opens, and the toolbar makes its request now.
const launch = rename('Launch');
// Before the dialog's OK, the palette renames and moves the card.
editor.execute(rename('Release'));
editor.execute(moveTo(80));
// OK: the editor runs the toolbar's request. Then Undo, once.
editor.execute(launch);
editor.undo();

const afterUndo = editor.card;
const past = editor.past.map((entry) => entry.forward.label);
const future = editor.future.map((entry) => entry.forward.label);
console.log(`${afterUndo.name} x=${afterUndo.x}`);
console.log(`past: ${past.join(', ')} | future: ${future.join(', ')}`);
A request that waits

A request can wait. The card keeps changing.

Waiting for a request
Editor
Observed card—x = —

Position follows the recorded card.x. The queued request and the card have separate lifetimes.

Capture the inverse when the command runs

Step into a request to follow the controller and its history.

PastUndo takes the newest entry
History not observed yet
FutureRedo takes the newest entry
History not observed yet
Inspect the observed action trail

Room geometry is authored context. Glow illustrates recorded brightness, not measured light. History positions are slots, not object identities. Program Undo/Redo is separate from debugger Back/Step.