/** * Graceful degradation utilities for when API is unavailable */ import { ApiConnectionMonitor } from './ApiConnectionMonitor'; import { ApiError } from './ApiError'; export interface DegradationOptions { /** * 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 { data: T; timestamp: Date; expiry: Date; } /** * Simple in-memory cache for API responses */ class ResponseCache { private cache = new Map>(); /** * Get cached data if not expired */ get(key: string): T | null { const entry = this.cache.get(key); if (!entry) return null; if (new globalThis.Date() > entry.expiry) { this.cache.delete(key); return null; } return entry.data as T; } /** * Set cached data with expiry */ set(key: string, data: T, ttlMs: number = 300000): void { const now = new globalThis.Date(); const expiry = new globalThis.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( fn: () => Promise, options: DegradationOptions = {} ): Promise { 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(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 globalThis.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((_, 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(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 { private monitor: ApiConnectionMonitor; private cacheKey: string; constructor( private serviceName: string, private getData: () => Promise, private defaultFallback: T ) { this.monitor = ApiConnectionMonitor.getInstance(); this.cacheKey = `service:${serviceName}`; } /** * Get data with graceful degradation */ async get(options: Partial> = {}): Promise { const result = await withGracefulDegradation(this.getData, { fallback: this.defaultFallback, throwOnError: false, useCache: true, ...options, }); return result ?? this.defaultFallback; } /** * Force refresh data */ async refresh(): Promise { 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(); }