"use client"; import { useState, useEffect, useCallback, useRef } from 'react'; import axios from 'axios'; // Убираем CancelTokenSource import api, { apiStatus } from '@/lib/api'; interface UseApiRequestOptions { // URL для запроса url: string; // Метод запроса method?: 'get' | 'post' | 'put' | 'delete'; // Параметры запроса params?: any; // Данные для отправки (для POST, PUT) data?: any; // Хедеры запроса headers?: Record; // Автоматически выполнять запрос при монтировании autoFetch?: boolean; // Интервал для повторного запроса в миллисекундах refreshInterval?: number; // Количество автоматических повторных попыток при ошибке retries?: number; // Интервал между повторными попытками в миллисекундах retryInterval?: number; // Преобразователь для данных ответа dataTransformer?: (data: any) => T; // Функция для определения успешности ответа isSuccessful?: (response: any) => boolean; // Максимальное время запроса в миллисекундах timeout?: number; } interface UseApiRequestResult { // Данные ответа data: T | null; // Состояние загрузки loading: boolean; // Ошибка, если есть error: Error | null; // Функция для выполнения запроса fetchData: (config?: Partial>) => Promise; // Функция для отмены текущего запроса cancelRequest: () => void; // Функция для сброса состояния reset: () => void; // Статус запроса status: 'idle' | 'loading' | 'success' | 'error'; } // AbortController используется вместо CancelTokenSource /** * Хук для выполнения API-запросов с оптимизацией * @param options Параметры запроса * @returns Результат запроса и функции управления */ export function useApiRequest( options: UseApiRequestOptions ): UseApiRequestResult { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); // Для хранения AbortController const abortControllerRef = useRef(null); // Для хранения таймера обновления const refreshTimerRef = useRef(null); // Для отслеживания количества повторных попыток const retriesCountRef = useRef(0); // Кэш последних успешных ответов по URL const responseCache = useRef>(new Map()); // Создаем ключ кэша на основе URL и параметров const getCacheKey = useCallback((url: string, params?: any, data?: any): string => { return `${url}${params ? `?${JSON.stringify(params)}` : ''}${data ? `|${JSON.stringify(data)}` : ''}`; }, []); // Функция для отмены текущего запроса const cancelRequest = useCallback(() => { if (abortControllerRef.current) { abortControllerRef.current.abort('Запрос отменен пользователем'); // Используем abort() abortControllerRef.current = null; } if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current); refreshTimerRef.current = null; } }, []); // Функция для сброса состояния const reset = useCallback(() => { cancelRequest(); setData(null); setLoading(false); setError(null); setStatus('idle'); retriesCountRef.current = 0; }, [cancelRequest]); // Функция для создания источника отмены - больше не нужна, используем axios.CancelToken.source() // Основная функция для выполнения запроса const fetchData = useCallback( async (overrideOptions?: Partial>): Promise => { try { // Отменяем предыдущий запрос, если он есть cancelRequest(); // Обновляем опции запроса const currentOptions = { ...options, ...overrideOptions }; const { url, method = 'get', params, data: requestData, headers, retries = 0, retryInterval = 1000, dataTransformer, isSuccessful = response => true, timeout = 30000 } = currentOptions; setLoading(true); setStatus('loading'); setError(null); // Проверяем наличие в кэше (только для GET запросов) if (method === 'get') { const cacheKey = getCacheKey(url || '', params || {}, null); const cachedResponse = responseCache.current.get(cacheKey); // Используем кэш, если он не старше 5 минут if (cachedResponse && Date.now() - cachedResponse.timestamp < 5 * 60 * 1000) { console.log(`Используются кэшированные данные для ${url}`); setData(cachedResponse.data); setLoading(false); setStatus('success'); return cachedResponse.data; } } // Создаем новый AbortController abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; // Выполняем запрос с помощью api-клиента, передавая signal let response; try { const config = { headers, timeout, signal }; // Добавляем signal в конфиг switch (method) { case 'get': response = await api.get(url, { params: params || {}, ...config }); // Передаем config break; case 'post': response = await api.post(url, requestData || {}, config); // Передаем config break; case 'put': response = await api.put(url, requestData || {}, config); // Передаем config break; case 'delete': response = await api.delete(url, config); // Передаем config break; default: throw new Error(`Неподдерживаемый метод: ${method}`); } } catch (error) { throw error; } // Проверяем успешность ответа if (!isSuccessful(response)) { throw new Error('Ответ API не соответствует ожидаемому формату'); } // Преобразуем данные, если указан трансформер const processedData = dataTransformer ? dataTransformer(response) : (response as T); // Кэшируем ответ для GET запросов if (method === 'get') { const cacheKey = getCacheKey(url || '', params || {}, null); responseCache.current.set(cacheKey, { data: processedData, timestamp: Date.now() }); // Ограничиваем размер кэша if (responseCache.current.size > 50) { const oldestKey = responseCache.current.keys().next().value; responseCache.current.delete(oldestKey); } } // Обновляем состояние с полученными данными setData(processedData); setLoading(false); setStatus('success'); retriesCountRef.current = 0; // Устанавливаем таймер для автоматического обновления, если задан if (currentOptions.refreshInterval && currentOptions.refreshInterval > 0) { refreshTimerRef.current = setTimeout(() => { fetchData(overrideOptions); }, currentOptions.refreshInterval); } return processedData; } catch (err: unknown) { // Явно типизируем err как unknown // Обработка ошибки отмены (AbortError) if (err instanceof Error && err.name === 'AbortError') { // Используем сообщение из AbortSignal, если оно есть, иначе стандартное const abortReason = (abortControllerRef.current?.signal.reason as string) || 'Запрос отменен'; console.log('Запрос отменен:', abortReason); setLoading(false); setStatus('idle'); return null; } // Обработка других ошибок console.error('Ошибка API-запроса:', err); const errorObj = err instanceof Error ? err : new Error('Неизвестная ошибка при запросе к API'); setError(errorObj); setLoading(false); setStatus('error'); // Повторная попытка, если указано количество повторов const { retries = 0, retryInterval = 1000 } = { ...options, ...overrideOptions }; if (retriesCountRef.current < retries) { console.log(`Повторная попытка ${retriesCountRef.current + 1}/${retries} через ${retryInterval}мс`); retriesCountRef.current++; await new Promise(resolve => setTimeout(resolve, retryInterval)); return fetchData(overrideOptions); } return null; } }, // Убираем createCancelToken из зависимостей [options, cancelRequest, getCacheKey] ); // Автоматический запрос при монтировании компонента useEffect(() => { if (options.autoFetch) { fetchData(); } // Очистка return () => { cancelRequest(); }; }, [fetchData, cancelRequest, options.autoFetch]); return { data, loading, error, fetchData, cancelRequest, reset, status }; }