4.4 KiB
async-stack
Purpose
Implements AsyncUndoStack<T>, the asynchronous counterpart to UndoStack<T>. All mutation methods (push, undo, redo) return Promise<T>, enabling undo history management for operations with async side-effects — such as API mutations, database writes, or filesystem changes that must be reversible.
Command failures during push, undo, and redo are caught and re-thrown as UndoError(UNDO.COMMAND_FAILED), ensuring callers always receive a structured error type regardless of what the underlying async command throws. The transaction API is also asynchronous (commit() returns Promise<void>).
Features
push(command)→Promise<T>— executes async command, pushes to stack, clears redoundo()→Promise<T>— reverses most recent command; throwsUndoError(UNDO.EMPTY_STACK)if emptyredo()→Promise<T>— re-applies most recently undone command; throwsUndoError(UNDO.EMPTY_STACK)if emptybeginTransaction()→AsyncTransactionHandle<T>— atomic async batchclear()/reset(newInitial)— history managementsnapshot()— serializableUndoStackState<T>- Getters:
current,canUndo,canRedo,undoSize,redoSize,activeBranchId EventBusLikeintegration — emitsundo:push,undo:undo,undo:redo,undo:clear- OTel counters and histogram with
stack_type: 'async'attribute maxEntriesandmaxMemoryByteslimits withonTruncatecallback
Usage: Basic Example
import { AsyncUndoStack } from "@suaveplan/undo/async/async-stack";
import { createAsyncCommand } from "@suaveplan/undo/async/async-command";
interface Counter { value: number }
const stack = new AsyncUndoStack<Counter>({ value: 0 });
const inc = createAsyncCommand(
"Increment",
async (s) => ({ value: s.value + 1 }),
async (s) => ({ value: s.value - 1 }),
);
await stack.push(inc);
console.log(stack.current.value); // 1
await stack.undo();
console.log(stack.current.value); // 0
Usage: Advanced Example
import { AsyncUndoStack } from "@suaveplan/undo/async/async-stack";
import { createAsyncCommand } from "@suaveplan/undo/async/async-command";
import { UNDO_CODES, UndoError } from "@suaveplan/undo/errors/undo-error";
interface Doc { title: string }
const stack = new AsyncUndoStack<Doc>({ title: "Draft" }, { maxEntries: 50 });
// Transaction: commit two commands atomically
const tx = stack.beginTransaction();
tx.add(createAsyncCommand("Title", async (d) => ({ ...d, title: "Final" }), async (d) => ({ ...d, title: "Draft" })));
tx.add(createAsyncCommand("Bang", async (d) => ({ ...d, title: d.title + "!" }), async (d) => ({ ...d, title: d.title.replace("!", "") })));
await tx.commit();
console.log(stack.current.title); // "Final!"
// Error handling
const failCmd = createAsyncCommand<Doc>("Fail", async () => { throw new Error("network"); }, async (d) => d);
try {
await stack.push(failCmd);
} catch (err) {
if (err instanceof UndoError && err.code === UNDO_CODES.COMMAND_FAILED) {
console.error("Push failed:", err.cause?.message); // "network"
}
}
API Reference
new AsyncUndoStack<T>(initial, options?, bus?)
Options validated with UndoStackOptionsSchema. Throws ZodError on invalid options.
push(command) → Promise<T>
Executes AsyncCommand, stores entry. Command failures wrapped in UndoError(UNDO.COMMAND_FAILED).
undo() → Promise<T>
Restores stateBefore directly (O(1)). Throws UndoError(UNDO.EMPTY_STACK) if empty.
redo() → Promise<T>
Re-executes stored execute closure. Throws UndoError(UNDO.EMPTY_STACK) if empty.
beginTransaction() → AsyncTransactionHandle<T>
Throws UndoError(UNDO.TRANSACTION_ACTIVE) if already open.
interface AsyncTransactionHandle<T> {
add(command: AsyncCommand<T>): void;
commit(): Promise<void>;
rollback(): Promise<void>;
}
snapshot() → UndoStackState<T>
Returns { current, undoStack, redoStack, undoSize, redoSize }.
Implementation Notes
The AsyncUndoStack stores stateBefore in each entry (same pattern as UndoStack) so that undo() can restore state in O(1) without re-awaiting the original async command's undo path. The stored command.execute closure returns the post-push current state captured at push time, enabling redo() to restore the state without re-running external side-effects. This is intentional: redo restores the recorded state, not re-executes the network call.