import { onScopeDispose, ref, watch, type Ref, type WatchOptions, type WatchSource, type WatchStopHandle, } from 'vue' export type DebouncedWatchCleanup = () => void export type DebouncedWatchSource = WatchSource | WatchSource[] | (() => T) export type DebouncedWatchCallback = ( value: T, oldValue: T | undefined, onCleanup: (cleanup: DebouncedWatchCleanup) => void, ) => void | Promise export interface UseDebouncedWatchOptions extends WatchOptions { /** Debounce interval in milliseconds. */ delay?: number } export interface UseDebouncedWatchReturn { /** Whether a callback is currently scheduled. */ isPending: Ref /** Cancel the pending callback without stopping the watcher. */ cancel: () => void /** Run the pending callback immediately, if one is scheduled. */ flush: () => void /** Stop the watcher and clear pending callbacks. */ stop: WatchStopHandle } /** Watch a Vue source with debounced callback execution and explicit cancel/flush controls. */ export function useDebouncedWatch( source: DebouncedWatchSource, callback: DebouncedWatchCallback, options: UseDebouncedWatchOptions = {}, ): UseDebouncedWatchReturn { const { delay = 300, ...watchOptions } = options const isPending = ref(false) let timer: ReturnType | null = null let hasLatestValue = false let latestValue: T let latestOldValue: T | undefined let callbackCleanup: DebouncedWatchCleanup | null = null let stopped = false function clearTimer(): void { if (timer) { clearTimeout(timer) timer = null } isPending.value = false } function runCallbackCleanup(): void { if (!callbackCleanup) return callbackCleanup() callbackCleanup = null } function cancel(): void { clearTimer() hasLatestValue = false latestOldValue = undefined } function flush(): void { if (!hasLatestValue) return const value = latestValue const oldValue = latestOldValue cancel() runCallbackCleanup() void callback(value, oldValue, (cleanup) => { callbackCleanup = cleanup }) } function schedule(value: T, oldValue: T | undefined): void { cancel() hasLatestValue = true latestValue = value latestOldValue = oldValue isPending.value = true timer = setTimeout(flush, delay) } const stopWatch = watch( source as WatchSource | WatchSource[], (value, oldValue, onCleanup) => { schedule(value as T, oldValue as T | undefined) onCleanup(() => { cancel() runCallbackCleanup() }) }, watchOptions, ) function stop(): void { if (stopped) return stopped = true stopWatch() cancel() runCallbackCleanup() } onScopeDispose(stop) return { isPending, cancel, flush, stop, } }