mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
One-time renormalization pass under the .gitattributes added in the previous commit. Every tracked text file now uses LF. No semantic changes — verified via git diff --cached --ignore-all-space showing zero real differences. Future diffs will only show real content changes. This commit will appear huge in git log --stat but represents zero behavior change. Use git log --follow --ignore-all-space or git blame -w when archaeologically tracing through this commit.
109 lines
3 KiB
TypeScript
109 lines
3 KiB
TypeScript
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
import type { MeshHealth, Alert, EnvEvent } from '@/lib/api'
|
|
|
|
interface WebSocketMessage {
|
|
type: string
|
|
data?: unknown
|
|
event?: EnvEvent
|
|
}
|
|
|
|
interface UseWebSocketReturn {
|
|
connected: boolean
|
|
lastHealth: MeshHealth | null
|
|
lastAlert: Alert | null
|
|
lastMessage: WebSocketMessage | null
|
|
}
|
|
|
|
export function useWebSocket(): UseWebSocketReturn {
|
|
const [connected, setConnected] = useState(false)
|
|
const [lastHealth, setLastHealth] = useState<MeshHealth | null>(null)
|
|
const [lastAlert, setLastAlert] = useState<Alert | null>(null)
|
|
const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null)
|
|
const wsRef = useRef<WebSocket | null>(null)
|
|
const reconnectTimeoutRef = useRef<number | null>(null)
|
|
const reconnectDelayRef = useRef(1000)
|
|
|
|
const connect = useCallback(() => {
|
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
return
|
|
}
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
const wsUrl = `${protocol}//${window.location.host}/ws/live`
|
|
|
|
try {
|
|
const ws = new WebSocket(wsUrl)
|
|
wsRef.current = ws
|
|
|
|
ws.onopen = () => {
|
|
setConnected(true)
|
|
reconnectDelayRef.current = 1000 // Reset backoff on successful connection
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const message: WebSocketMessage = JSON.parse(event.data)
|
|
|
|
// Store all messages for generic handling
|
|
setLastMessage(message)
|
|
|
|
switch (message.type) {
|
|
case 'health_update':
|
|
setLastHealth(message.data as MeshHealth)
|
|
break
|
|
case 'alert_fired':
|
|
setLastAlert(message.data as Alert)
|
|
break
|
|
// env_update messages are handled via lastMessage
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to parse WebSocket message:', e)
|
|
}
|
|
}
|
|
|
|
ws.onclose = () => {
|
|
setConnected(false)
|
|
wsRef.current = null
|
|
|
|
// Schedule reconnect with exponential backoff
|
|
const delay = Math.min(reconnectDelayRef.current, 30000)
|
|
reconnectTimeoutRef.current = window.setTimeout(() => {
|
|
reconnectDelayRef.current = Math.min(delay * 2, 30000)
|
|
connect()
|
|
}, delay)
|
|
}
|
|
|
|
ws.onerror = () => {
|
|
ws.close()
|
|
}
|
|
|
|
// Keepalive ping every 30 seconds
|
|
const pingInterval = setInterval(() => {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send('ping')
|
|
}
|
|
}, 30000)
|
|
|
|
ws.addEventListener('close', () => {
|
|
clearInterval(pingInterval)
|
|
})
|
|
} catch (e) {
|
|
console.error('Failed to create WebSocket:', e)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
connect()
|
|
|
|
return () => {
|
|
if (reconnectTimeoutRef.current) {
|
|
clearTimeout(reconnectTimeoutRef.current)
|
|
}
|
|
if (wsRef.current) {
|
|
wsRef.current.close()
|
|
}
|
|
}
|
|
}, [connect])
|
|
|
|
return { connected, lastHealth, lastAlert, lastMessage }
|
|
}
|