Files
Codeman/src/utils/debouncer.ts
T
arkonandClaude Opus 4.6 e0a2774d37 perf: implement phase 1-3 performance optimizations
Add implementation plans and code structure analysis for a 3-phase
performance optimization effort. Refactor core modules to reduce
timer overhead, consolidate regex usage, extract exec timeout config,
add debouncer utility, and streamline server/schema validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:39:44 +01:00

175 lines
4.2 KiB
TypeScript

/**
* @fileoverview Debounce utilities to replace manual timer management.
*
* Two variants:
* - `Debouncer` — single debounced operation (replaces timer + clearTimeout pattern)
* - `KeyedDebouncer` — per-key debouncing (replaces Map<string, Timeout> pattern)
*
* Both integrate with CleanupManager via dispose().
*
* @module utils/debouncer
*/
/**
* Single-operation debouncer.
*
* Replaces the common pattern of:
* ```
* private timer: NodeJS.Timeout | null = null;
* debounce(fn) { if (this.timer) clearTimeout(this.timer); this.timer = setTimeout(fn, delay); }
* cancel() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } }
* ```
*
* @example
* ```typescript
* private saveDeb = new Debouncer(500);
*
* onChange() {
* this.saveDeb.schedule(() => this.save());
* }
*
* stop() {
* this.saveDeb.dispose();
* }
* ```
*/
export class Debouncer {
private timer: NodeJS.Timeout | null = null;
constructor(private readonly delayMs: number) {}
/**
* Schedule a debounced callback. Resets the timer on each call.
* If a previous call is pending, it is cancelled.
*/
schedule(fn: () => void): void {
this.cancel();
this.timer = setTimeout(() => {
this.timer = null;
fn();
}, this.delayMs);
}
/** Cancel any pending execution without invoking the callback. */
cancel(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
/** Whether a callback is currently pending. */
get isPending(): boolean {
return this.timer !== null;
}
/**
* Cancel pending callback and flush immediately.
* Useful for shutdown: cancel the timer but run the action now.
*
* @param fn - The flush function to run (typically the same function passed to schedule)
*/
flush(fn: () => void): void {
this.cancel();
fn();
}
/** Alias for cancel() — matches CleanupManager/Disposable convention. */
dispose(): void {
this.cancel();
}
}
/**
* Per-key debouncer for operations that need independent timers per resource.
*
* Replaces the common pattern of:
* ```
* private timers = new Map<string, NodeJS.Timeout>();
* debounce(key, fn) {
* const existing = this.timers.get(key);
* if (existing) clearTimeout(existing);
* this.timers.set(key, setTimeout(() => { this.timers.delete(key); fn(); }, delay));
* }
* ```
*
* @example
* ```typescript
* private fileDebouncers = new KeyedDebouncer(100);
*
* onFileChange(path: string) {
* this.fileDebouncers.schedule(path, () => this.processFile(path));
* }
*
* stop() {
* this.fileDebouncers.dispose();
* }
* ```
*/
export class KeyedDebouncer {
private timers = new Map<string, NodeJS.Timeout>();
constructor(private readonly delayMs: number) {}
/**
* Schedule a debounced callback for a specific key.
* Each key has its own independent timer.
*/
schedule(key: string, fn: () => void): void {
this.cancelKey(key);
this.timers.set(
key,
setTimeout(() => {
this.timers.delete(key);
fn();
}, this.delayMs)
);
}
/** Cancel a pending callback for a specific key. */
cancelKey(key: string): void {
const existing = this.timers.get(key);
if (existing) {
clearTimeout(existing);
this.timers.delete(key);
}
}
/** Whether a callback is pending for a specific key. */
has(key: string): boolean {
return this.timers.has(key);
}
/** Number of active timers. */
get size(): number {
return this.timers.size;
}
/** Get all currently active keys. */
keys(): IterableIterator<string> {
return this.timers.keys();
}
/** Cancel all pending callbacks. */
dispose(): void {
for (const timer of this.timers.values()) {
clearTimeout(timer);
}
this.timers.clear();
}
/**
* Cancel all pending callbacks and run a flush function for each active key.
* Useful for shutdown: cancel timers but run the action for each pending key.
*
* @param fn - Called once per active key with the key as argument
*/
flushAll(fn: (key: string) => void): void {
const activeKeys = Array.from(this.timers.keys());
this.dispose();
for (const key of activeKeys) {
fn(key);
}
}
}