import { useEffect, useRef, useState } from 'react' export const ASYNC_SEARCH_DEBOUNCE_MS = 250 type UseAsyncSearchOptionsResult = { options: T[] loading: boolean failed: boolean } type UseAsyncSearchOptionsArgs = { open: boolean query: string search: (query: string) => Promise isQueryReady?: (query: string) => boolean } /** * E1 — the shared async-search-with-debounce effect (requestId guard against * out-of-order responses, loading/failed states, per-consumer query gate). * The server action is injected as a prop by the consumer, keeping this module * client-safe; each consumer keeps its own filtering/grouping/pinned options. * Contract quirks, preserved from the three original copies: a not-ready query * resets options/loading/failed without invalidating an in-flight request * (a stale response still lands after the reset), and the search receives the * trimmed query. */ export function useAsyncSearchOptions({ open, query, search, isQueryReady = () => true, }: UseAsyncSearchOptionsArgs): UseAsyncSearchOptionsResult { const [options, setOptions] = useState([]) const [loading, setLoading] = useState(false) const [failed, setFailed] = useState(false) const requestId = useRef(0) const searchRef = useRef(search) const isQueryReadyRef = useRef(isQueryReady) searchRef.current = search isQueryReadyRef.current = isQueryReady useEffect(() => { if (!open) return if (!isQueryReadyRef.current(query)) { setOptions([]) setLoading(false) setFailed(false) return } const currentRequestId = ++requestId.current const timeout = window.setTimeout(() => { setLoading(true) setFailed(false) void searchRef .current(query.trim()) .then((nextOptions) => { if (requestId.current !== currentRequestId) return setOptions(nextOptions) }) .catch(() => { if (requestId.current !== currentRequestId) return setFailed(true) }) .finally(() => { if (requestId.current === currentRequestId) setLoading(false) }) }, ASYNC_SEARCH_DEBOUNCE_MS) return () => window.clearTimeout(timeout) }, [open, query]) return { options, loading, failed } }