Files
awoooi/apps/web/src/hooks/useSSE.ts
Your Name 6ccdf199ad
All checks were successful
CD Pipeline / tests (push) Successful in 1m23s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 4m18s
CD Pipeline / post-deploy-checks (push) Successful in 1m44s
chore(web): 清理 IwoooS D2 註解語氣
2026-06-05 01:11:44 +08:00

150 lines
4.5 KiB
TypeScript

'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 { 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 = process.env.NEXT_PUBLIC_API_URL
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',
}
}