/** * Global Configuration Cache Utility * Prevents repeated API calls and manages singleton pattern for configs * Designed to stop Safari iOS reload loops caused by repeated config fetches */ import type { RecaptchaConfig, RegistrationConfig } from './types'; // Global cache storage - use window object to persist across Vue reactivity cycles interface ConfigCache { recaptcha: RecaptchaConfig | null; registration: RegistrationConfig | null; recaptchaLoading: boolean; registrationLoading: boolean; recaptchaError: string | null; registrationError: string | null; } // Use window object for true persistence across component lifecycle function getGlobalCache(): ConfigCache { if (typeof window === 'undefined') { return { recaptcha: null, registration: null, recaptchaLoading: false, registrationLoading: false, recaptchaError: null, registrationError: null }; } if (!(window as any).__configCache) { (window as any).__configCache = { recaptcha: null, registration: null, recaptchaLoading: false, registrationLoading: false, recaptchaError: null, registrationError: null }; } return (window as any).__configCache; } // Circuit breaker to prevent rapid successive calls const CIRCUIT_BREAKER_THRESHOLD = 5; // Max calls in time window const CIRCUIT_BREAKER_WINDOW = 10000; // 10 seconds const callHistory: { [key: string]: number[] } = {}; /** * Check if API calls should be blocked due to circuit breaker */ function shouldBlockCall(apiName: string): boolean { const now = Date.now(); const history = callHistory[apiName] || []; // Clean old calls outside the time window const recentCalls = history.filter(time => now - time < CIRCUIT_BREAKER_WINDOW); callHistory[apiName] = recentCalls; if (recentCalls.length >= CIRCUIT_BREAKER_THRESHOLD) { console.warn(`[config-cache] Circuit breaker activated for ${apiName} - too many calls`); return true; } // Record this call recentCalls.push(now); return false; } /** * Get reCAPTCHA configuration with caching and circuit breaker */ export async function getCachedRecaptchaConfig(): Promise { const globalConfigCache = getGlobalCache(); // Return cached result if available if (globalConfigCache.recaptcha) { console.log('[config-cache] Returning cached reCAPTCHA config'); return globalConfigCache.recaptcha; } // Check if already loading if (globalConfigCache.recaptchaLoading) { console.log('[config-cache] reCAPTCHA config already loading, waiting...'); // Wait for loading to complete return new Promise((resolve, reject) => { const checkInterval = setInterval(() => { if (!globalConfigCache.recaptchaLoading) { clearInterval(checkInterval); if (globalConfigCache.recaptcha) { resolve(globalConfigCache.recaptcha); } else if (globalConfigCache.recaptchaError) { reject(new Error(globalConfigCache.recaptchaError)); } else { reject(new Error('Unknown error loading reCAPTCHA config')); } } }, 100); // Timeout after 10 seconds setTimeout(() => { clearInterval(checkInterval); reject(new Error('Timeout waiting for reCAPTCHA config')); }, 10000); }); } // Check circuit breaker if (shouldBlockCall('recaptcha-config')) { const fallbackConfig: RecaptchaConfig = { siteKey: '', secretKey: '' }; globalConfigCache.recaptcha = fallbackConfig; return fallbackConfig; } try { console.log('[config-cache] Loading reCAPTCHA config...'); globalConfigCache.recaptchaLoading = true; globalConfigCache.recaptchaError = null; const response = await $fetch('/api/recaptcha-config') as any; if (response?.success && response?.data) { globalConfigCache.recaptcha = response.data; console.log('[config-cache] reCAPTCHA config loaded successfully'); return response.data; } else { throw new Error('Invalid reCAPTCHA config response'); } } catch (error: any) { const errorMessage = error.message || 'Failed to load reCAPTCHA config'; console.warn('[config-cache] reCAPTCHA config load failed:', errorMessage); globalConfigCache.recaptchaError = errorMessage; // Return fallback config instead of throwing const fallbackConfig: RecaptchaConfig = { siteKey: '', secretKey: '' }; globalConfigCache.recaptcha = fallbackConfig; return fallbackConfig; } finally { globalConfigCache.recaptchaLoading = false; } } /** * Get registration configuration with caching and circuit breaker */ export async function getCachedRegistrationConfig(): Promise { const globalConfigCache = getGlobalCache(); // Return cached result if available if (globalConfigCache.registration) { console.log('[config-cache] Returning cached registration config'); return globalConfigCache.registration; } // Check if already loading if (globalConfigCache.registrationLoading) { console.log('[config-cache] Registration config already loading, waiting...'); // Wait for loading to complete return new Promise((resolve, reject) => { const checkInterval = setInterval(() => { if (!globalConfigCache.registrationLoading) { clearInterval(checkInterval); if (globalConfigCache.registration) { resolve(globalConfigCache.registration); } else if (globalConfigCache.registrationError) { reject(new Error(globalConfigCache.registrationError)); } else { reject(new Error('Unknown error loading registration config')); } } }, 100); // Timeout after 10 seconds setTimeout(() => { clearInterval(checkInterval); reject(new Error('Timeout waiting for registration config')); }, 10000); }); } // Check circuit breaker if (shouldBlockCall('registration-config')) { const fallbackConfig: RegistrationConfig = { membershipFee: 150, iban: 'MC58 1756 9000 0104 0050 1001 860', accountHolder: 'ASSOCIATION MONACO USA' }; globalConfigCache.registration = fallbackConfig; return fallbackConfig; } try { console.log('[config-cache] Loading registration config...'); globalConfigCache.registrationLoading = true; globalConfigCache.registrationError = null; const response = await $fetch('/api/registration-config') as any; if (response?.success && response?.data) { globalConfigCache.registration = response.data; console.log('[config-cache] Registration config loaded successfully'); return response.data; } else { throw new Error('Invalid registration config response'); } } catch (error: any) { const errorMessage = error.message || 'Failed to load registration config'; console.warn('[config-cache] Registration config load failed:', errorMessage); globalConfigCache.registrationError = errorMessage; // Return fallback config instead of throwing const fallbackConfig: RegistrationConfig = { membershipFee: 150, iban: 'MC58 1756 9000 0104 0050 1001 860', accountHolder: 'ASSOCIATION MONACO USA' }; globalConfigCache.registration = fallbackConfig; return fallbackConfig; } finally { globalConfigCache.registrationLoading = false; } } /** * Load both configs with optimal batching * Useful for components that need both configs */ export async function loadAllConfigs(): Promise<{ recaptcha: RecaptchaConfig; registration: RegistrationConfig; }> { try { const [recaptcha, registration] = await Promise.allSettled([ getCachedRecaptchaConfig(), getCachedRegistrationConfig() ]); return { recaptcha: recaptcha.status === 'fulfilled' ? recaptcha.value : { siteKey: '', secretKey: '' }, registration: registration.status === 'fulfilled' ? registration.value : { membershipFee: 150, iban: 'MC58 1756 9000 0104 0050 1001 860', accountHolder: 'ASSOCIATION MONACO USA' } }; } catch (error) { console.error('[config-cache] Error loading configs:', error); // Return fallback configs return { recaptcha: { siteKey: '', secretKey: '' }, registration: { membershipFee: 150, iban: 'MC58 1756 9000 0104 0050 1001 860', accountHolder: 'ASSOCIATION MONACO USA' } }; } } /** * Clear all cached configurations (useful for testing or cache refresh) */ export function clearConfigCache(): void { console.log('[config-cache] Clearing all cached configurations'); if (typeof window !== 'undefined' && (window as any).__configCache) { (window as any).__configCache = { recaptcha: null, registration: null, recaptchaLoading: false, registrationLoading: false, recaptchaError: null, registrationError: null }; } // Clear call history for circuit breaker Object.keys(callHistory).forEach(key => { delete callHistory[key]; }); } /** * Get cache status for debugging */ export function getConfigCacheStatus(): ConfigCache { return { ...getGlobalCache() }; } /** * Force reload specific config (bypasses cache) */ export async function reloadRecaptchaConfig(): Promise { const globalConfigCache = getGlobalCache(); globalConfigCache.recaptcha = null; globalConfigCache.recaptchaError = null; return getCachedRecaptchaConfig(); } export async function reloadRegistrationConfig(): Promise { const globalConfigCache = getGlobalCache(); globalConfigCache.registration = null; globalConfigCache.registrationError = null; return getCachedRegistrationConfig(); }