# history-compressor _(no submodules)_ ## Purpose Provides `HistoryCompressor` and the standalone `compressHistory` function for reducing memory usage in long-running undo histories. Old entries beyond a configurable `compressionDepth` are squashed by label using `composeCommands`, grouping consecutive same-label commands into a single composite command. An optional `maxHistorySize` further limits the total retained entries by dropping the oldest compressed entries. Compression is non-destructive with respect to the current application state — the rebuilt stack always reflects the same `current` value as the original stack. The `CompressionResult` reports how many entries were squashed and dropped, enabling callers to decide whether re-compression is worthwhile. ## Features - `HistoryCompressor` class with configurable `compressionDepth` and `maxHistorySize` - `compress(stack)` → `CompressionResult & { stack: UndoStack }` — squashes and returns a new stack - `compressHistory(stack, options?)` — standalone function equivalent - Groups consecutive same-label entries in the old region into composite commands - Respects `maxHistorySize` by dropping the oldest entries beyond the limit - OTel: increments `undo.compress.total`; records `undo.compress.ratio` histogram - Validates options with `HistoryCompressorOptionsSchema` ## Usage: Basic Example ```ts import { compressHistory } from "@suaveplan/undo/compression/history-compressor"; import { UndoStack } from "@suaveplan/undo/stack/stack"; import { createCommand } from "@suaveplan/undo/command/command"; interface Counter { value: number } const stack = new UndoStack({ value: 0 }); for (let i = 0; i < 100; i++) { stack.push(createCommand("Inc", (s) => ({ value: s.value + 1 }), (s) => ({ value: s.value - 1 }))); } const { stack: compressed, squashedCount, newSize } = compressHistory(stack, { compressionDepth: 20, maxHistorySize: 30, }); console.log(squashedCount); // number of entries squashed into composites console.log(newSize); // entries in the compressed stack console.log(compressed.current.value); // same as original stack ``` ## Usage: Advanced Example ```ts import { HistoryCompressor } from "@suaveplan/undo/compression/history-compressor"; import { UndoStack } from "@suaveplan/undo/stack/stack"; import { createCommand } from "@suaveplan/undo/command/command"; interface Doc { version: number } const compressor = new HistoryCompressor({ compressionDepth: 50, maxHistorySize: 100, }); const stack = new UndoStack({ version: 0 }); for (let i = 0; i < 200; i++) { stack.push(createCommand("Bump", (d) => ({ version: d.version + 1 }), (d) => ({ version: d.version - 1 }))); } const result = compressor.compress(stack); console.log(result.squashedCount); // entries merged console.log(result.droppedCount); // entries removed for maxHistorySize console.log(result.newSize); // final stack depth console.log(result.stack.current.version); // 200 — state preserved // Compression is idempotent within depth: const second = compressor.compress(result.stack); console.log(second.squashedCount); // 0 (nothing to squash) ``` ## API Reference ### `new HistoryCompressor(options?)` Options validated with `HistoryCompressorOptionsSchema`. | Option | Type | Default | Description | |---|---|---|---| | `compressionDepth` | `number` | `50` | Entries to keep uncompressed at the top | | `maxHistorySize` | `number` | `undefined` | Maximum entries after compression | ### `compress(stack)` → `CompressionResult & { stack: UndoStack }` Returns a new `UndoStack` with compressed history. Does not mutate the original stack. ### `compressHistory(stack, options?)` → `CompressionResult & { stack: UndoStack }` Standalone version. Same semantics as `compress`. ### `CompressionResult` ```ts interface CompressionResult { squashedCount: number; // original entries merged into fewer composites droppedCount: number; // entries removed by maxHistorySize newSize: number; // total entries in the result stack } ``` ## Implementation Notes The squash pass groups consecutive same-label entries — only adjacent runs with matching labels are merged. Non-adjacent groups of the same label are treated independently. The rebuilt stack directly inserts `StackEntry` objects without re-executing commands, so the `current` pointer is set from the last entry's post-execute state. This avoids re-running potentially expensive execute closures at compression time.