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:
@@ -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') {
|
||||
|
||||
@@ -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, () => {
|
||||
|
||||
Reference in New Issue
Block a user