# command-merger _(no submodules)_ ## Purpose Implements `CommandMerger`, 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({ value: 0 }); const merger = new CommandMerger({ 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( { timeWindowMs: 500, clock }, (a, b) => a.label === "Type" && b.label === "Type" ); const typeChar = (ch: string) => createCommand("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(options?, predicate?)` | Param | Type | Description | |---|---|---| | `options` | `CommandMergerOptions` | `maxCount`, `timeWindowMs`, `clock` | | `predicate` | `MergePredicate` | `(a, b) => boolean` — default: same label | ### `add(command)` → `Command | 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 | 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.