Enhance WebSocket error handling and implement command idempotency for dropper nickname and recipe management endpoints

This commit is contained in:
MayaTheShy
2026-03-22 02:38:30 -04:00
parent ea75c1eabc
commit 460cf34252

View File

@@ -107,7 +107,11 @@ function broadcastToClients(data) {
const message = JSON.stringify(data); const message = JSON.stringify(data);
webClients.forEach((client) => { webClients.forEach((client) => {
if (client.readyState === 1) { 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; let sent = false;
for (const bridge of bridgeClients) { for (const bridge of bridgeClients) {
if (bridge.readyState === 1) { if (bridge.readyState === 1) {
bridge.send(JSON.stringify(command)); try {
sent = true; bridge.send(JSON.stringify(command));
sent = true;
} catch (err) {
console.error('❌ WS send error (bridge):', err.message);
}
} }
} }
if (!sent) { if (!sent) {
@@ -352,7 +360,11 @@ app.get('/api/dropper-nicknames', (req, res) => {
// Set a single dropper nickname // Set a single dropper nickname
app.post('/api/dropper-nicknames', (req, res) => { app.post('/api/dropper-nicknames', (req, res) => {
try { 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 (!dropperName) return res.status(400).json({ error: 'Missing dropperName' });
if (nickname && nickname.trim()) { if (nickname && nickname.trim()) {
@@ -363,7 +375,9 @@ app.post('/api/dropper-nicknames', (req, res) => {
saveState('dropperNicknames', dropperNicknames); saveState('dropperNicknames', dropperNicknames);
console.log(`🏷️ Dropper nickname: ${dropperName}${nickname || '(removed)'}`); 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) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
@@ -427,25 +441,33 @@ app.post('/api/recipes/toggle', (req, res) => {
// Enable/disable all recipes // Enable/disable all recipes
app.post('/api/recipes/enable-all', (req, res) => { app.post('/api/recipes/enable-all', (req, res) => {
const { commandId } = req.body || {}; try {
const cached = checkIdempotent(commandId); const { commandId } = req.body || {};
if (cached) return res.json(cached); const cached = checkIdempotent(commandId);
if (cached) return res.json(cached);
pushCommandToBridge({ type: 'command', action: 'enable_all', commandId }); pushCommandToBridge({ type: 'command', action: 'enable_all', commandId });
const result = { success: true, commandId }; const result = { success: true, commandId };
recordCommand(commandId, result); recordCommand(commandId, result);
res.json(result); res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}); });
app.post('/api/recipes/disable-all', (req, res) => { app.post('/api/recipes/disable-all', (req, res) => {
const { commandId } = req.body || {}; try {
const cached = checkIdempotent(commandId); const { commandId } = req.body || {};
if (cached) return res.json(cached); const cached = checkIdempotent(commandId);
if (cached) return res.json(cached);
pushCommandToBridge({ type: 'command', action: 'disable_all', commandId }); pushCommandToBridge({ type: 'command', action: 'disable_all', commandId });
const result = { success: true, commandId }; const result = { success: true, commandId };
recordCommand(commandId, result); recordCommand(commandId, result);
res.json(result); res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
}); });
// Sort barrel // Sort barrel
@@ -662,6 +684,7 @@ function updateStateFromBridge(data) {
smeltable: smeltableRecipes, smeltable: smeltableRecipes,
craftable: craftableRecipes, craftable: craftableRecipes,
craftTurtleOk, craftTurtleOk,
dropperNicknames,
lastUpdate, lastUpdate,
bridgeConnected: bridgeClients.size > 0, bridgeConnected: bridgeClients.size > 0,
}); });
@@ -865,13 +888,24 @@ server.listen(PORT, HOST, () => {
function shutdown() { function shutdown() {
console.log('\n🛑 Shutting down server...'); console.log('\n🛑 Shutting down server...');
try { try {
wss.close();
server.close();
closeDb(); closeDb();
console.log('💾 Database closed'); console.log('💾 Database closed');
} catch (err) { } catch (err) {
console.error('❌ Error closing database:', err.message); console.error('❌ Error during shutdown:', err.message);
} }
process.exit(0); process.exit(0);
} }
process.on('SIGINT', shutdown); process.on('SIGINT', shutdown);
process.on('SIGTERM', 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();
});