import { useCallback, useEffect, useRef, useState } from 'react'; import { PAGE_FORCE_REFRESH_EVENT } from '../utils/feedCache'; import { PAGE_SOFT_REFRESH_COMMIT_EVENT } from '../utils/softRefresh'; import { getSessionSnapshot, setSessionSnapshot, deleteSessionSnapshot, } from '../utils/sessionPageCache'; type Fetcher = () => Promise; /** * 会话快照数据源:有缓存则跳过请求;无缓存时保留上一份画面直到新数据返回。 * 手机下拉软刷新 commit / PAGE_FORCE_REFRESH_EVENT 会应用新快照。 */ function isEmptyArray(value: unknown): boolean { return Array.isArray(value) && value.length === 0; } export function useSessionResource( key: string | null, fetcher: Fetcher, opts?: { enabled?: boolean; onError?: (e: unknown) => void; /** 缓存为空数组时仍后台再拉,避免启动竞态假空粘死 */ revalidateEmpty?: boolean; }, ) { const enabled = opts?.enabled !== false; const revalidateEmpty = opts?.revalidateEmpty === true; const fetcherRef = useRef(fetcher); fetcherRef.current = fetcher; const onErrorRef = useRef(opts?.onError); onErrorRef.current = opts?.onError; const [data, setData] = useState(() => key && enabled ? getSessionSnapshot(key) : undefined, ); const [loading, setLoading] = useState(() => { if (!key || !enabled) return false; return getSessionSnapshot(key) === undefined; }); const [pending, setPending] = useState(false); const seqRef = useRef(0); const dataRef = useRef(data); dataRef.current = data; useEffect(() => { if (!key || !enabled) { if (!enabled) { setLoading(false); setPending(false); } return; } const hit = getSessionSnapshot(key); if (hit !== undefined && !(revalidateEmpty && isEmptyArray(hit))) { setData(hit); setLoading(false); setPending(false); return; } const seq = ++seqRef.current; const keep = dataRef.current !== undefined || (hit !== undefined && isEmptyArray(hit)); if (hit !== undefined && isEmptyArray(hit)) { setData(hit); setPending(true); setLoading(false); } else if (keep && dataRef.current !== undefined) { setPending(true); } else { setLoading(true); } fetcherRef.current() .then((next) => { if (seq !== seqRef.current) return; setSessionSnapshot(key, next); setData(next); }) .catch((e: unknown) => { if (seq !== seqRef.current) return; onErrorRef.current?.(e); // 失败不写空快照;已有画面则保留 if (!keep) setData(undefined); }) .finally(() => { if (seq !== seqRef.current) return; setLoading(false); setPending(false); }); return () => { seqRef.current += 1; }; }, [key, enabled, revalidateEmpty]); const replace = useCallback((next: T | ((prev: T | undefined) => T)) => { setData((prev) => { const value = typeof next === 'function' ? (next as (p: T | undefined) => T)(prev) : next; if (key) setSessionSnapshot(key, value); return value; }); }, [key]); const invalidate = useCallback(() => { if (key) deleteSessionSnapshot(key); }, [key]); useEffect(() => { const reload = (opts: { allowLoading: boolean }) => { if (!key || !enabled) return; const warm = getSessionSnapshot(key); if (warm !== undefined && !(revalidateEmpty && isEmptyArray(warm))) { setData(warm); setLoading(false); setPending(false); return; } const seq = ++seqRef.current; const keep = dataRef.current !== undefined || (warm !== undefined && isEmptyArray(warm)); if (opts.allowLoading) { deleteSessionSnapshot(key); if (keep) setPending(true); else setLoading(true); } else if (warm !== undefined && isEmptyArray(warm)) { setPending(true); } fetcherRef.current() .then((next) => { if (seq !== seqRef.current) return; setSessionSnapshot(key, next); setData(next); }) .catch((e: unknown) => { if (seq !== seqRef.current) return; onErrorRef.current?.(e); if (opts.allowLoading && !keep) setData(undefined); }) .finally(() => { if (seq !== seqRef.current) return; setLoading(false); setPending(false); }); }; const onForce = () => reload({ allowLoading: true }); // 软刷新 commit:未命中不卸 UI,后台静默再拉 const onCommit = () => reload({ allowLoading: false }); window.addEventListener(PAGE_FORCE_REFRESH_EVENT, onForce); window.addEventListener(PAGE_SOFT_REFRESH_COMMIT_EVENT, onCommit); return () => { window.removeEventListener(PAGE_FORCE_REFRESH_EVENT, onForce); window.removeEventListener(PAGE_SOFT_REFRESH_COMMIT_EVENT, onCommit); }; }, [key, enabled, revalidateEmpty]); return { data, loading, pending, replace, invalidate }; }