first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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.