first commit
This commit is contained in:
110
docs/foundation/flow/undo/src/async/async-stack.md
Normal file
110
docs/foundation/flow/undo/src/async/async-stack.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 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 redo
|
||||
- `undo()` → `Promise<T>` — reverses most recent command; throws `UndoError(UNDO.EMPTY_STACK)` if empty
|
||||
- `redo()` → `Promise<T>` — re-applies most recently undone command; throws `UndoError(UNDO.EMPTY_STACK)` if empty
|
||||
- `beginTransaction()` → `AsyncTransactionHandle<T>` — atomic async batch
|
||||
- `clear()` / `reset(newInitial)` — history management
|
||||
- `snapshot()` — serializable `UndoStackState<T>`
|
||||
- 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<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
|
||||
|
||||
```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<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.
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user