321 lines
6.9 KiB
TypeScript
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();
|
|
} |