fix: stabilize datasource progress and websocket flow

This commit is contained in:
linkong
2026-04-01 12:04:28 +08:00
parent 126d4dadb7
commit b8f70f8b71
11 changed files with 408 additions and 134 deletions

View File

@@ -1,7 +1,38 @@
import { useEffect, useRef, useState, useCallback } from 'react'
import { useAuthStore } from '../stores/auth'
const WS_URL = (import.meta as any).env?.VITE_WS_URL || 'ws://localhost:8000/ws'
const DEFAULT_WS_URL = (() => {
if (typeof window === 'undefined') {
return 'ws://localhost:8000/ws'
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
return `${protocol}//${window.location.host}/ws`
})()
const WS_URL = (import.meta as any).env?.VITE_WS_URL || DEFAULT_WS_URL
function buildWebSocketCandidates(): string[] {
if ((import.meta as any).env?.VITE_WS_URL) {
return [WS_URL]
}
if (typeof window === 'undefined') {
return ['ws://localhost:8000/ws']
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const sameOrigin = `${protocol}//${window.location.host}/ws`
const candidates = [sameOrigin]
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
const directBackend = `${protocol}//${window.location.hostname}:8000/ws`
if (!candidates.includes(directBackend)) {
candidates.push(directBackend)
}
}
return candidates
}
interface WebSocketMessage {
type: string
@@ -41,71 +72,130 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
} = options
const { token } = useAuthStore()
console.log('[WebSocket] Token present:', !!token, token ? token.substring(0, 20) + '...' : 'none')
console.log('[WebSocket] autoConnect:', autoConnect)
const wsRef = useRef<WebSocket | null>(null)
const [connected, setConnected] = useState(false)
const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null)
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const heartbeatTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const activeWsUrlRef = useRef<string | null>(null)
const intentionalCloseRef = useRef(false)
const pendingCloseSocketRef = useRef<WebSocket | null>(null)
const autoSubscribeRef = useRef(autoSubscribe)
const onMessageRef = useRef(onMessage)
const onConnectRef = useRef(onConnect)
const onDisconnectRef = useRef(onDisconnect)
const onErrorRef = useRef(onError)
useEffect(() => {
autoSubscribeRef.current = autoSubscribe
onMessageRef.current = onMessage
onConnectRef.current = onConnect
onDisconnectRef.current = onDisconnect
onErrorRef.current = onError
}, [autoSubscribe, onMessage, onConnect, onDisconnect, onError])
const connect = useCallback(() => {
if (!token) {
console.log('[WebSocket] No token, skipping connect')
return
}
const wsUrl = `${WS_URL}?token=${token}`
console.log('[WebSocket] Connecting to:', wsUrl)
const ws = new WebSocket(wsUrl)
intentionalCloseRef.current = false
const candidates = buildWebSocketCandidates()
let candidateIndex = 0
let opened = false
ws.onopen = () => {
console.log('[WebSocket] Connected!')
setConnected(true)
if (autoSubscribe.length > 0) {
ws.send(JSON.stringify({ type: 'subscribe', data: { channels: autoSubscribe } }))
const tryConnect = () => {
const baseUrl = candidates[candidateIndex]
const wsUrl = `${baseUrl}?token=${token}`
activeWsUrlRef.current = baseUrl
const ws = new WebSocket(wsUrl)
ws.onopen = () => {
if (intentionalCloseRef.current || pendingCloseSocketRef.current === ws) {
pendingCloseSocketRef.current = null
ws.close()
return
}
opened = true
setConnected(true)
if (autoSubscribeRef.current.length > 0) {
ws.send(JSON.stringify({ type: 'subscribe', data: { channels: autoSubscribeRef.current } }))
}
onConnectRef.current?.()
}
onConnect?.()
ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data)
setLastMessage(message)
onMessageRef.current?.(message)
} catch {
console.error('Failed to parse WebSocket message')
}
}
ws.onclose = () => {
if (wsRef.current === ws) {
wsRef.current = null
}
if (pendingCloseSocketRef.current === ws) {
pendingCloseSocketRef.current = null
}
setConnected(false)
if (heartbeatTimerRef.current) {
clearInterval(heartbeatTimerRef.current)
heartbeatTimerRef.current = null
}
if (!opened && candidateIndex < candidates.length - 1) {
candidateIndex += 1
tryConnect()
return
}
if (intentionalCloseRef.current) {
return
}
onDisconnectRef.current?.()
if (autoConnect && token) {
reconnectTimeoutRef.current = setTimeout(() => {
connect()
}, 3000)
}
}
ws.onerror = (error) => {
setConnected(false)
if (intentionalCloseRef.current || ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED) {
return
}
if (candidateIndex >= candidates.length - 1) {
console.warn('[WebSocket] Connection error', { url: baseUrl, error })
onErrorRef.current?.(new Error('WebSocket error'))
}
}
wsRef.current = ws
}
ws.onmessage = (event) => {
console.log('[WebSocket] Received:', event.data)
try {
const message: WebSocketMessage = JSON.parse(event.data)
setLastMessage(message)
onMessage?.(message)
} catch {
console.error('Failed to parse WebSocket message')
}
}
ws.onclose = (event) => {
console.log('[WebSocket] Disconnected:', event.code, event.reason)
try {
tryConnect()
} catch (error) {
setConnected(false)
if (heartbeatTimerRef.current) {
clearInterval(heartbeatTimerRef.current)
heartbeatTimerRef.current = null
}
onDisconnect?.()
console.warn('[WebSocket] Failed to initialize connection', { url: activeWsUrlRef.current, error })
if (autoConnect && token) {
reconnectTimeoutRef.current = setTimeout(() => {
console.log('[WebSocket] Reconnecting...')
connect()
}, 3000)
}
}
ws.onerror = (error) => {
console.error('[WebSocket] Error:', error)
onError?.(new Error('WebSocket error'))
}
wsRef.current = ws
}, [token, autoConnect, autoSubscribe, onConnect, onDisconnect, onError])
}, [token, autoConnect])
const disconnect = useCallback(() => {
intentionalCloseRef.current = true
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
reconnectTimeoutRef.current = null
@@ -114,8 +204,14 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
clearInterval(heartbeatTimerRef.current)
heartbeatTimerRef.current = null
}
wsRef.current?.close()
wsRef.current = null
const socket = wsRef.current
if (socket) {
if (socket.readyState === WebSocket.CONNECTING) {
pendingCloseSocketRef.current = socket
} else if (socket.readyState === WebSocket.OPEN) {
socket.close()
}
}
setConnected(false)
}, [])
@@ -131,7 +227,6 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
}, [])
useEffect(() => {
console.log('[WebSocket] useEffect triggered, autoConnect:', autoConnect, 'token:', !!token)
if (autoConnect && token) {
connect()
}