UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

194 lines • 6.99 kB
import { AuthService } from '../services/AuthService'; export class SSEAuthHandler { constructor(options) { this.reconnectTimer = null; this.unsubscribeAuth = null; this.authService = AuthService.getInstance(); this.options = { withCredentials: true, reconnectInterval: 5000, maxReconnectAttempts: 5, ...options, }; this.connection = { eventSource: null, isConnected: false, reconnectAttempts: 0, url: options.url, }; this.setupAuthSubscription(); } async connect() { const authState = this.authService.getAuthState(); if (!authState.isAuthenticated || !authState.token) { console.warn('SSE connection requires authentication'); this.options.onAuthRequired?.(); return false; } try { await this.establishConnection(); return true; } catch (error) { console.error('Failed to establish SSE connection:', error); this.options.onError?.(error); return false; } } disconnect() { this.clearReconnectTimer(); if (this.connection.eventSource) { this.connection.eventSource.close(); this.connection.eventSource = null; } this.connection.isConnected = false; this.connection.reconnectAttempts = 0; console.log('SSE connection disconnected'); } isConnected() { return (this.connection.isConnected && this.connection.eventSource?.readyState === EventSource.OPEN); } getConnectionStatus() { return { ...this.connection }; } async establishConnection() { const authHeader = this.authService.getAuthHeader(); if (!authHeader) { throw new Error('No authentication token available'); } const token = authHeader.replace('Bearer ', ''); const separator = this.options.url.includes('?') ? '&' : '?'; const authenticatedUrl = `${this.options.url}${separator}token=${encodeURIComponent(token)}`; this.connection.eventSource = new EventSource(authenticatedUrl, { withCredentials: this.options.withCredentials, }); this.setupEventListeners(); } setupEventListeners() { if (!this.connection.eventSource) return; this.connection.eventSource.onopen = event => { console.log('SSE connection established'); this.connection.isConnected = true; this.connection.reconnectAttempts = 0; this.clearReconnectTimer(); this.options.onOpen?.(event); }; this.connection.eventSource.onmessage = event => { this.options.onMessage?.(event); }; this.connection.eventSource.onerror = event => { console.error('SSE connection error:', event); this.connection.isConnected = false; if (this.connection.eventSource?.readyState === EventSource.CLOSED) { this.handleConnectionError(); } this.options.onError?.(event); }; this.setupCustomEventHandlers(); } setupCustomEventHandlers() { if (!this.connection.eventSource) return; this.connection.eventSource.addEventListener('auth-error', event => { console.warn('SSE authentication error:', event); this.handleAuthError(); }); this.connection.eventSource.addEventListener('token-refresh', event => { console.log('Token refresh requested by server'); this.handleTokenRefresh(); }); this.connection.eventSource.addEventListener('close', event => { console.log('Server requested connection close'); this.disconnect(); this.options.onClose?.(event); }); } handleConnectionError() { if (this.connection.reconnectAttempts >= (this.options.maxReconnectAttempts || 5)) { console.error('Max reconnection attempts reached'); this.options.onAuthRequired?.(); return; } this.connection.reconnectAttempts++; const delay = this.options.reconnectInterval * Math.pow(2, this.connection.reconnectAttempts - 1); console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.connection.reconnectAttempts})`); this.reconnectTimer = window.setTimeout(() => { this.reconnect(); }, delay); } handleAuthError() { console.warn('SSE authentication failed, requesting login'); this.disconnect(); this.options.onAuthRequired?.(); } async handleTokenRefresh() { try { const refreshed = await this.authService.refreshToken(); if (refreshed) { console.log('Token refreshed, reconnecting SSE'); await this.reconnect(); } else { console.error('Token refresh failed'); this.handleAuthError(); } } catch (error) { console.error('Token refresh error:', error); this.handleAuthError(); } } async reconnect() { console.log('Attempting SSE reconnection'); if (this.connection.eventSource) { this.connection.eventSource.close(); } try { await this.establishConnection(); return true; } catch (error) { console.error('SSE reconnection failed:', error); this.handleConnectionError(); return false; } } setupAuthSubscription() { this.unsubscribeAuth = this.authService.subscribe(authState => { if (!authState.isAuthenticated && this.connection.isConnected) { console.log('User logged out, disconnecting SSE'); this.disconnect(); } }); } clearReconnectTimer() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } } addEventListener(type, listener) { this.connection.eventSource?.addEventListener(type, listener); } removeEventListener(type, listener) { this.connection.eventSource?.removeEventListener(type, listener); } destroy() { this.disconnect(); this.unsubscribeAuth?.(); this.clearReconnectTimer(); } } export function createAuthenticatedSSE(options) { return new SSEAuthHandler(options); } export const SSEEventTypes = { QUIZ_UPDATE: 'quiz-update', USER_PROGRESS: 'user-progress', SYSTEM_NOTIFICATION: 'system-notification', AUTH_ERROR: 'auth-error', TOKEN_REFRESH: 'token-refresh', CONNECTION_CLOSE: 'close', }; //# sourceMappingURL=SSEAuthHandler.js.map