mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 19:46:38 +08:00
优化,排查项目问题
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import type { SSEEvent } from '../types'
|
||||
|
||||
type SSEEventHandler = (event: SSEEvent) => void
|
||||
|
||||
/**
|
||||
* useSSE hook - 管理 Server-Sent Events 连接
|
||||
*
|
||||
* @param onEvent - 事件处理函数
|
||||
* @param options - 配置选项
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const { connect, disconnect } = useSSE((event) => {
|
||||
* if (event.type === 'scan') {
|
||||
* updateScanProgress(event.payload)
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* useEffect(() => {
|
||||
* connect()
|
||||
* return () => disconnect()
|
||||
* }, [])
|
||||
*
|
||||
* return <div>SSE Demo</div>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useSSE(
|
||||
onEvent: SSEEventHandler,
|
||||
options: { autoConnect?: boolean } = {}
|
||||
) {
|
||||
const { autoConnect = true } = options
|
||||
const onEventRef = useRef(onEvent)
|
||||
onEventRef.current = onEvent
|
||||
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const isConnectedRef = useRef(false)
|
||||
const reconnectAttemptsRef = useRef(0)
|
||||
const maxReconnectAttempts = 5
|
||||
|
||||
const connect = useCallback(() => {
|
||||
// 如果已有连接,先断开
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
}
|
||||
|
||||
const token = useAuthStore.getState().token
|
||||
if (!token) {
|
||||
console.warn('Cannot connect to SSE: No auth token')
|
||||
return
|
||||
}
|
||||
|
||||
const url = `/api/events?token=${encodeURIComponent(token)}`
|
||||
const eventSource = new EventSource(url)
|
||||
eventSourceRef.current = eventSource
|
||||
|
||||
eventSource.onopen = () => {
|
||||
isConnectedRef.current = true
|
||||
reconnectAttemptsRef.current = 0
|
||||
}
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as SSEEvent
|
||||
onEventRef.current(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to parse SSE event:', err)
|
||||
}
|
||||
}
|
||||
|
||||
eventSource.onerror = () => {
|
||||
isConnectedRef.current = false
|
||||
eventSource.close()
|
||||
|
||||
// 尝试重连
|
||||
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000)
|
||||
reconnectAttemptsRef.current++
|
||||
reconnectTimeoutRef.current = setTimeout(connect, delay)
|
||||
} else {
|
||||
console.error('SSE connection failed after max attempts')
|
||||
}
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
eventSourceRef.current = null
|
||||
}
|
||||
isConnectedRef.current = false
|
||||
reconnectAttemptsRef.current = 0
|
||||
}, [])
|
||||
|
||||
const isConnected = useCallback(() => {
|
||||
return isConnectedRef.current
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (autoConnect) {
|
||||
connect()
|
||||
}
|
||||
return () => {
|
||||
disconnect()
|
||||
}
|
||||
}, [autoConnect, connect, disconnect])
|
||||
|
||||
return {
|
||||
connect,
|
||||
disconnect,
|
||||
isConnected,
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@ import { useEffect, useRef } from 'react'
|
||||
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const MAX_RECONNECT_ATTEMPTS = 5
|
||||
// 前 5 次沿用原快速退避间隔;之后进入 60s 慢速重试并不再放弃,
|
||||
// 服务重启或网络恢复后仍能自动重连(清理函数可随时取消定时器)。
|
||||
const FAST_RECONNECT_ATTEMPTS = 5
|
||||
const SLOW_RECONNECT_INTERVAL = 60_000
|
||||
|
||||
// useWebSocket opens a single connection to /api/ws and dispatches every
|
||||
// message to the supplied handler. Auto-reconnects with back-off while the
|
||||
// auth token is present, but stops after repeated failures so an expired token
|
||||
// cannot create an endless /api/ws 401 loop.
|
||||
// auth token is present; after the fast retries are exhausted it keeps a
|
||||
// slow 60s retry loop instead of giving up permanently.
|
||||
export function useWebSocket(onEvent: (topic: string, payload: unknown) => void) {
|
||||
const ref = useRef<WebSocket | null>(null)
|
||||
const token = useAuthStore((s) => s.token)
|
||||
@@ -22,7 +25,6 @@ export function useWebSocket(onEvent: (topic: string, payload: unknown) => void)
|
||||
|
||||
const open = () => {
|
||||
if (closed) return
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return
|
||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const url = `${proto}//${window.location.host}/api/ws?token=${encodeURIComponent(token)}`
|
||||
const ws = new WebSocket(url)
|
||||
@@ -43,7 +45,12 @@ export function useWebSocket(onEvent: (topic: string, payload: unknown) => void)
|
||||
ws.onclose = () => {
|
||||
if (closed) return
|
||||
reconnectAttempts += 1
|
||||
const delay = Math.min(3_000 * reconnectAttempts, 30_000)
|
||||
// 快速阶段保持原有线性退避,之后固定 60s 慢速重试;
|
||||
// timer 始终只有一个在途,cleanup 时统一清除,不会堆积。
|
||||
const delay =
|
||||
reconnectAttempts <= FAST_RECONNECT_ATTEMPTS
|
||||
? Math.min(3_000 * reconnectAttempts, 30_000)
|
||||
: SLOW_RECONNECT_INTERVAL
|
||||
timer = window.setTimeout(open, delay)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user