feat: add inspection_expiry/has_etc/tire_type fields, icons in schedule view, and WebSocket real-time sync
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com> Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/6d0f25ae-6db4-4937-ae2b-6674456a5ca1
This commit is contained in:
83
frontend/src/hooks/useWebSocket.js
Normal file
83
frontend/src/hooks/useWebSocket.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
|
||||
const WS_BASE = (() => {
|
||||
if (import.meta.env.VITE_WS_URL) return import.meta.env.VITE_WS_URL;
|
||||
const loc = window.location;
|
||||
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// Use the same origin as the page; in development the Vite dev server
|
||||
// proxies /ws to the backend, and in production nginx does the same.
|
||||
return `${proto}//${loc.host}/ws`;
|
||||
})();
|
||||
|
||||
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 15000, 30000]; // ms
|
||||
|
||||
/**
|
||||
* Maintains a WebSocket connection with automatic reconnection.
|
||||
*
|
||||
* @param {(message: object) => void} onMessage - Called for each parsed JSON message.
|
||||
* @returns {{ status: 'connecting'|'connected'|'disconnected'|'error' }}
|
||||
*/
|
||||
export default function useWebSocket(onMessage) {
|
||||
const [status, setStatus] = useState('connecting');
|
||||
const wsRef = useRef(null);
|
||||
const retryCountRef = useRef(0);
|
||||
const unmountedRef = useRef(false);
|
||||
const retryTimerRef = useRef(null);
|
||||
const onMessageRef = useRef(onMessage);
|
||||
|
||||
// Keep onMessage ref up-to-date without re-running the effect
|
||||
useEffect(() => {
|
||||
onMessageRef.current = onMessage;
|
||||
}, [onMessage]);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (unmountedRef.current) return;
|
||||
|
||||
setStatus('connecting');
|
||||
const ws = new WebSocket(WS_BASE);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
if (unmountedRef.current) { ws.close(); return; }
|
||||
retryCountRef.current = 0;
|
||||
setStatus('connected');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
onMessageRef.current(msg);
|
||||
} catch {
|
||||
// ignore malformed messages
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (unmountedRef.current) return;
|
||||
const delay = RECONNECT_DELAYS[Math.min(retryCountRef.current, RECONNECT_DELAYS.length - 1)];
|
||||
retryCountRef.current += 1;
|
||||
// After exhausting all backoff levels, keep status as 'error'
|
||||
if (retryCountRef.current > RECONNECT_DELAYS.length) {
|
||||
setStatus('error');
|
||||
} else {
|
||||
setStatus('disconnected');
|
||||
}
|
||||
retryTimerRef.current = setTimeout(connect, delay);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
// onclose will fire right after, which handles reconnect
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
return () => {
|
||||
unmountedRef.current = true;
|
||||
clearTimeout(retryTimerRef.current);
|
||||
wsRef.current?.close();
|
||||
};
|
||||
}, [connect]);
|
||||
|
||||
return { status };
|
||||
}
|
||||
Reference in New Issue
Block a user