From c25ef9f2cc358f3ddb229eb60c24ba5bb9943c54 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sat, 21 Mar 2026 18:35:17 -0400 Subject: [PATCH] Fix inventory disappearing: add WS keep-alive pings + HTTP API fallback - Server pings web clients every 25s to keep connections alive through reverse proxies - Client fetches /api/inventory on page load (doesn't depend solely on WebSocket) - Prevent duplicate WebSocket connections on reconnect - Deduplicate initial_state/state_update handlers --- web/client/src/store/inventoryStore.js | 66 ++++++++++++++++++-------- web/server/server.js | 21 +++++++- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/web/client/src/store/inventoryStore.js b/web/client/src/store/inventoryStore.js index bd3724f..720eb0b 100644 --- a/web/client/src/store/inventoryStore.js +++ b/web/client/src/store/inventoryStore.js @@ -36,8 +36,43 @@ export const useInventoryStore = create((set, get) => ({ searchQuery: '', commandResult: null, + // Fetch state via HTTP API (fallback / initial load) + fetchState: async () => { + try { + const response = await fetch(`${API_URL}/inventory`); + if (!response.ok) return; + const data = await response.json(); + 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, + }); + } + } catch (error) { + // Silent fail — WebSocket will provide data + } + }, + // WebSocket connection connect: () => { + // Prevent duplicate connections + const existing = get().ws; + if (existing && (existing.readyState === WebSocket.CONNECTING || existing.readyState === WebSocket.OPEN)) { + return; + } + + // Fetch state via HTTP immediately (don't wait for WS) + get().fetchState(); + const ws = new WebSocket(WS_URL); ws.onopen = () => { @@ -49,28 +84,17 @@ export const useInventoryStore = create((set, get) => ({ try { const data = JSON.parse(event.data); - if (data.type === 'initial_state') { + if (data.type === 'initial_state' || data.type === 'state_update') { + const current = get(); set({ - inventory: data.inventory || get().inventory, - activity: data.activity || {}, - alerts: data.alerts || [], - smeltingPaused: data.smeltingPaused || false, - disabledRecipes: data.disabledRecipes || {}, - smeltable: data.smeltable || {}, - craftable: data.craftable || [], - craftTurtleOk: data.craftTurtleOk || false, - lastUpdate: data.lastUpdate || Date.now(), - }); - } else if (data.type === 'state_update') { - set({ - inventory: data.inventory || get().inventory, - activity: data.activity || get().activity, - alerts: data.alerts || get().alerts, - smeltingPaused: data.smeltingPaused !== undefined ? data.smeltingPaused : get().smeltingPaused, - disabledRecipes: data.disabledRecipes || get().disabledRecipes, - smeltable: data.smeltable || get().smeltable, - craftable: data.craftable || get().craftable, - craftTurtleOk: data.craftTurtleOk !== undefined ? data.craftTurtleOk : get().craftTurtleOk, + 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(), }); } else if (data.type === 'command_result') { diff --git a/web/server/server.js b/web/server/server.js index 31c9b87..181456e 100644 --- a/web/server/server.js +++ b/web/server/server.js @@ -469,6 +469,7 @@ wss.on('connection', (ws, req) => { // ---- Web client WebSocket connection ---- console.log('🌐 New web client connected'); webClients.add(ws); + ws.isAlive = true; // Send current state to new client ws.send(JSON.stringify({ @@ -484,10 +485,11 @@ wss.on('connection', (ws, req) => { lastUpdate, })); + ws.on('pong', () => { ws.isAlive = true; }); + ws.on('message', (message) => { try { const data = JSON.parse(message); - console.log('📨 Received from web client:', data); if (data.type === 'command') { // Forward command to bridge @@ -508,6 +510,23 @@ wss.on('connection', (ws, req) => { }); }); +// ========== WebSocket Keep-Alive ========== +// Ping all web clients every 25s to keep connections alive through reverse proxies +const WS_PING_INTERVAL = setInterval(() => { + webClients.forEach((ws) => { + if (!ws.isAlive) { + webClients.delete(ws); + return ws.terminate(); + } + ws.isAlive = false; + ws.ping(); + }); +}, 25000); + +wss.on('close', () => { + clearInterval(WS_PING_INTERVAL); +}); + // ========== Start Server ========== server.listen(PORT, HOST, () => {