From 460cf34252eedee4b0c4307d8778a6c2813976a8 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sun, 22 Mar 2026 02:38:30 -0400 Subject: [PATCH] Enhance WebSocket error handling and implement command idempotency for dropper nickname and recipe management endpoints --- web/server/server.js | 74 ++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/web/server/server.js b/web/server/server.js index 8388b13..dee8df0 100644 --- a/web/server/server.js +++ b/web/server/server.js @@ -107,7 +107,11 @@ function broadcastToClients(data) { const message = JSON.stringify(data); webClients.forEach((client) => { if (client.readyState === 1) { - client.send(message); + try { + client.send(message); + } catch (err) { + console.error('āŒ WS send error (client):', err.message); + } } }); } @@ -116,8 +120,12 @@ function pushCommandToBridge(command) { let sent = false; for (const bridge of bridgeClients) { if (bridge.readyState === 1) { - bridge.send(JSON.stringify(command)); - sent = true; + try { + bridge.send(JSON.stringify(command)); + sent = true; + } catch (err) { + console.error('āŒ WS send error (bridge):', err.message); + } } } if (!sent) { @@ -352,7 +360,11 @@ app.get('/api/dropper-nicknames', (req, res) => { // Set a single dropper nickname app.post('/api/dropper-nicknames', (req, res) => { try { - const { dropperName, nickname } = req.body; + const { dropperName, nickname, commandId } = req.body; + + const cached = checkIdempotent(commandId); + if (cached) return res.json(cached); + if (!dropperName) return res.status(400).json({ error: 'Missing dropperName' }); if (nickname && nickname.trim()) { @@ -363,7 +375,9 @@ app.post('/api/dropper-nicknames', (req, res) => { saveState('dropperNicknames', dropperNicknames); console.log(`šŸ·ļø Dropper nickname: ${dropperName} → ${nickname || '(removed)'}`); - res.json({ success: true, nicknames: dropperNicknames }); + const result = { success: true, nicknames: dropperNicknames }; + recordCommand(commandId, result); + res.json(result); } catch (error) { res.status(500).json({ error: error.message }); } @@ -427,25 +441,33 @@ app.post('/api/recipes/toggle', (req, res) => { // Enable/disable all recipes app.post('/api/recipes/enable-all', (req, res) => { - const { commandId } = req.body || {}; - const cached = checkIdempotent(commandId); - if (cached) return res.json(cached); + try { + const { commandId } = req.body || {}; + const cached = checkIdempotent(commandId); + if (cached) return res.json(cached); - pushCommandToBridge({ type: 'command', action: 'enable_all', commandId }); - const result = { success: true, commandId }; - recordCommand(commandId, result); - res.json(result); + pushCommandToBridge({ type: 'command', action: 'enable_all', commandId }); + const result = { success: true, commandId }; + recordCommand(commandId, result); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } }); app.post('/api/recipes/disable-all', (req, res) => { - const { commandId } = req.body || {}; - const cached = checkIdempotent(commandId); - if (cached) return res.json(cached); + try { + const { commandId } = req.body || {}; + const cached = checkIdempotent(commandId); + if (cached) return res.json(cached); - pushCommandToBridge({ type: 'command', action: 'disable_all', commandId }); - const result = { success: true, commandId }; - recordCommand(commandId, result); - res.json(result); + pushCommandToBridge({ type: 'command', action: 'disable_all', commandId }); + const result = { success: true, commandId }; + recordCommand(commandId, result); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } }); // Sort barrel @@ -662,6 +684,7 @@ function updateStateFromBridge(data) { smeltable: smeltableRecipes, craftable: craftableRecipes, craftTurtleOk, + dropperNicknames, lastUpdate, bridgeConnected: bridgeClients.size > 0, }); @@ -865,13 +888,24 @@ server.listen(PORT, HOST, () => { function shutdown() { console.log('\nšŸ›‘ Shutting down server...'); try { + wss.close(); + server.close(); closeDb(); console.log('šŸ’¾ Database closed'); } catch (err) { - console.error('āŒ Error closing database:', err.message); + console.error('āŒ Error during shutdown:', err.message); } process.exit(0); } process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); + +// Catch unhandled errors to prevent silent crashes +process.on('unhandledRejection', (reason) => { + console.error('āŒ Unhandled rejection:', reason); +}); +process.on('uncaughtException', (err) => { + console.error('āŒ Uncaught exception:', err); + shutdown(); +});