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);
|
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();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user