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);
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();
});