# async-stack ## Purpose Implements `AsyncUndoStack`, the asynchronous counterpart to `UndoStack`. All mutation methods (`push`, `undo`, `redo`) return `Promise`, 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`). ## Features - `push(command)` → `Promise` — executes async command, pushes to stack, clears redo - `undo()` → `Promise` — reverses most recent command; throws `UndoError(UNDO.EMPTY_STACK)` if empty - `redo()` → `Promise` — re-applies most recently undone command; throws `UndoError(UNDO.EMPTY_STACK)` if empty - `beginTransaction()` → `AsyncTransactionHandle` — atomic async batch - `clear()` / `reset(newInitial)` — history management - `snapshot()` — serializable `UndoStackState` - Getters: `current`, `canUndo`, `canRedo`, `undoSize`, `redoSize`, `activeBranchId` - `EventBusLike` integration — emits `undo:push`, `undo:undo`, `undo:redo`, `undo:clear` - OTel counters and histogram with `stack_type: 'async'` attribute - `maxEntries` and `maxMemoryBytes` limits with `onTruncate` callback ## Usage: Basic Example ```ts import { AsyncUndoStack } from "@suaveplan/undo/async/async-stack"; import { createAsyncCommand } from "@suaveplan/undo/async/async-command"; interface Counter { value: number } const stack = new AsyncUndoStack({ 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 ```ts 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({ 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("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(initial, options?, bus?)` Options validated with `UndoStackOptionsSchema`. Throws `ZodError` on invalid options. ### `push(command)` → `Promise` Executes `AsyncCommand`, stores entry. Command failures wrapped in `UndoError(UNDO.COMMAND_FAILED)`. ### `undo()` → `Promise` Restores `stateBefore` directly (O(1)). Throws `UndoError(UNDO.EMPTY_STACK)` if empty. ### `redo()` → `Promise` Re-executes stored execute closure. Throws `UndoError(UNDO.EMPTY_STACK)` if empty. ### `beginTransaction()` → `AsyncTransactionHandle` Throws `UndoError(UNDO.TRANSACTION_ACTIVE)` if already open. ```ts interface AsyncTransactionHandle { add(command: AsyncCommand): void; commit(): Promise; rollback(): Promise; } ``` ### `snapshot()` → `UndoStackState` 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.