'use client' /** * useSSE - Server-Sent Events 連線 Hook * ===================================== * Enterprise-grade SSE 封裝 * * 特性: * - 自動重連 (Exponential Backoff) * - Heartbeat 超時偵測 * - AbortController 資源清理 * - 記憶體洩漏防禦 (cleanup) * * 絕對鐵律: * - useEffect cleanup 必須呼叫 eventSource.close() * - 否則頁面切換會產生殭屍連線 */ import { useEffect, useRef, useCallback } from 'react' import { getRuntimeApiBaseUrl } from '@/lib/runtime-api-base' import { useDashboardStore } from '@/stores/dashboard.store' // ============================================================================= // Types // ============================================================================= interface UseSSEOptions { /** 自動連線 (預設 true) */ autoConnect?: boolean /** API Base URL (覆蓋環境變數) */ apiBaseUrl?: string } interface UseSSEReturn { /** 連線狀態 */ connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error' /** 是否已連線 */ isConnected: boolean /** 是否重連中 */ isReconnecting: boolean /** 錯誤訊息 */ error: string | null /** 手動連線 */ connect: () => void /** 手動斷線 */ disconnect: () => void } // ============================================================================= // Constants // ============================================================================= const getApiBaseUrl = (): string => { // Server-side rendering 時回傳空字串 if (typeof window === 'undefined') return '' // 專案鐵律: 禁止任何 Fallback IP const url = getRuntimeApiBaseUrl() if (!url) { console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL. SSE will not connect.') return '' } return url } // ============================================================================= // Hook // ============================================================================= export function useSSE(options: UseSSEOptions = {}): UseSSEReturn { const { autoConnect = true, apiBaseUrl: customApiBaseUrl, } = options // Zustand store actions const connect = useDashboardStore((s) => s.connect) const disconnect = useDashboardStore((s) => s.disconnect) const connectionStatus = useDashboardStore((s) => s.connectionStatus) const error = useDashboardStore((s) => s.error) // Ref to track if mounted (防止 unmount 後更新 state) const mountedRef = useRef(true) // Resolve API URL const apiBaseUrl = customApiBaseUrl ?? getApiBaseUrl() // Connect handler const handleConnect = useCallback(() => { if (!mountedRef.current) return console.log('[useSSE] Initiating connection to:', apiBaseUrl) connect(apiBaseUrl) }, [apiBaseUrl, connect]) // Disconnect handler const handleDisconnect = useCallback(() => { console.log('[useSSE] Disconnecting...') disconnect() }, [disconnect]) // ========================================================================== // Effect: 自動連線 + 資源清理 (記憶體洩漏防禦) // ========================================================================== useEffect(() => { mountedRef.current = true if (autoConnect && apiBaseUrl) { console.log('[useSSE] Auto-connecting...') handleConnect() } // ⚠️ CRITICAL: 清理函數 - 防止殭屍連線 // 此處必須呼叫 disconnect() 關閉 EventSource // 否則頁面切換時連線不會關閉,造成記憶體洩漏 return () => { console.log('[useSSE] Cleanup: closing EventSource') mountedRef.current = false handleDisconnect() } }, [autoConnect, apiBaseUrl, handleConnect, handleDisconnect]) return { connectionStatus, isConnected: connectionStatus === 'connected', isReconnecting: connectionStatus === 'reconnecting', error, connect: handleConnect, disconnect: handleDisconnect, } } // ============================================================================= // Utility Hook: SSE 連線狀態顯示 // ============================================================================= export function useSSEStatus() { const connectionStatus = useDashboardStore((s) => s.connectionStatus) const reconnectAttempts = useDashboardStore((s) => s.reconnectAttempts) const lastConnected = useDashboardStore((s) => s.lastConnected) const error = useDashboardStore((s) => s.error) return { status: connectionStatus, attempts: reconnectAttempts, lastConnected, error, isHealthy: connectionStatus === 'connected', needsAttention: connectionStatus === 'error' || connectionStatus === 'reconnecting', } }