'use client' import { useCallback, useEffect, useRef, useState } from 'react' import { texts } from '@/texts' interface AsyncResourceOptions { cacheKey?: string enabled?: boolean initialData?: T | null retries?: number retryDelayMs?: number staleTimeMs?: number } interface CacheEntry { data: unknown updatedAt: number } const resourceCache = new Map() export const invalidateAsyncResource = (key: string) => { resourceCache.delete(key) } /** Drop every module-level entry — call on logout so a later session cannot reuse prior data. */ export const clearAsyncResourceCache = () => { resourceCache.clear() } export const useAsyncResource = ( load: (signal: AbortSignal) => Promise, { cacheKey, enabled = true, initialData = null, retries = 1, retryDelayMs = 300, staleTimeMs = 30_000 }: AsyncResourceOptions = {} ) => { const cached = cacheKey ? resourceCache.get(cacheKey) : undefined const [data, setData] = useState((cached?.data as T | undefined) ?? initialData) const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(enabled) const requestIdRef = useRef(0) const controllerRef = useRef(null) useEffect(() => { const nextCached = cacheKey ? resourceCache.get(cacheKey) : undefined setData((nextCached?.data as T | undefined) ?? initialData) }, [cacheKey, initialData]) const execute = useCallback( async (ignoreCache = false) => { if (!enabled) return null const currentCache = cacheKey ? resourceCache.get(cacheKey) : undefined if (!ignoreCache && currentCache && Date.now() - currentCache.updatedAt < staleTimeMs) { const cachedData = currentCache.data as T setData(cachedData) setError(null) setIsLoading(false) return cachedData } controllerRef.current?.abort() const controller = new AbortController() const requestId = ++requestIdRef.current controllerRef.current = controller setIsLoading(true) setError(null) for (let attempt = 0; attempt <= retries; attempt += 1) { try { const nextData = await load(controller.signal) if (!controller.signal.aborted && requestId === requestIdRef.current) { setData(nextData) setIsLoading(false) if (cacheKey) resourceCache.set(cacheKey, { data: nextData, updatedAt: Date.now() }) } return nextData } catch (cause) { if (controller.signal.aborted) return null if (attempt < retries) { await new Promise((resolve) => window.setTimeout(resolve, retryDelayMs * (attempt + 1))) continue } const nextError = cause instanceof Error ? cause : new Error(texts.common.unexpectedError) if (requestId === requestIdRef.current) setError(nextError) } } if (requestId === requestIdRef.current) setIsLoading(false) return null }, [cacheKey, enabled, retries, retryDelayMs, staleTimeMs, load] ) useEffect(() => { void execute(false) return () => controllerRef.current?.abort() }, [execute]) const refetch = useCallback(() => execute(true), [execute]) const updateData = useCallback( (nextData: T | null) => { setData(nextData) if (cacheKey) { if (nextData === null) resourceCache.delete(cacheKey) else resourceCache.set(cacheKey, { data: nextData, updatedAt: Date.now() }) } }, [cacheKey] ) return { data, error, isLoading, refetch, setData: updateData } }