import { createContext, useCallback, useContext, useEffect, useRef, useState, type DependencyList } from 'react'; export interface AsyncState { data: T | undefined; error: unknown; loading: boolean; reload: () => void; setData: (update: T | ((prev: T | undefined) => T | undefined)) => void; } /** * کشِ همین نشست برای پاسخ‌های GET، با کلیدی که صفحه می‌دهد. صفحه‌ای که دوباره باز می‌شود (برگشت، تعویضِ تب) * فوراً با داده‌ی قبلی رسم می‌شود و همان لحظه بی‌صدا از سرور تازه می‌شود (stale-while-revalidate). قبلاً هر بار * اول کارتِ خالی یا «نداری» دیده می‌شد و چند صد میلی‌ثانیه بعد داده‌ی واقعی جایش می‌پرید. * با خروج از حساب پاک می‌شود (`clearAsyncCache`)، تا داده‌ی یک کاربر به کاربرِ بعدی نرسد. */ const cache = new Map(); export function clearAsyncCache() { cache.clear(); } /** * «دروازه»ی صفحه‌ی تازه: Navigator صفحه‌ی بعدی را پنهان زیرِ صفحه‌ی فعلی می‌سازد و تا وقتی داده‌ی اولش * نرسیده (یا سقفِ انتظار نگذشته) نشانش نمی‌دهد. هر useAsyncی که بارِ اولش بی‌کش است یک hold می‌گیرد. * بیرون از صفحه‌ها (مثلاً جریانِ ورود) این context خالی است و کاری نمی‌کند. */ export interface ScreenGate { hold: () => () => void; } export const ScreenGateContext = createContext(null); /** * برای صفحه‌ای که داده‌اش را بی useAsync می‌گیرد (آزمون، دستیار): تا `loading` درست است، صفحه‌ی تازه پنهان * می‌ماند. همان قاعده‌ی useAsync: دروازه‌ی باز‌شده hold را نادیده می‌گیرد. */ export function useGateHold(loading: boolean) { const gate = useContext(ScreenGateContext); useEffect(() => (loading && gate ? gate.hold() : undefined), [loading, gate]); } /** * داده‌ای که از سرور می‌آید، با حالت‌های بارگذاری/خطا و «دوباره». * پاسخِ درخواستِ کهنه (بعد از عوض شدنِ deps) نادیده گرفته می‌شود. * با `key`، پاسخ در کشِ نشست می‌ماند و بارِ بعد فوراً نشان داده می‌شود (بالای همین فایل). */ export function useAsync(load: () => Promise, deps: DependencyList, opts: { key?: string } = {}): AsyncState { const { key } = opts; const [data, setDataState] = useState(() => (key !== undefined ? (cache.get(key) as T | undefined) : undefined)); const [error, setError] = useState(null); const [loading, setLoading] = useState(() => key === undefined || !cache.has(key)); const [nonce, setNonce] = useState(0); const loadRef = useRef(load); loadRef.current = load; const gate = useContext(ScreenGateContext); useEffect(() => { let alive = true; const hit = key !== undefined && cache.has(key); if (hit) { // کلید عوض شده (مثلاً «هفتگی» ← «همیشه») و برای کلیدِ تازه کش هست: همان را نشان بده و بی‌لودر تازه کن. setDataState(cache.get(key) as T); setLoading(false); } else { setLoading(true); } setError(null); // دروازه‌ای که یک بار باز شده hold را نادیده می‌گیرد؛ پس «دوباره» و تازه‌سازی صفحه را پنهان نمی‌کنند. const release = !hit && gate ? gate.hold() : null; loadRef.current().then( (value) => { if (!alive) return; if (key !== undefined) cache.set(key, value); setDataState(value); setLoading(false); release?.(); }, (err: unknown) => { if (!alive) return; if (hit) { // داده‌ی کش‌شده روی صفحه است؛ شکستِ تازه‌سازی آن را با صفحه‌ی خطا عوض نمی‌کند، ولی دیده می‌شود. console.warn(`refresh of ${key} failed; showing cached data`, err); } else { setError(err); } setLoading(false); release?.(); }, ); return () => { alive = false; release?.(); }; }, [...deps, key, nonce]); const reload = useCallback(() => setNonce((n) => n + 1), []); const setData = useCallback( (update: T | ((prev: T | undefined) => T | undefined)) => { setDataState((prev) => { const next = typeof update === 'function' ? (update as (p: T | undefined) => T | undefined)(prev) : update; if (key !== undefined) { if (next === undefined) cache.delete(key); else cache.set(key, next); } return next; }); }, [key], ); return { data, error, loading, reload, setData }; }