import { message } from 'antd'; import { useCallback, useEffect, useRef, useState } from 'react'; import { ApiError } from '../../api/client'; import { getConsolidatedOsv, getOsvSummary } from '../../services/api/osv'; import type { ConsolidatedRowOut, OsvSummaryOut } from '../../types/api'; import { type OsvFilterDraft, type OsvFilterState, applyFilterPatch } from './osvFilterState'; export const OSV_PAGE_SIZE = 500; /** Понятный русский текст ошибки загрузки свода. */ export function osvErrorText(e: unknown): string { if (e instanceof ApiError) { if (e.status === 404) return 'Версия не найдена или недоступна'; if (e.status === 409) return `Сводная ОСВ сейчас недоступна: ${e.message}`; return e.message; } return 'Не удалось получить сводную ОСВ'; } /** * @param pairsMode режим «Встречные записи»: строки свода не нужны и не запрашиваются (сводка — запрашивается). * Фильтры и страница — одно состояние: смена фильтра с возвратом на 1-ю страницу даёт один запрос, а не два. */ export function useConsolidatedOsv(versionId: string, pairsMode = false) { const [query, setQuery] = useState({ filters: {}, page: 1 }); const { filters, page } = query; const setPage = useCallback((p: number) => setQuery((q) => ({ ...q, page: p })), []); const [rows, setRows] = useState([]); const [total, setTotal] = useState(0); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // ошибка загрузки сводки (для режима пар: без неё там не на что смотреть) const [summaryError, setSummaryError] = useState(null); // номер последнего запроса: ответы более старых запросов игнорируются const requestRef = useRef(0); // номер последнего запроса сводки: перечитывание после сопоставления не перетирается более старым ответом const summaryRef = useRef(0); const loadSummary = useCallback(async () => { const requestId = ++summaryRef.current; try { const s = await getOsvSummary(versionId); if (requestId === summaryRef.current) { setSummary(s); setSummaryError(null); } } catch (e: unknown) { if (requestId === summaryRef.current) { message.error(osvErrorText(e)); setSummaryError(osvErrorText(e)); } } }, [versionId]); useEffect(() => { void loadSummary(); return () => { // размонтирование: ответ уже отправленного запроса игнорируется summaryRef.current++; }; }, [loadSummary]); const load = useCallback(async () => { const requestId = ++requestRef.current; setLoading(true); try { const result = await getConsolidatedOsv(versionId, filters, OSV_PAGE_SIZE, (page - 1) * OSV_PAGE_SIZE); if (requestId === requestRef.current) { setRows(result.items); setTotal(result.total); setError(null); } } catch (e: unknown) { if (requestId === requestRef.current) { setRows([]); setTotal(0); setError(osvErrorText(e)); } } finally { if (requestId === requestRef.current) setLoading(false); } }, [versionId, filters, page]); useEffect(() => { if (pairsMode) { // ответ ещё летящего запроса строк больше не нужен requestRef.current++; setLoading(false); return; } void load(); }, [load, pairsMode]); const patchFilters = (patch: OsvFilterDraft) => { setQuery((q) => applyFilterPatch(q, patch)); }; /** После ручного сопоставления: «Осталось N» и строки меняются сразу у нескольких строк (автоприменение) — перечитать обе. */ const refresh = () => { if (!pairsMode) void load(); void loadSummary(); }; return { filters, patchFilters, page, setPage, rows, total, summary, summaryError, loadSummary, loading, error, refresh }; }