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 stackpushState(newState)— pushes a state snapshot without an explicit commandundo()— reverses the most recent command; throwsUndoError(UNDO.EMPTY_STACK)when emptyredo()— re-applies the most recently undone command; throwsUndoError(UNDO.EMPTY_STACK)when emptybeginTransaction()— returns aTransactionHandlefor atomic multi-command batchingclear()— discards all history, keeps current state; emitsundo:clearreset(newInitial)— resets state and clears historysnapshot()— returns a serializableUndoStackState<T>getUndoStack()/getRedoStack()— copies of internal stacks- Getters:
current,canUndo,canRedo,undoSize,redoSize,activeBranchId EventBusLikeintegration — emitsundo:push,undo:undo,undo:redo,undo:clear- OTel counters (
undo.push.total,undo.undo.total,undo.redo.total) andundo.stack.depthhistogram - Injectable
clockandidGeneratorviaUndoStackOptions
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.