Memento-based undo/redo with full encounter snapshots. Undo stack capped at 50 entries, persisted to localStorage. Triggered via buttons in the top bar (inboard of turn navigation) and keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z, Cmd on Mac, case-insensitive key matching). Clear encounter resets both stacks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
3.4 KiB
Markdown
84 lines
3.4 KiB
Markdown
# Data Model: Undo/Redo
|
|
|
|
**Feature**: 037-undo-redo
|
|
**Date**: 2026-03-26
|
|
|
|
## Entities
|
|
|
|
### UndoRedoState
|
|
|
|
Represents the complete undo/redo history for an encounter session.
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| undoStack | Encounter[] | Ordered list of encounter snapshots, most recent last. Max 50 entries. |
|
|
| redoStack | Encounter[] | Ordered list of encounter snapshots accumulated by undo operations. Cleared on any new action. |
|
|
|
|
### Encounter (existing, unchanged)
|
|
|
|
Each stack entry is a full `Encounter` snapshot as defined in `packages/domain/src/types.ts`. No schema changes to the encounter type.
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| combatants | Combatant[] | Ordered list of combatants |
|
|
| activeIndex | number | Index of the active combatant |
|
|
| roundNumber | number | Current round number |
|
|
|
|
## State Transitions
|
|
|
|
### pushUndo(state, snapshot) -> UndoRedoState
|
|
|
|
Push a snapshot onto the undo stack. If the stack exceeds 50 entries, drop the oldest (index 0). Clear the redo stack.
|
|
|
|
**Precondition**: snapshot is a valid Encounter
|
|
**Postcondition**: undoStack length <= 50, redoStack is empty
|
|
|
|
### undo(state, currentEncounter) -> { state: UndoRedoState, encounter: Encounter } | DomainError
|
|
|
|
Pop the most recent snapshot from the undo stack. Push the current encounter onto the redo stack. Return the popped snapshot as the new current encounter.
|
|
|
|
**Precondition**: undoStack is non-empty
|
|
**Postcondition**: undoStack length decremented by 1, redoStack length incremented by 1
|
|
**Error**: "nothing-to-undo" if undoStack is empty
|
|
|
|
### redo(state, currentEncounter) -> { state: UndoRedoState, encounter: Encounter } | DomainError
|
|
|
|
Pop the most recent snapshot from the redo stack. Push the current encounter onto the undo stack. Return the popped snapshot as the new current encounter.
|
|
|
|
**Precondition**: redoStack is non-empty
|
|
**Postcondition**: redoStack length decremented by 1, undoStack length incremented by 1
|
|
**Error**: "nothing-to-redo" if redoStack is empty
|
|
|
|
### clearHistory() -> UndoRedoState
|
|
|
|
Reset both stacks to empty. Used when the encounter is cleared.
|
|
|
|
**Postcondition**: undoStack and redoStack are both empty
|
|
|
|
## Persistence
|
|
|
|
### Storage Keys
|
|
|
|
| Key | Content | Format |
|
|
|-----|---------|--------|
|
|
| `initiative:encounter:undo` | Undo stack | JSON array of serialized Encounter objects |
|
|
| `initiative:encounter:redo` | Redo stack | JSON array of serialized Encounter objects |
|
|
|
|
### Serialization
|
|
|
|
Stacks are serialized as JSON arrays of `Encounter` objects, identical to the existing encounter serialization format. On load, each entry is validated using the same rehydration logic as `loadEncounter()`.
|
|
|
|
### Failure Modes
|
|
|
|
- **localStorage quota exceeded**: Stacks continue in-memory; persistence is best-effort. Silently swallow write errors (matching existing encounter persistence pattern).
|
|
- **Corrupt data on load**: Start with empty stacks. Log no error (matching existing pattern).
|
|
- **Schema mismatch after upgrade**: Invalid entries are dropped during rehydration; stacks may be shorter than persisted but never contain invalid data.
|
|
|
|
## Invariants
|
|
|
|
1. `undoStack.length <= 50` at all times
|
|
2. `redoStack` is empty after any non-undo/redo action
|
|
3. `undoStack.length + redoStack.length` represents the total history depth (not capped as a whole — redo can grow up to 50 if all actions are undone)
|
|
4. Each stack entry is a valid, complete `Encounter` snapshot
|
|
5. Undo followed by redo returns the encounter to the exact same state
|