Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# stack
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 stack
- `pushState(newState)` — pushes a state snapshot without an explicit command
- `undo()` — reverses the most recent command; throws `UndoError(UNDO.EMPTY_STACK)` when empty
- `redo()` — re-applies the most recently undone command; throws `UndoError(UNDO.EMPTY_STACK)` when empty
- `beginTransaction()` — returns a `TransactionHandle` for atomic multi-command batching
- `clear()` — discards all history, keeps current state; emits `undo:clear`
- `reset(newInitial)` — resets state and clears history
- `snapshot()` — returns a serializable `UndoStackState<T>`
- `getUndoStack()` / `getRedoStack()` — copies of internal stacks
- Getters: `current`, `canUndo`, `canRedo`, `undoSize`, `redoSize`, `activeBranchId`
- `EventBusLike` integration — emits `undo:push`, `undo:undo`, `undo:redo`, `undo:clear`
- OTel counters (`undo.push.total`, `undo.undo.total`, `undo.redo.total`) and `undo.stack.depth` histogram
- Injectable `clock` and `idGenerator` via `UndoStackOptions`
## Usage: Basic Example
```ts
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
```ts
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.