first commit
This commit is contained in:
107
docs/foundation/flow/undo/src/merge/command-merger.md
Normal file
107
docs/foundation/flow/undo/src/merge/command-merger.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# command-merger
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements `CommandMerger<T>`, a utility for coalescing high-frequency user actions — such as rapid keystroke edits, slider movements, or drag operations — into a single undoable step. Commands are accumulated in a pending buffer and flushed into a single composite command when a count or time window closes, or when two consecutive commands fail the merge predicate.
|
||||
|
||||
The injectable `Clock` makes time-window behavior deterministic in tests without needing real timers or `setTimeout`. The default merge predicate merges consecutive commands with identical labels; callers can supply any custom predicate.
|
||||
|
||||
## Features
|
||||
|
||||
- `add(command)` — adds to buffer; returns merged command if window closes, else `undefined`
|
||||
- `flush()` — immediately flushes buffer; returns merged command or `undefined` if empty
|
||||
- `reset()` — discards pending commands without returning them
|
||||
- `canMerge(a, b)` — checks predicate without mutating state
|
||||
- Getters: `hasPending`, `pendingCount`
|
||||
- `maxCount` window — flushes after accumulating N commands
|
||||
- `timeWindowMs` window — flushes when elapsed time exceeds threshold
|
||||
- Default predicate: merge consecutive commands with the same `label`
|
||||
- Injectable `Clock` for deterministic tests
|
||||
- Options validated with `CommandMergerOptionsSchema`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CommandMerger } from "@suaveplan/undo/merge/command-merger";
|
||||
import { createCommand } from "@suaveplan/undo/command/command";
|
||||
import { UndoStack } from "@suaveplan/undo/stack/stack";
|
||||
|
||||
interface Counter { value: number }
|
||||
const stack = new UndoStack<Counter>({ value: 0 });
|
||||
const merger = new CommandMerger<Counter>({ maxCount: 3 });
|
||||
|
||||
for (const delta of [1, 1, 1, 2]) {
|
||||
const cmd = createCommand("Inc", (s) => ({ value: s.value + delta }), (s) => ({ value: s.value - delta }));
|
||||
const flushed = merger.add(cmd);
|
||||
if (flushed) stack.push(flushed);
|
||||
}
|
||||
|
||||
// Flush remaining
|
||||
const last = merger.flush();
|
||||
if (last) stack.push(last);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CommandMerger } from "@suaveplan/undo/merge/command-merger";
|
||||
import { createCommand } from "@suaveplan/undo/command/command";
|
||||
|
||||
interface Doc { text: string }
|
||||
|
||||
let time = 0;
|
||||
const clock = () => ++time;
|
||||
|
||||
// Custom predicate: only merge "Type" commands
|
||||
const merger = new CommandMerger<Doc>(
|
||||
{ timeWindowMs: 500, clock },
|
||||
(a, b) => a.label === "Type" && b.label === "Type"
|
||||
);
|
||||
|
||||
const typeChar = (ch: string) =>
|
||||
createCommand<Doc>("Type", (d) => ({ text: d.text + ch }), (d) => ({ text: d.text.slice(0, -1) }));
|
||||
|
||||
merger.add(typeChar("H")); // pending
|
||||
merger.add(typeChar("i")); // pending
|
||||
time = 600; // advance clock past window
|
||||
const flushed = merger.add(typeChar("!")); // triggers time-window flush
|
||||
// flushed = merged "Hi" command; "!" starts new pending
|
||||
const remaining = merger.flush(); // flushes "!"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new CommandMerger<T>(options?, predicate?)`
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `options` | `CommandMergerOptions` | `maxCount`, `timeWindowMs`, `clock` |
|
||||
| `predicate` | `MergePredicate<T>` | `(a, b) => boolean` — default: same label |
|
||||
|
||||
### `add(command)` → `Command<T> | undefined`
|
||||
|
||||
Returns a flushed composite when: time window closes, count limit reached, or predicate fails for the new command against the last pending. Returns `undefined` when the command is accumulated.
|
||||
|
||||
### `flush()` → `Command<T> | undefined`
|
||||
|
||||
Returns the merged command for all pending items, or `undefined` if nothing is pending.
|
||||
|
||||
### `reset()` → `void`
|
||||
|
||||
Discards all pending commands.
|
||||
|
||||
### `canMerge(a, b)` → `boolean`
|
||||
|
||||
Calls the merge predicate. Non-mutating.
|
||||
|
||||
### Getters
|
||||
|
||||
- `hasPending` — `true` when there are accumulated commands
|
||||
- `pendingCount` — number of commands in the buffer
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `_doFlush` method returns the single command directly (without wrapping in `composeCommands`) when the buffer contains only one entry, avoiding unnecessary composite overhead. The time window is measured from the first `add` call after a flush. Non-mergeable commands trigger an implicit flush of the existing buffer before the new command starts a fresh buffer — so `add` can return the old buffer's merged command in this case.
|
||||
Reference in New Issue
Block a user