150 lines
4.5 KiB
TypeScript
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',
|
|
}
|
|
}
|