diff --git a/src/App.jsx b/src/App.jsx index 66278a9..9125536 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,10 @@ -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import './App.css' const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://app-01:8080' +const WS_URL = import.meta.env.VITE_WS_URL || '' const POLL_INTERVAL_MS = 10000 +const RECONNECT_DELAY_MS = 5000 const statusColors = { queued: 'status-queued', @@ -70,37 +72,92 @@ function App() { const [sort, setSort] = useState({ key: 'created_at', direction: 'desc' }) const [filters, setFilters] = useState(defaultFilters) const [selectedId, setSelectedId] = useState('') + const [transport, setTransport] = useState(WS_URL ? 'connecting websocket…' : 'polling') + + const load = useCallback(async () => { + try { + const response = await fetch(`${API_BASE_URL}/work`) + if (!response.ok) throw new Error(`Request failed with ${response.status}`) + const data = await response.json() + setItems(data) + setError('') + setSelectedId((current) => { + if (current && data.some((item) => item.id === current)) return current + return data[0]?.id ?? '' + }) + } catch (err) { + setError(err.message || 'Failed to load queue') + } finally { + setLoading(false) + } + }, []) useEffect(() => { - let cancelled = false + let active = true + let socket + let pollTimer + let reconnectTimer + + const startPolling = () => { + setTransport('polling') + load() + pollTimer = window.setInterval(load, POLL_INTERVAL_MS) + } + + const stopPolling = () => { + if (pollTimer) window.clearInterval(pollTimer) + } + + const connectWebSocket = () => { + if (!WS_URL || !active) { + startPolling() + return + } - async function load() { try { - const response = await fetch(`${API_BASE_URL}/work`) - if (!response.ok) throw new Error(`Request failed with ${response.status}`) - const data = await response.json() - if (!cancelled) { - setItems(data) - setError('') - setSelectedId((current) => { - if (current && data.some((item) => item.id === current)) return current - return data[0]?.id ?? '' - }) - } - } catch (err) { - if (!cancelled) setError(err.message || 'Failed to load queue') - } finally { - if (!cancelled) setLoading(false) + socket = new WebSocket(WS_URL) + } catch { + startPolling() + return + } + + socket.onopen = () => { + if (!active) return + stopPolling() + setTransport('websocket') + load() + } + + socket.onmessage = () => { + if (!active) return + load() + } + + socket.onerror = () => { + if (!active) return + setTransport('polling') + } + + socket.onclose = () => { + if (!active) return + startPolling() + reconnectTimer = window.setTimeout(() => { + stopPolling() + connectWebSocket() + }, RECONNECT_DELAY_MS) } } load() - const timer = setInterval(load, POLL_INTERVAL_MS) + connectWebSocket() + return () => { - cancelled = true - clearInterval(timer) + active = false + stopPolling() + if (reconnectTimer) window.clearTimeout(reconnectTimer) + if (socket && socket.readyState < 2) socket.close() } - }, []) + }, [load]) const filterOptions = useMemo( () => ({ @@ -156,7 +213,7 @@ function App() {