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
This commit is contained in:
MayaTheShy
2026-03-21 18:35:17 -04:00
parent 4af91235e1
commit c25ef9f2cc
2 changed files with 65 additions and 22 deletions

View File

@@ -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') {

View File

@@ -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, () => {