view data fixes
This commit is contained in:
321
apps/website/lib/gateways/api/base/GracefulDegradation.ts
Normal file
321
apps/website/lib/gateways/api/base/GracefulDegradation.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 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<unknown>>();
|
||||
|
||||
/**
|
||||
* Get cached data if not expired
|
||||
*/
|
||||
get<T>(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<T>(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<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 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<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();
|
||||
}
|
||||
Reference in New Issue
Block a user