interface SessionCheckOptions { nuxtApp?: any cacheKey?: string cacheExpiry?: number fetchFn?: () => Promise bypassCache?: boolean } interface SessionResult { user: any authenticated: boolean groups: string[] reason?: string fromCache?: boolean timestamp?: number } interface SessionCache { result: SessionResult timestamp: number expiresAt: number } /** * Centralized session management with request deduplication and caching */ class SessionManager { private static instance: SessionManager private sessionCheckPromise: Promise | null = null private sessionCheckLock = false private lastCheckTime = 0 private readonly minCheckInterval = 1000 // 1 second minimum between checks private sessionCache: Map = new Map() private readonly defaultCacheExpiry = 3 * 60 * 1000 // 3 minutes private readonly gracePeriod = 5 * 60 * 1000 // 5 minutes grace period static getInstance(): SessionManager { if (!SessionManager.instance) { SessionManager.instance = new SessionManager() } return SessionManager.instance } /** * Check session with request deduplication and caching */ async checkSession(options: SessionCheckOptions = {}): Promise { const requestId = Math.random().toString(36).substring(7) console.log(`[SESSION_MANAGER:${requestId}] Session check requested`) // Use default cache key if not provided const cacheKey = options.cacheKey || 'default' const cacheExpiry = options.cacheExpiry || this.defaultCacheExpiry // Check cache first (unless bypassing) if (!options.bypassCache) { const cached = this.getCachedSession(cacheKey) if (cached) { console.log(`[SESSION_MANAGER:${requestId}] Using cached session (age: ${Math.round((Date.now() - cached.timestamp) / 1000)}s)`) return cached.result } } // Implement request deduplication if (this.sessionCheckPromise) { console.log(`[SESSION_MANAGER:${requestId}] Using in-flight session check`) return this.sessionCheckPromise } // Prevent rapid successive checks const now = Date.now() if (now - this.lastCheckTime < this.minCheckInterval) { console.log(`[SESSION_MANAGER:${requestId}] Rate limiting - using last cached result`) const cached = this.getCachedSession(cacheKey) if (cached) { return cached.result } } this.lastCheckTime = now this.sessionCheckPromise = this.performSessionCheck(options, requestId, cacheKey, cacheExpiry) try { const result = await this.sessionCheckPromise console.log(`[SESSION_MANAGER:${requestId}] Session check completed:`, { authenticated: result.authenticated, reason: result.reason, fromCache: result.fromCache }) return result } finally { this.sessionCheckPromise = null } } /** * Get cached session if valid */ private getCachedSession(cacheKey: string): SessionCache | null { const cached = this.sessionCache.get(cacheKey) if (!cached) return null const now = Date.now() // Check if cache is still valid if (now < cached.expiresAt) { return cached } // Check if we're within grace period for network issues if (now - cached.timestamp < this.gracePeriod) { console.log(`[SESSION_MANAGER] Cache expired but within grace period`) return cached } // Remove expired cache this.sessionCache.delete(cacheKey) return null } /** * Perform actual session check */ private async performSessionCheck( options: SessionCheckOptions, requestId: string, cacheKey: string, cacheExpiry: number ): Promise { const startTime = Date.now() try { let result: SessionResult if (options.fetchFn) { console.log(`[SESSION_MANAGER:${requestId}] Using custom fetch function`) result = await options.fetchFn() } else { console.log(`[SESSION_MANAGER:${requestId}] Using default session check`) result = await this.defaultSessionCheck() } // Add metadata result.timestamp = Date.now() result.fromCache = false // Cache the result this.cacheSessionResult(cacheKey, result, cacheExpiry) const duration = Date.now() - startTime console.log(`[SESSION_MANAGER:${requestId}] Session check completed in ${duration}ms`) return result } catch (error: any) { console.error(`[SESSION_MANAGER:${requestId}] Session check failed:`, error) // Try to return cached result during network errors const cached = this.getCachedSession(cacheKey) if (cached && this.isNetworkError(error)) { console.log(`[SESSION_MANAGER:${requestId}] Using cached result due to network error`) return { ...cached.result, reason: 'NETWORK_ERROR_CACHED' } } // Return failed result return { user: null, authenticated: false, groups: [], reason: error.message || 'SESSION_CHECK_FAILED', timestamp: Date.now() } } } /** * Default session check implementation */ private async defaultSessionCheck(): Promise { // This would normally make a call to /api/auth/session // For now, return a placeholder - this will be replaced by actual API call throw new Error('Default session check not implemented - use fetchFn option') } /** * Cache session result */ private cacheSessionResult(cacheKey: string, result: SessionResult, cacheExpiry: number): void { const jitter = Math.floor(Math.random() * 10000) // 0-10 seconds jitter const expiresAt = Date.now() + cacheExpiry + jitter this.sessionCache.set(cacheKey, { result, timestamp: Date.now(), expiresAt }) console.log(`[SESSION_MANAGER] Cached session result for ${cacheExpiry + jitter}ms`) } /** * Check if error is a network error */ private isNetworkError(error: any): boolean { return error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.name === 'AbortError' || error.code === 'ENOTFOUND' || (error.status >= 500 && error.status < 600) } /** * Validate session (used by auth refresh plugin) */ async validateSession(): Promise { return this.checkSession({ cacheKey: 'validation', bypassCache: true, // Always fresh check for validation fetchFn: async () => { // This will be implemented to call the session API const response = await fetch('/api/auth/session', { headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } }) if (!response.ok) { throw new Error(`Session validation failed: ${response.status}`) } return response.json() } }) } /** * Clear all cached sessions */ clearCache(): void { this.sessionCache.clear() console.log('[SESSION_MANAGER] Session cache cleared') } /** * Get cache statistics */ getCacheStats(): { entries: number; oldestEntry: number | null; newestEntry: number | null } { const entries = this.sessionCache.size let oldestEntry: number | null = null let newestEntry: number | null = null for (const cache of this.sessionCache.values()) { if (oldestEntry === null || cache.timestamp < oldestEntry) { oldestEntry = cache.timestamp } if (newestEntry === null || cache.timestamp > newestEntry) { newestEntry = cache.timestamp } } return { entries, oldestEntry, newestEntry } } } // Export singleton instance export const sessionManager = SessionManager.getInstance() // Export class for testing export { SessionManager }