/** * SSE Client - Server-Sent Events Manager * Zero-Dependency EventSource wrapper with auto-reconnect */ class SSEManager { constructor() { this.connections = new Map(); } /** * Connect to SSE endpoint * @param {string} url - SSE endpoint URL * @param {Object} handlers - Event handlers * @param {Function} [handlers.onOpen] - Connection opened callback * @param {Function} [handlers.onError] - Error callback * @param {Function} [handlers.onMessage] - Default message handler * @param {Object} [handlers.events] - Named event handlers * @returns {EventSource} */ connect(url, handlers = {}) { // Close existing connection this.disconnect(url); const eventSource = new EventSource(url); // Default handlers eventSource.onopen = () => { console.log('SSE connected:', url); handlers.onOpen?.(); }; eventSource.onerror = (error) => { console.error('SSE error:', error); handlers.onError?.(error); // Auto-reconnect after 5 seconds setTimeout(() => { console.log('SSE reconnecting:', url); this.connect(url, handlers); }, 5000); }; // Custom event handlers if (handlers.events) { Object.entries(handlers.events).forEach(([event, handler]) => { eventSource.addEventListener(event, (e) => { try { const data = JSON.parse(e.data); handler(data, e); } catch { handler(e.data, e); } }); }); } // Default message handler eventSource.onmessage = (e) => { try { const data = JSON.parse(e.data); handlers.onMessage?.(data, e); } catch { handlers.onMessage?.(e.data, e); } }; this.connections.set(url, eventSource); return eventSource; } /** * Disconnect from SSE endpoint * @param {string} url - SSE endpoint URL */ disconnect(url) { const connection = this.connections.get(url); if (connection) { connection.close(); this.connections.delete(url); console.log('SSE disconnected:', url); } } /** * Disconnect all SSE connections */ disconnectAll() { this.connections.forEach((conn, url) => { conn.close(); console.log('SSE disconnected:', url); }); this.connections.clear(); } /** * Check if connected to URL * @param {string} url * @returns {boolean} */ isConnected(url) { return this.connections.has(url); } /** * Get connection by URL * @param {string} url * @returns {EventSource|undefined} */ getConnection(url) { return this.connections.get(url); } } // Global instance window.sseManager = new SSEManager(); // Cleanup on page unload window.addEventListener('beforeunload', () => { window.sseManager.disconnectAll(); }); /** * Usage Examples: * * // 1. Notification Stream * window.sseManager.connect('/notifications/stream', { * events: { * 'notification': (data) => { * showNotification(data.message, data.type); * }, * 'heartbeat': (data) => { * console.log('Server alive:', data.status); * } * }, * onError: () => { * console.log('Reconnecting to notifications...'); * } * }); * * // 2. Live Component Updates via SSE * window.sseManager.connect('/live-component/UserCardComponent:123/stream', { * events: { * 'component-update': (data) => { * const component = window.liveComponents.components.get('UserCardComponent:123'); * if (component) { * component.element.innerHTML = data.html; * component.state = JSON.parse(data.state); * window.liveComponents.setupListeners(component.element, 'UserCardComponent:123'); * } * } * } * }); * * // 3. Job Progress Tracking * function trackJobProgress(jobId) { * const progressBar = document.getElementById('progress-bar'); * const progressText = document.getElementById('progress-text'); * * window.sseManager.connect(`/jobs/${jobId}/progress`, { * events: { * 'progress': (data) => { * progressBar.style.width = `${data.percentage}%`; * progressText.textContent = data.message; * * if (data.status === 'completed') { * window.sseManager.disconnect(`/jobs/${jobId}/progress`); * alert('Job completed!'); * } * } * } * }); * } */