import axios from 'axios'; import type { RiskMapResponse, GridDetailResponse, AlertResponse, Stats, ForecastDay, CaseTrendResponse, DistrictCaseResponse, CaseStatsResponse, CaseGridResponse, GeocodedCasesResponse, } from '@/types'; interface CacheEntry { data: T; timestamp: number; promise?: Promise; } const CACHE_TTL = 30000; const cache = new Map>(); const pendingControllers = new Map(); function getCacheKey(url: string, params?: Record): string { if (!params) return url; const sorted = Object.entries(params) .filter(([, v]) => v !== undefined) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => `${k}=${v}`) .join('&'); return sorted ? `${url}?${sorted}` : url; } function getCached(key: string): T | undefined { const entry = cache.get(key); if (!entry) return undefined; if (Date.now() - entry.timestamp > CACHE_TTL) { cache.delete(key); return undefined; } return entry.data; } function setCache(key: string, data: T): void { cache.set(key, { data, timestamp: Date.now() }); } function clearPending(key: string): void { const controller = pendingControllers.get(key); if (controller) { controller.abort(); pendingControllers.delete(key); } } const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || '/api', timeout: 30000, }); api.interceptors.request.use((config) => { const token = localStorage.getItem('cbpoa_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } const key = getCacheKey(config.url || '', config.params); const controller = new AbortController(); config.signal = controller.signal; clearPending(key); pendingControllers.set(key, controller); return config; }); api.interceptors.response.use( (response) => { const key = getCacheKey(response.config.url || '', response.config.params); pendingControllers.delete(key); return response; }, (error) => { if (error.config) { const key = getCacheKey(error.config.url || '', error.config.params); pendingControllers.delete(key); } return Promise.reject(error); } ); export async function cachedGet(url: string, params?: Record): Promise { const key = getCacheKey(url, params); const cached = getCached(key); if (cached !== undefined) return cached; const entry = cache.get(key); if (entry?.promise) return entry.promise; const promise = api.get(url, { params }).then((res) => { setCache(key, res.data); const updated = cache.get(key); if (updated) updated.promise = undefined; return res.data; }); cache.set(key, { data: undefined as T, timestamp: Date.now(), promise }); return promise; } export const riskApi = { getCurrentRiskMap: (): Promise => cachedGet('/risk/current'), getForecast: (days: ForecastDay): Promise => { const d = days === 0 ? '' : `/${days}`; return cachedGet(`/risk/forecast${d}`); }, getGridDetail: (gridId: string): Promise => cachedGet(`/risk/grid/${encodeURIComponent(gridId)}`), getStats: (): Promise => cachedGet('/risk/stats'), }; export const alertApi = { getAlerts: (params?: { min_risk?: number; priority?: string; region?: string; }): Promise => cachedGet('/alerts', params), getAlertRules: (): Promise => cachedGet('/alerts/rules'), }; export const historyApi = { getHistory: (params: { grid_id?: string; region?: string; days?: number; }): Promise => cachedGet('/history', params), getTrend: (gridId: string, days: number = 7): Promise => cachedGet('/history/trend', { grid_id: gridId, days }), }; export const caseApi = { getTrend: (days: number = 7): Promise => cachedGet('/cases/trend', { days }), getDistricts: (): Promise => cachedGet('/cases/districts'), getStats: (): Promise => cachedGet('/cases/stats'), getGrid: (): Promise => cachedGet('/cases/grid'), getGeocoded: (limit: number = 5000): Promise => cachedGet('/cases/geocoded', { limit }), }; export function clearApiCache(): void { cache.clear(); } export function cancelPendingRequests(): void { pendingControllers.forEach((controller) => controller.abort()); pendingControllers.clear(); } export const gridApi = { getHistoricalAggregated: ( startDate: string, endDate: string, aggregation: 'daily' | 'weekly' | 'monthly' = 'daily', district?: string ): Promise => { const params: Record = { start_date: startDate, end_date: endDate, aggregation }; if (district) params.district = district; return cachedGet('/history/aggregated', params); }, getGridsGeoJSON: (date: string, district?: string): Promise => { const params: Record = { date }; if (district) params.district = district; return cachedGet('/grids/geojson', params); }, getMultiDayPrediction: async (date: string, days: number = 7, district?: string): Promise => { const response = await api.post('/predict/multi-day', { date, days, district }); return response.data; }, getGridHistory: (gridId: string, days: number = 30): Promise => cachedGet(`/grids/${encodeURIComponent(gridId)}/history`, { days }), }; export const analysisApi = { getTrend: (days: number = 7): Promise => cachedGet('/analysis/trend', { days }), getDistricts: (): Promise => cachedGet('/analysis/districts'), }; export const insightsApi = { getOverview: (): Promise => cachedGet('/insights/overview'), }; export default api;