logancyang_obsidian-copilot/src/utils/debounce.ts
Zero Liu d6255374cb
chore(deps): swap unmaintained/legacy deps per e18e module-replacements (#2447)
- npm-run-all → npm-run-all2 (dev script uses run-p)
- lint-staged → nano-staged (config block + husky hook)
- lodash.debounce → local src/utils/debounce.ts with cancel/flush/leading/trailing
- dotenv → process.loadEnvFile in integration test bootstrap
- eslint-plugin-react → @eslint-react/eslint-plugin (flat-config migration)
- js-yaml → yaml (mock + jest CJS moduleNameMapper for jsdom)
- crypto-js → pure-JS md5/sha256 in src/utils/hash.ts producing byte-identical
  digests so all on-disk cache keys (PDF, file, project, search index)
  remain valid across the upgrade — no migration or re-parse cost

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 01:12:52 -07:00

89 lines
2.3 KiB
TypeScript

/**
* Returned by {@link debounce}. Calling it schedules the underlying function,
* with `cancel` and `flush` controls modeled after `lodash.debounce`.
*/
export interface DebouncedFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): ReturnType<T> | undefined;
cancel(): void;
flush(): ReturnType<T> | undefined;
}
export interface DebounceOptions {
leading?: boolean;
trailing?: boolean;
}
/**
* Delay-invoking `func` until `wait` ms have elapsed since the last call.
* Supports the `leading` / `trailing` options and `cancel` / `flush` methods
* used by the `lodash.debounce` API the codebase relied on previously.
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
options: DebounceOptions = {}
): DebouncedFunction<T> {
const leading = options.leading ?? false;
const trailing = options.trailing ?? true;
let timeoutId: number | undefined;
let lastArgs: Parameters<T> | undefined;
let lastResult: ReturnType<T> | undefined;
let hasPendingTrailingCall = false;
const invoke = (): ReturnType<T> | undefined => {
const args = lastArgs;
lastArgs = undefined;
hasPendingTrailingCall = false;
if (!args) return lastResult;
lastResult = func(...args) as ReturnType<T>;
return lastResult;
};
const startTimer = () => {
timeoutId = window.setTimeout(() => {
timeoutId = undefined;
if (trailing && hasPendingTrailingCall) {
invoke();
}
}, wait);
};
const debounced = ((...args: Parameters<T>) => {
lastArgs = args;
hasPendingTrailingCall = true;
const isFirstCallInWindow = timeoutId === undefined;
if (timeoutId !== undefined) {
window.clearTimeout(timeoutId);
}
if (isFirstCallInWindow && leading) {
invoke();
}
startTimer();
return lastResult;
}) as DebouncedFunction<T>;
debounced.cancel = () => {
if (timeoutId !== undefined) {
window.clearTimeout(timeoutId);
timeoutId = undefined;
}
lastArgs = undefined;
hasPendingTrailingCall = false;
};
debounced.flush = () => {
if (timeoutId === undefined) return lastResult;
window.clearTimeout(timeoutId);
timeoutId = undefined;
if (hasPendingTrailingCall) {
return invoke();
}
return lastResult;
};
return debounced;
}