Manages undo/redo history for match operations. Maintains two stacks: undo stack and redo stack. When a command is executed, it's pushed to the undo stack and the redo stack is cleared. Commands can be undone/redone using the public methods.

export class UndoRedoManager {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
private isEnabled: boolean = true;

/**
* Create a new undo/redo manager.
* @param maxHistorySize - Maximum number of commands to keep in history (default: 50).
* @source
*/
constructor(private readonly maxHistorySize: number = 50) {}

/**
* Execute a command and add it to undo history.
* Calling this method will:
* 1. Execute the command
* 2. Push it to the undo stack
* 3. Clear the redo stack (since we've made a new action)
* 4. Maintain history size by removing oldest entries
* @param command - The command to execute.
* @source
*/
executeCommand(command: Command): void {
if (!this.isEnabled) return;

command.execute();
this.undoStack.push(command);
this.redoStack = []; // Clear redo stack when new action is performed

// Maintain max history size
if (this.undoStack.length > this.maxHistorySize) {
this.undoStack.shift();
}
}

/**
* Undo the last command.
* @returns Metadata about the undone command, or null if nothing to undo.
* @source
*/
undo(): CommandMetadata | null {
if (this.undoStack.length === 0) return null;

const command = this.undoStack.pop()!;
command.undo();
this.redoStack.push(command);

return command.getMetadata();
}

/**
* Redo the last undone command.
* @returns Metadata about the redone command, or null if nothing to redo.
* @source
*/
redo(): CommandMetadata | null {
if (this.redoStack.length === 0) return null;

const command = this.redoStack.pop()!;
command.execute();
this.undoStack.push(command);

return command.getMetadata();
}

/**
* Check if there are commands available to undo.
* @source
*/
canUndo(): boolean {
return this.undoStack.length > 0;
}

/**
* Check if there are commands available to redo.
* @source
*/
canRedo(): boolean {
return this.redoStack.length > 0;
}

/**
* Get a description of what will be undone (for UI display).
* @source
*/
getUndoDescription(): string | null {
return this.undoStack.at(-1)?.getDescription() ?? null;
}

/**
* Get a description of what will be redone (for UI display).
* @source
*/
getRedoDescription(): string | null {
return this.redoStack.at(-1)?.getDescription() ?? null;
}

/**
* Clear all undo and redo history.
* Use this when operations occur that invalidate the history,
* such as rematch operations that clear the cache.
* @source
*/
clear(): void {
this.undoStack = [];
this.redoStack = [];
}

/**
* Temporarily enable or disable command recording.
* When disabled, executeCommand() becomes a no-op. This is useful
* during operations like initial data loading where we don't want
* to record intermediate states.
* @param enabled - Whether to enable or disable the manager.
* @source
*/
setEnabled(enabled: boolean): void {
this.isEnabled = enabled;
}

/**
* Get information about current history size.
* @source
*/
getHistorySize(): { undo: number; redo: number } {
return {
undo: this.undoStack.length,
redo: this.redoStack.length,
};
}
}

Constructors

Methods

  • Execute a command and add it to undo history. Calling this method will:

    1. Execute the command
    2. Push it to the undo stack
    3. Clear the redo stack (since we've made a new action)
    4. Maintain history size by removing oldest entries

    Parameters

    • command: Command

      The command to execute.

    Returns void

      executeCommand(command: Command): void {
    if (!this.isEnabled) return;

    command.execute();
    this.undoStack.push(command);
    this.redoStack = []; // Clear redo stack when new action is performed

    // Maintain max history size
    if (this.undoStack.length > this.maxHistorySize) {
    this.undoStack.shift();
    }
    }
  • Undo the last command.

    Returns null | CommandMetadata

    Metadata about the undone command, or null if nothing to undo.

      undo(): CommandMetadata | null {
    if (this.undoStack.length === 0) return null;

    const command = this.undoStack.pop()!;
    command.undo();
    this.redoStack.push(command);

    return command.getMetadata();
    }
  • Redo the last undone command.

    Returns null | CommandMetadata

    Metadata about the redone command, or null if nothing to redo.

      redo(): CommandMetadata | null {
    if (this.redoStack.length === 0) return null;

    const command = this.redoStack.pop()!;
    command.execute();
    this.undoStack.push(command);

    return command.getMetadata();
    }
  • Check if there are commands available to undo.

    Returns boolean

      canUndo(): boolean {
    return this.undoStack.length > 0;
    }
  • Check if there are commands available to redo.

    Returns boolean

      canRedo(): boolean {
    return this.redoStack.length > 0;
    }
  • Get a description of what will be undone (for UI display).

    Returns null | string

      getUndoDescription(): string | null {
    return this.undoStack.at(-1)?.getDescription() ?? null;
    }
  • Get a description of what will be redone (for UI display).

    Returns null | string

      getRedoDescription(): string | null {
    return this.redoStack.at(-1)?.getDescription() ?? null;
    }
  • Clear all undo and redo history. Use this when operations occur that invalidate the history, such as rematch operations that clear the cache.

    Returns void

      clear(): void {
    this.undoStack = [];
    this.redoStack = [];
    }
  • Temporarily enable or disable command recording. When disabled, executeCommand() becomes a no-op. This is useful during operations like initial data loading where we don't want to record intermediate states.

    Parameters

    • enabled: boolean

      Whether to enable or disable the manager.

    Returns void

      setEnabled(enabled: boolean): void {
    this.isEnabled = enabled;
    }
  • Get information about current history size.

    Returns { undo: number; redo: number }

      getHistorySize(): { undo: number; redo: number } {
    return {
    undo: this.undoStack.length,
    redo: this.redoStack.length,
    };
    }