Initial Commit
This commit is contained in:
120
packages/foundation/flow/undo/src/stack/stack.md
Normal file
120
packages/foundation/flow/undo/src/stack/stack.md
Normal 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.
|
||||
Reference in New Issue
Block a user