Implement WebSocket health checks and HTTP polling for improved connection reliability
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user