312 lines
9.7 KiB
TypeScript
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();
|
|
}
|