4.5 KiB
history-compressor
(no submodules)
Purpose
Provides HistoryCompressor<T> 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<T>class with configurablecompressionDepthandmaxHistorySizecompress(stack)→CompressionResult & { stack: UndoStack<T> }— squashes and returns a new stackcompressHistory<T>(stack, options?)— standalone function equivalent- Groups consecutive same-label entries in the old region into composite commands
- Respects
maxHistorySizeby dropping the oldest entries beyond the limit - OTel: increments
undo.compress.total; recordsundo.compress.ratiohistogram - Validates options with
HistoryCompressorOptionsSchema
Usage: Basic Example
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<Counter>({ 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
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<Doc>({
compressionDepth: 50,
maxHistorySize: 100,
});
const stack = new UndoStack<Doc>({ 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<T>(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<T> }
Returns a new UndoStack with compressed history. Does not mutate the original stack.
compressHistory<T>(stack, options?) → CompressionResult & { stack: UndoStack<T> }
Standalone version. Same semantics as compress.
CompressionResult
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.