Files
2026-05-27 23:47:53 +01:00

4.9 KiB

stack

(no submodules)

Purpose

Implements UndoStack<T>, a synchronous linear undo/redo stack with configurable memory limits, atomic transaction support, and EventBusLike integration for reactive applications. It is the core data structure used by most higher-level modules in @suaveplan/undo.

Commands are stored as StackEntry<T> pairs capturing both the command and the state immediately before execution, enabling exact rollback without re-replaying history. All mutations respect maxEntries and maxMemoryBytes limits, trimming the oldest entries and calling an onTruncate callback when limits are exceeded.

Features

  • push(command) — executes command, pushes to undo stack, clears redo stack
  • pushState(newState) — pushes a state snapshot without an explicit command
  • undo() — reverses the most recent command; throws UndoError(UNDO.EMPTY_STACK) when empty
  • redo() — re-applies the most recently undone command; throws UndoError(UNDO.EMPTY_STACK) when empty
  • beginTransaction() — returns a TransactionHandle for atomic multi-command batching
  • clear() — discards all history, keeps current state; emits undo:clear
  • reset(newInitial) — resets state and clears history
  • snapshot() — returns a serializable UndoStackState<T>
  • getUndoStack() / getRedoStack() — copies of internal stacks
  • Getters: current, canUndo, canRedo, undoSize, redoSize, activeBranchId
  • EventBusLike integration — emits undo:push, undo:undo, undo:redo, undo:clear
  • OTel counters (undo.push.total, undo.undo.total, undo.redo.total) and undo.stack.depth histogram
  • Injectable clock and idGenerator via UndoStackOptions

Usage: Basic Example

import { UndoStack } from "@suaveplan/undo/stack/stack";
import { createCommand } from "@suaveplan/undo/command/command";

interface Counter { value: number }

const stack = new UndoStack<Counter>({ value: 0 });

const inc = createCommand("Inc", (s) => ({ value: s.value + 1 }), (s) => ({ value: s.value - 1 }));
stack.push(inc);
console.log(stack.current.value); // 1

stack.undo();
console.log(stack.current.value); // 0

stack.redo();
console.log(stack.current.value); // 1

Usage: Advanced Example

import { UndoStack } from "@suaveplan/undo/stack/stack";
import { createCommand } from "@suaveplan/undo/command/command";
import { EventBus } from "@suaveplan/events";
import type { UndoEventMap } from "@suaveplan/undo/types/undo.types";

interface Doc { title: string }

const bus = new EventBus<UndoEventMap>();
bus.on("undo:push", ({ commandId, undoSize }) => {
  console.log(`Pushed ${commandId}, depth=${undoSize}`);
});

const stack = new UndoStack<Doc>(
  { title: "Draft" },
  { maxEntries: 10, onTruncate: (removed) => console.log("Dropped", removed.length) },
  bus
);

// Transaction: batch two commands into one atomic undo unit
const tx = stack.beginTransaction();
tx.add(createCommand("Title", (d) => ({ ...d, title: "Final" }), (d) => ({ ...d, title: "Draft" })));
tx.add(createCommand("Suffix", (d) => ({ ...d, title: d.title + "!" }), (d) => ({ ...d, title: d.title.slice(0, -1) })));
tx.commit(); // pushes both atomically

console.log(stack.current.title); // "Final!"
stack.undo(); // undoes "Suffix"
stack.undo(); // undoes "Title"
console.log(stack.current.title); // "Draft"

API Reference

new UndoStack<T>(initial, options?, bus?)

Param Type Description
initial T Initial state
options UndoStackOptions Optional limits and injections
bus EventBusLike<UndoEventMap> Optional event bus

Options are validated with UndoStackOptionsSchema. Throws ZodError on invalid options.

push(command) → T

Executes command and stores it. Clears redo stack.

pushState(newState) → T

Pushes a synthetic command wrapping the state change.

undo() → T

Pops from undo stack, applies inverse. Throws UndoError(UNDO.EMPTY_STACK) if empty.

redo() → T

Pops from redo stack, re-applies command. Throws UndoError(UNDO.EMPTY_STACK) if empty.

beginTransaction() → TransactionHandle<T>

Opens a transaction. Throws UndoError(UNDO.TRANSACTION_ACTIVE) if one is already open. Handle methods throw UndoError(UNDO.TRANSACTION_INACTIVE) after commit/rollback.

snapshot() → UndoStackState<T>

Returns { current, undoStack, redoStack, undoSize, redoSize }.

Implementation Notes

The undo stack stores stateBefore in each entry rather than re-executing the full history chain, making undo() O(1). Trimming removes the oldest entries (front of array) to preserve the most recent history. Transactions use a simple "pending commands" buffer flushed to push() on commit. All class members use protected rather than private to enable SelectiveUndoStack and other subclasses to access internals without re-implementing the whole class.