Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -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.