Files
gridpilot.gg/apps/website/lib/api/base/GracefulDegradation.ts
2026-01-12 01:01:49 +01:00

321 lines
6.9 KiB
TypeScript

/**
* Graceful degradation utilities for when API is unavailable
*/
import { ApiConnectionMonitor } from './ApiConnectionMonitor';
import { ApiError } from './ApiError';
export interface DegradationOptions<T> {
/**
* Fallback data to return when API is unavailable
*/
fallback?: T;
/**
* Whether to throw error or return fallback
*/
throwOnError?: boolean;
/**
* Maximum time to wait for API response
*/
timeout?: number;
/**
* Whether to use cached data if available
*/
useCache?: boolean;
}
export interface CacheEntry<T> {
data: T;
timestamp: Date;
expiry: Date;
}
/**
* Simple in-memory cache for API responses
*/
class ResponseCache {
private cache = new Map<string, CacheEntry<any>>();
/**
* Get cached data if not expired
*/
get<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (new Date() > entry.expiry) {
this.cache.delete(key);
return null;
}
return entry.data;
}
/**
* Set cached data with expiry
*/
set<T>(key: string, data: T, ttlMs: number = 300000): void {
const now = new Date();
const expiry = new Date(now.getTime() + ttlMs);
this.cache.set(key, {
data,
timestamp: now,
expiry,
});
}
/**
* Clear all cached data
*/
clear(): void {
this.cache.clear();
}
/**
* Get cache statistics
*/
getStats() {
return {
size: this.cache.size,
entries: Array.from(this.cache.entries()).map(([key, entry]) => ({
key,
timestamp: entry.timestamp,
expiry: entry.expiry,
})),
};
}
}
/**
* Global cache instance
*/
export const responseCache = new ResponseCache();
/**
* Execute a function with graceful degradation
*/
export async function withGracefulDegradation<T>(
fn: () => Promise<T>,
options: DegradationOptions<T> = {}
): Promise<T | undefined> {
const {
fallback,
throwOnError = false,
timeout = 10000,
useCache = true,
} = options;
const monitor = ApiConnectionMonitor.getInstance();
// Check if API is available
if (!monitor.isAvailable()) {
// Try cache first
if (useCache && options.fallback) {
const cacheKey = `graceful:${fn.toString()}`;
const cached = responseCache.get<T>(cacheKey);
if (cached) {
return cached;
}
}
// Return fallback
if (fallback !== undefined) {
return fallback;
}
// Throw error if no fallback
if (throwOnError) {
throw new ApiError(
'API unavailable and no fallback provided',
'NETWORK_ERROR',
{
timestamp: new Date().toISOString(),
}
);
}
// Return undefined (caller must handle)
return undefined;
}
// API is available, try to execute
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const result = await Promise.race([
fn(),
new Promise<never>((_, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new Error('Request timeout'));
});
}),
]);
clearTimeout(timeoutId);
// Cache the result if enabled
if (useCache && result !== null && result !== undefined) {
const cacheKey = `graceful:${fn.toString()}`;
responseCache.set(cacheKey, result);
}
return result;
} catch (error) {
// Record failure in monitor
if (error instanceof ApiError) {
monitor.recordFailure(error);
} else {
monitor.recordFailure(error as Error);
}
// Try cache as fallback
if (useCache && options.fallback) {
const cacheKey = `graceful:${fn.toString()}`;
const cached = responseCache.get<T>(cacheKey);
if (cached) {
return cached;
}
}
// Return fallback if provided
if (fallback !== undefined) {
return fallback;
}
// Re-throw or return undefined
if (throwOnError) {
throw error;
}
return undefined;
}
}
/**
* Service wrapper for graceful degradation
*/
export class GracefulService<T> {
private monitor: ApiConnectionMonitor;
private cacheKey: string;
constructor(
private serviceName: string,
private getData: () => Promise<T>,
private defaultFallback: T
) {
this.monitor = ApiConnectionMonitor.getInstance();
this.cacheKey = `service:${serviceName}`;
}
/**
* Get data with graceful degradation
*/
async get(options: Partial<DegradationOptions<T>> = {}): Promise<T> {
const result = await withGracefulDegradation(this.getData, {
fallback: this.defaultFallback,
throwOnError: false,
useCache: true,
...options,
});
return result ?? this.defaultFallback;
}
/**
* Force refresh data
*/
async refresh(): Promise<T> {
responseCache.clear(); // Clear cache for this service
return this.get({ useCache: false });
}
/**
* Get service health status
*/
getStatus() {
const health = this.monitor.getHealth();
const isAvailable = this.monitor.isAvailable();
return {
serviceName: this.serviceName,
available: isAvailable,
reliability: health.totalRequests > 0
? (health.successfulRequests / health.totalRequests) * 100
: 100,
lastCheck: health.lastCheck,
};
}
}
/**
* Offline mode detection
*/
export class OfflineDetector {
private static instance: OfflineDetector;
private isOffline = false;
private listeners: Array<(isOffline: boolean) => void> = [];
private constructor() {
if (typeof window !== 'undefined') {
window.addEventListener('online', () => this.setOffline(false));
window.addEventListener('offline', () => this.setOffline(true));
// Initial check
this.isOffline = !navigator.onLine;
}
}
static getInstance(): OfflineDetector {
if (!OfflineDetector.instance) {
OfflineDetector.instance = new OfflineDetector();
}
return OfflineDetector.instance;
}
private setOffline(offline: boolean): void {
if (this.isOffline !== offline) {
this.isOffline = offline;
this.listeners.forEach(listener => listener(offline));
}
}
/**
* Check if browser is offline
*/
isBrowserOffline(): boolean {
return this.isOffline;
}
/**
* Add listener for offline status changes
*/
onStatusChange(callback: (isOffline: boolean) => void): void {
this.listeners.push(callback);
}
/**
* Remove listener
*/
removeListener(callback: (isOffline: boolean) => void): void {
this.listeners = this.listeners.filter(cb => cb !== callback);
}
}
/**
* Hook for offline detection
*/
export function useOfflineStatus() {
if (typeof window === 'undefined') {
return false; // Server-side
}
// This would need to be used in a React component context
// For now, provide a simple check function
return OfflineDetector.getInstance().isBrowserOffline();
}