Enhance WebSocket error handling and implement command idempotency for dropper nickname and recipe management endpoints
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user