frontend_nyla/src/hooks/useApi.ts

216 lines
No EOL
7.1 KiB
TypeScript

import { useState, useCallback } from 'react';
import api from '../api';
// Global request cache to prevent duplicate requests
const requestCache = new Map<string, Promise<any>>();
const cacheTimestamps = new Map<string, number>();
const CACHE_DURATION = 5000; // 5 seconds cache duration
// Generate cache key for request deduplication
function generateCacheKey(url: string, method: string, params?: Record<string, any>): string {
const paramsString = params ? JSON.stringify(params) : '';
return `${method.toUpperCase()}:${url}:${paramsString}`;
}
// Check if cached request is still valid
function isCacheValid(cacheKey: string): boolean {
const timestamp = cacheTimestamps.get(cacheKey);
if (!timestamp) return false;
return Date.now() - timestamp < CACHE_DURATION;
}
// Generic API error handling
export function formatApiError(error: any, defaultMessage: string): string {
if (error.response) {
const data = error.response.data;
// Handle FastAPI validation errors (detail is an array)
if (data?.detail && Array.isArray(data.detail)) {
return data.detail.map((err: any) => {
if (typeof err === 'string') return err;
if (err.msg) return `${err.loc?.join('.') || 'field'}: ${err.msg}`;
return JSON.stringify(err);
}).join(', ');
}
// Handle other error formats
if (typeof data?.detail === 'string') return data.detail;
if (typeof data?.message === 'string') return data.message;
if (typeof data === 'string') return data;
return defaultMessage;
} else if (error.request) {
return 'Keine Antwort vom Server erhalten';
} else {
return error.message || defaultMessage;
}
}
// Type for API request options
export interface ApiRequestOptions<T> {
url: string;
method: 'get' | 'post' | 'put' | 'delete';
data?: T;
params?: Record<string, string | number | boolean>;
additionalConfig?: Record<string, any>; // For responseType, headers, etc.
}
// Hook for making API requests with consistent error handling
export function useApiRequest<RequestData = any, ResponseData = any>() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const request = useCallback(async ({
url,
method,
data,
params,
additionalConfig = {}
}: ApiRequestOptions<RequestData>): Promise<ResponseData> => {
setIsLoading(true);
setError(null);
try {
// Generate cache key for GET requests (only cache GET requests)
const cacheKey = method === 'get' ? generateCacheKey(url, method, params) : null;
// Check if we have a valid cached request for GET requests
if (cacheKey && requestCache.has(cacheKey) && isCacheValid(cacheKey)) {
console.log('🔧 useApiRequest: Using cached request', { url, method, cacheKey });
setIsLoading(false);
return await requestCache.get(cacheKey)!;
}
console.log('🔧 useApiRequest: Making request', {
url,
method,
hasData: !!data,
hasParams: !!params,
cacheKey,
dataStructure: data ? {
data,
dataType: typeof data,
dataKeys: Object.keys(data),
dataEntries: Object.entries(data).map(([key, value]) => ({
key,
value,
valueType: typeof value,
valueIsObject: typeof value === 'object' && value !== null && !Array.isArray(value),
valueStringified: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
})),
dataStringified: JSON.stringify(data, null, 2)
} : null,
fullRequestConfig: {
url,
method,
data,
params,
...additionalConfig
}
});
// Create the request promise
const requestPromise = api({
url,
method,
data,
params,
...additionalConfig
}).then(response => {
console.log('🔧 useApiRequest: Request successful', { url, status: response.status, hasData: !!response.data });
// For blob responses, return the blob data directly
if (additionalConfig.responseType === 'blob') {
return response.data;
}
return response.data;
});
// Cache GET requests
if (cacheKey) {
requestCache.set(cacheKey, requestPromise);
cacheTimestamps.set(cacheKey, Date.now());
// Clean up old cache entries
setTimeout(() => {
if (requestCache.has(cacheKey) && !isCacheValid(cacheKey)) {
requestCache.delete(cacheKey);
cacheTimestamps.delete(cacheKey);
}
}, CACHE_DURATION + 1000);
}
const result = await requestPromise;
setIsLoading(false);
return result;
} catch (error: any) {
// Clear cache on error to allow retry
const cacheKey = method === 'get' ? generateCacheKey(url, method, params) : null;
if (cacheKey) {
requestCache.delete(cacheKey);
cacheTimestamps.delete(cacheKey);
}
console.log('🔧 useApiRequest: Request failed', {
url,
error: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
hasResponse: !!error.response,
hasRequest: !!error.request,
isAborted: error.code === 'ERR_CANCELED',
authAuthority: sessionStorage.getItem('auth_authority'),
hasCookies: document.cookie.includes('access_token') || document.cookie.includes('refresh_token')
});
// Handle aborted requests specifically
if (error.code === 'ERR_CANCELED' || error.message?.includes('aborted')) {
const errorMessage = 'Request aborted';
setError(errorMessage);
throw new Error(errorMessage);
}
// Handle authentication errors specifically
if (error.response?.status === 401) {
const errorMessage = 'Not authenticated';
setError(errorMessage);
throw new Error(errorMessage);
}
// Handle rate limiting specifically
if (error.response?.status === 429) {
const errorMessage = error.response.data?.detail || error.response.data?.message || '30 per 1 minute';
setError(errorMessage);
throw new Error(errorMessage);
}
const errorMessage = formatApiError(error, `Fehler bei ${method.toUpperCase()} ${url}`);
setError(errorMessage);
throw new Error(String(errorMessage)); // Ensure it's a string
} finally {
setIsLoading(false);
}
}, []);
// Function to clear cache manually
const clearCache = useCallback((url?: string, method?: string) => {
if (url && method) {
const cacheKey = generateCacheKey(url, method);
requestCache.delete(cacheKey);
cacheTimestamps.delete(cacheKey);
console.log('🔧 useApiRequest: Cleared cache for', { url, method, cacheKey });
} else {
requestCache.clear();
cacheTimestamps.clear();
console.log('🔧 useApiRequest: Cleared all cache');
}
}, []);
return {
request,
isLoading,
error,
clearCache
};
}