import { useState, useEffect, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Server, ApiResponse } from '@/types'; import { getApiUrl } from '../utils/runtime'; // Configuration options const CONFIG = { // Initialization phase configuration startup: { maxAttempts: 60, // Maximum number of attempts during initialization pollingInterval: 3000, // Polling interval during initialization (3 seconds) }, // Normal operation phase configuration normal: { pollingInterval: 10000, // Polling interval during normal operation (10 seconds) }, }; export const useServerData = () => { const { t } = useTranslation(); const [servers, setServers] = useState([]); const [error, setError] = useState(null); const [refreshKey, setRefreshKey] = useState(0); const [isInitialLoading, setIsInitialLoading] = useState(true); const [fetchAttempts, setFetchAttempts] = useState(0); // Timer reference for polling const intervalRef = useRef(null); // Track current attempt count to avoid dependency cycles const attemptsRef = useRef(0); // Clear the timer const clearTimer = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; // Start normal polling const startNormalPolling = useCallback(() => { // Ensure no other timers are running clearTimer(); const fetchServers = async () => { try { const token = localStorage.getItem('mcphub_token'); const response = await fetch(getApiUrl('/servers'), { headers: { 'x-auth-token': token || '', }, }); const data = await response.json(); if (data && data.success && Array.isArray(data.data)) { setServers(data.data); } else if (data && Array.isArray(data)) { setServers(data); } else { console.error('Invalid server data format:', data); setServers([]); } // Reset error state setError(null); } catch (err) { console.error('Error fetching servers during normal polling:', err); // Use friendly error message if (!navigator.onLine) { setError(t('errors.network')); } else if ( err instanceof TypeError && (err.message.includes('NetworkError') || err.message.includes('Failed to fetch')) ) { setError(t('errors.serverConnection')); } else { setError(t('errors.serverFetch')); } } }; // Execute immediately fetchServers(); // Set up regular polling intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval); }, [t]); useEffect(() => { // Reset attempt count if (refreshKey > 0) { attemptsRef.current = 0; setFetchAttempts(0); } // Initialization phase request function const fetchInitialData = async () => { try { const token = localStorage.getItem('mcphub_token'); const response = await fetch(getApiUrl('/servers'), { headers: { 'x-auth-token': token || '', }, }); const data = await response.json(); // Handle API response wrapper object, extract data field if (data && data.success && Array.isArray(data.data)) { setServers(data.data); setIsInitialLoading(false); // Initialization successful, start normal polling startNormalPolling(); return true; } else if (data && Array.isArray(data)) { // Compatibility handling, if API directly returns array setServers(data); setIsInitialLoading(false); // Initialization successful, start normal polling startNormalPolling(); return true; } else { // If data format is not as expected, set to empty array console.error('Invalid server data format:', data); setServers([]); setIsInitialLoading(false); // Initialization successful but data is empty, start normal polling startNormalPolling(); return true; } } catch (err) { // Increment attempt count, use ref to avoid triggering effect rerun attemptsRef.current += 1; console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err); // Update state for display setFetchAttempts(attemptsRef.current); // Set appropriate error message if (!navigator.onLine) { setError(t('errors.network')); } else { setError(t('errors.initialStartup')); } // If maximum attempt count is exceeded, give up initialization and switch to normal polling if (attemptsRef.current >= CONFIG.startup.maxAttempts) { console.log('Maximum startup attempts reached, switching to normal polling'); setIsInitialLoading(false); // Clear initialization polling clearTimer(); // Switch to normal polling mode startNormalPolling(); } return false; } }; // On component mount, set appropriate polling based on current state if (isInitialLoading) { // Ensure no other timers are running clearTimer(); // Execute initial request immediately fetchInitialData(); // Set polling interval for initialization phase intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval); console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`); } else { // Initialization completed, start normal polling startNormalPolling(); } // Cleanup function return () => { clearTimer(); }; }, [refreshKey, t, isInitialLoading, startNormalPolling]); // Manually trigger refresh const triggerRefresh = () => { // Clear current timer clearTimer(); // If in initialization phase, reset initialization state if (isInitialLoading) { setIsInitialLoading(true); attemptsRef.current = 0; setFetchAttempts(0); } // Change in refreshKey will trigger useEffect to run again setRefreshKey((prevKey) => prevKey + 1); }; // Server related operations const handleServerAdd = () => { setRefreshKey((prevKey) => prevKey + 1); }; const handleServerEdit = async (server: Server) => { try { // Fetch settings to get the full server config before editing const token = localStorage.getItem('mcphub_token'); const response = await fetch(getApiUrl('/settings'), { headers: { 'x-auth-token': token || '', }, }); const settingsData: ApiResponse<{ mcpServers: Record }> = await response.json(); if ( settingsData && settingsData.success && settingsData.data && settingsData.data.mcpServers && settingsData.data.mcpServers[server.name] ) { const serverConfig = settingsData.data.mcpServers[server.name]; return { name: server.name, status: server.status, tools: server.tools || [], config: serverConfig, }; } else { console.error('Failed to get server config from settings:', settingsData); setError(t('server.invalidConfig', { serverName: server.name })); return null; } } catch (err) { console.error('Error fetching server settings:', err); setError(err instanceof Error ? err.message : String(err)); return null; } }; const handleServerRemove = async (serverName: string) => { try { const token = localStorage.getItem('mcphub_token'); const response = await fetch(getApiUrl(`/servers/${serverName}`), { method: 'DELETE', headers: { 'x-auth-token': token || '', }, }); const result = await response.json(); if (!response.ok) { setError(result.message || t('server.deleteError', { serverName })); return false; } setRefreshKey((prevKey) => prevKey + 1); return true; } catch (err) { setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err))); return false; } }; const handleServerToggle = async (server: Server, enabled: boolean) => { try { const token = localStorage.getItem('mcphub_token'); const response = await fetch(getApiUrl(`/servers/${server.name}/toggle`), { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-token': token || '', }, body: JSON.stringify({ enabled }), }); const result = await response.json(); if (!response.ok) { console.error('Failed to toggle server:', result); setError(t('server.toggleError', { serverName: server.name })); return false; } // Update the UI immediately to reflect the change setRefreshKey((prevKey) => prevKey + 1); return true; } catch (err) { console.error('Error toggling server:', err); setError(err instanceof Error ? err.message : String(err)); return false; } }; return { servers, error, setError, isLoading: isInitialLoading, fetchAttempts, triggerRefresh, handleServerAdd, handleServerEdit, handleServerRemove, handleServerToggle, }; };