Initial Commit
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# history-compressor
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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 configurable `compressionDepth` and `maxHistorySize`
|
||||
- `compress(stack)` → `CompressionResult & { stack: UndoStack<T> }` — squashes and returns a new stack
|
||||
- `compressHistory<T>(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<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
|
||||
|
||||
```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<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`
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user