import { fuzzyMatch } from './string' // ─── Types ──────────────────────────────────────────────────────────────────── type RawResponse = T[] | { data: T[] } | { items: T[] } type ClientFetcher = () => Promise> type ServerFetcher = (query: string) => Promise> type Fetcher = ClientFetcher | ServerFetcher type Mapper = (items: T[]) => R[] export interface OptionItem { label: string value: string | number [key: string]: any } export interface ToOptionsConfig { label: keyof T | ((item: T) => string) value: keyof T } export interface NormalizedFetchResult { (filter?: string): Promise clearCache: () => void } // ─── toOptions ──────────────────────────────────────────────────────────────── function defaultLabel(item: T): string { const t = item as any return t.label ?? t.name ?? t.title ?? String(t.id ?? '') } function defaultValue(item: T): string | number { const t = item as any return t.value ?? t.key ?? t.id ?? '' } export function toOptions(items: T[]): OptionItem[] export function toOptions(config: ToOptionsConfig): Mapper export function toOptions( itemsOrConfig: T[] | ToOptionsConfig, ): OptionItem[] | Mapper { if (Array.isArray(itemsOrConfig)) { return itemsOrConfig.map(item => ({ ...item as any, label: defaultLabel(item), value: defaultValue(item), })) } const config = itemsOrConfig return (items: T[]) => items.map(item => ({ ...item as any, label: typeof config.label === 'function' ? config.label(item) : String(item[config.label] ?? ''), value: item[config.value] as string | number, })) } // ─── normalizedFetch ────────────────────────────────────────────────────────── function unwrap(response: RawResponse): T[] { if (Array.isArray(response)) return response if ('data' in response) return response.data if ('items' in response) return response.items return [] } export function normalizedFetch( fetcher: Fetcher, mapper: Mapper, options?: { timeout?: number }, ): NormalizedFetchResult { const timeout = options?.timeout ?? 10_000 const isServerSearch = fetcher.length > 0 let cache: R[] | null = null async function fetchWithTimeout(query?: string): Promise { const call = isServerSearch ? (fetcher as ServerFetcher)(query ?? '') : (fetcher as ClientFetcher)() const result = await Promise.race([ call, new Promise((_, reject) => setTimeout(() => { reject(new Error('Request timeout')) }, timeout), ), ]) return mapper(unwrap(result)) } const getter = async (filter?: string): Promise => { try { if (isServerSearch) { return await fetchWithTimeout(filter) } if (!cache) { cache = await fetchWithTimeout() } if (filter) { return cache.filter((item) => { const { label } = item as any if (typeof label !== 'string') return true return fuzzyMatch(label, filter).matches }).sort((a, b) => { const labelA = (a as any).label ?? '' const labelB = (b as any).label ?? '' return fuzzyMatch(labelB, filter!).score - fuzzyMatch(labelA, filter!).score }) } return cache } catch (error) { console.error('normalizedFetch error:', error) return [] } } getter.clearCache = () => { cache = null } return getter }