Implement WebSocket health checks and HTTP polling for improved connection reliability

This commit is contained in:
MayaTheShy
2026-03-21 19:26:33 -04:00
parent 854fcaf66f
commit d1c9256ed8

View File

@@ -8,6 +8,29 @@ const API_URL = import.meta.env.VITE_API_URL ||
console.log('🔌 WebSocket URL:', WS_URL);
console.log('📡 API URL:', API_URL);
// ========== Timers (module-level so they survive store re-creates) ==========
let _httpPollTimer = null;
let _wsHealthTimer = null;
let _lastWsMessage = 0;
const HTTP_POLL_INTERVAL = 10_000; // Poll HTTP every 10 s as fallback
const WS_STALE_TIMEOUT = 35_000; // If no WS message for 35 s → reconnect
function _applyStateData(data, current) {
return {
inventory: data.inventory || current.inventory,
activity: data.activity || current.activity,
alerts: data.alerts || current.alerts,
smeltingPaused: data.smeltingPaused !== undefined ? data.smeltingPaused : current.smeltingPaused,
disabledRecipes: data.disabledRecipes || current.disabledRecipes,
smeltable: data.smeltable || current.smeltable,
craftable: data.craftable || current.craftable,
craftTurtleOk: data.craftTurtleOk !== undefined ? data.craftTurtleOk : current.craftTurtleOk,
bridgeConnected: data.bridgeConnected !== undefined ? data.bridgeConnected : current.bridgeConnected,
lastUpdate: data.lastUpdate || Date.now(),
};
}
export const useInventoryStore = create((set, get) => ({
// State
inventory: {
@@ -30,6 +53,7 @@ export const useInventoryStore = create((set, get) => ({
smeltable: {},
craftable: [],
craftTurtleOk: false,
bridgeConnected: false,
connected: false,
ws: null,
lastUpdate: 0,
@@ -47,23 +71,35 @@ export const useInventoryStore = create((set, get) => ({
const current = get();
// Only apply if we have actual data with items
if (data.inventory?.itemList?.length || !current.inventory.itemList?.length) {
set({
inventory: data.inventory || current.inventory,
activity: data.activity || current.activity,
alerts: data.alerts || current.alerts,
smeltingPaused: data.smeltingPaused !== undefined ? data.smeltingPaused : current.smeltingPaused,
disabledRecipes: data.disabledRecipes || current.disabledRecipes,
smeltable: data.smeltable || current.smeltable,
craftable: data.craftable || current.craftable,
craftTurtleOk: data.craftTurtleOk !== undefined ? data.craftTurtleOk : current.craftTurtleOk,
lastUpdate: data.lastUpdate || current.lastUpdate,
});
set(_applyStateData(data, current));
}
} catch (error) {
// Silent fail — WebSocket will provide data
}
},
// Start periodic HTTP polling (runs alongside WS for redundancy)
_startHttpPoll: () => {
if (_httpPollTimer) return;
_httpPollTimer = setInterval(() => {
get().fetchState();
}, HTTP_POLL_INTERVAL);
},
// Client-side WS health check — reconnect if no message received recently
_startWsHealthCheck: () => {
if (_wsHealthTimer) return;
_wsHealthTimer = setInterval(() => {
const ws = get().ws;
if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (_lastWsMessage > 0 && Date.now() - _lastWsMessage > WS_STALE_TIMEOUT) {
console.warn('⚠️ WebSocket stale (no messages for', Math.round((Date.now() - _lastWsMessage) / 1000), 's) — reconnecting');
ws.close(); // triggers onclose → reconnect
_lastWsMessage = 0; // reset so we don't loop
}
}, 10_000);
},
// WebSocket connection
connect: () => {
// Prevent duplicate connections
@@ -75,30 +111,25 @@ export const useInventoryStore = create((set, get) => ({
// Fetch state via HTTP immediately (don't wait for WS)
get().fetchState();
// Ensure background timers are running
get()._startHttpPoll();
get()._startWsHealthCheck();
const ws = new WebSocket(WS_URL);
ws.onopen = () => {
console.log('✅ Connected to server');
_lastWsMessage = Date.now();
set({ connected: true, ws });
};
ws.onmessage = (event) => {
_lastWsMessage = Date.now();
try {
const data = JSON.parse(event.data);
if (data.type === 'initial_state' || data.type === 'state_update') {
const current = get();
set({
inventory: data.inventory || current.inventory,
activity: data.activity || current.activity,
alerts: data.alerts || current.alerts,
smeltingPaused: data.smeltingPaused !== undefined ? data.smeltingPaused : current.smeltingPaused,
disabledRecipes: data.disabledRecipes || current.disabledRecipes,
smeltable: data.smeltable || current.smeltable,
craftable: data.craftable || current.craftable,
craftTurtleOk: data.craftTurtleOk !== undefined ? data.craftTurtleOk : current.craftTurtleOk,
lastUpdate: data.lastUpdate || Date.now(),
});
set(_applyStateData(data, get()));
} else if (data.type === 'command_result') {
set({ commandResult: data });
// Clear after 5s