monacousa-portal/utils/config-cache.ts

312 lines
9.7 KiB
TypeScript

/**
* 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<RecaptchaConfig> {
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<RegistrationConfig> {
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<RecaptchaConfig> {
const globalConfigCache = getGlobalCache();
globalConfigCache.recaptcha = null;
globalConfigCache.recaptchaError = null;
return getCachedRecaptchaConfig();
}
export async function reloadRegistrationConfig(): Promise<RegistrationConfig> {
const globalConfigCache = getGlobalCache();
globalConfigCache.registration = null;
globalConfigCache.registrationError = null;
return getCachedRegistrationConfig();
}