For issue #380 (AC-2, AC-3, AC-4 + NFR debounce). The search input is now the single fetch trigger. The dropdown's searchQuery reactivity calls onSearch on every change — whether sourced from the editor mirror or the user's own input. PersonMentionEditor debounces these calls at 150 ms, short-circuits on empty queries (no fetch, items cleared), and tears down pending timers on destroy. The Tiptap suggestion plugin's items() now returns [] — per-keystroke fetches in the editor are gone. The same /api/persons?q= endpoint is used; the difference is in when and how often the request fires. Adds a cancel() method to the debounce utility so destroyed editors don't leave trailing fetches alive (which previously polluted the test ledger and would have wasted bandwidth in production tab-close races). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
26 lines
879 B
TypeScript
26 lines
879 B
TypeScript
/**
|
|
* Returns a debounced version of fn that delays invocation until after
|
|
* `delay` ms have elapsed since the last call. The returned function
|
|
* exposes a `cancel()` method that clears any pending invocation —
|
|
* essential when the host context (a destroyed component, an unmounted
|
|
* editor) shouldn't fire the trailing call.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function debounce<T extends (...args: any[]) => void>(
|
|
fn: T,
|
|
delay: number
|
|
): T & { cancel: () => void } {
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
const wrapped = ((...args: Parameters<T>) => {
|
|
if (timer !== undefined) clearTimeout(timer);
|
|
timer = setTimeout(() => fn(...args), delay);
|
|
}) as T & { cancel: () => void };
|
|
wrapped.cancel = () => {
|
|
if (timer !== undefined) {
|
|
clearTimeout(timer);
|
|
timer = undefined;
|
|
}
|
|
};
|
|
return wrapped;
|
|
}
|