fix: resolve critical race conditions and high-severity bugs

- Add isAdvancing guard to playNext() to prevent concurrent calls from
  skipping songs (trackEnd + user !next race condition)
- Replace removeAllListeners in WebSocket setup with tracked named
  listeners, attach only once per bot instead of every 5 seconds
- Add .catch() to unhandled playNext() in cmdVote
- Add sessionId counter to AudioPlayer to discard stale setTimeout
  callbacks after stop()+play() transitions
- Defer nulling TS3 client until disconnect() promise resolves
- Add null checks for providers in play-by-id and add-by-id endpoints
- Add PCM buffer backpressure (pause FFmpeg at 960KB, resume at 480KB)
- Change volume slider from @input to @change to avoid flooding server
- Wrap error-reporting sendTextMessage in try/catch to prevent
  unhandled rejection in error handler
- Clamp elapsed getter to song duration
- Log FFmpeg stderr at debug level instead of swallowing
- Return null from prev() in Sequential mode instead of wrapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 14:31:03 +08:00
1 parent 4c71a0a37f
commit 17991b936a
8 files changed
+150 -57

No files matched your search

+60 -28
View File
@@ -1,5 +1,6 @@
import { WebSocketServer, WebSocket } from "ws";
import type { BotManager } from "../bot/manager.js";
import type { BotInstance } from "../bot/instance.js";
import type { Logger } from "../logger.js";
export function setupWebSocket(
@@ -9,6 +10,13 @@ export function setupWebSocket(
): () => void {
const clients = new Set<WebSocket>();
/** Track which bots already have listeners attached */
const attachedBots = new Map<string, {
stateChange: () => void;
connected: () => void;
disconnected: () => void;
}>();
wss.on("connection", (ws) => {
clients.add(ws);
logger.debug("WebSocket client connected");
@@ -36,39 +44,63 @@ export function setupWebSocket(
}
};
const attachBotListeners = () => {
function attachBotListener(bot: BotInstance): void {
if (attachedBots.has(bot.id)) return;
const onStateChange = () => {
broadcast({
type: "stateChange",
botId: bot.id,
status: bot.getStatus(),
queue: bot.getQueue(),
});
};
const onConnected = () => {
broadcast({
type: "botConnected",
botId: bot.id,
status: bot.getStatus(),
});
};
const onDisconnected = () => {
broadcast({ type: "botDisconnected", botId: bot.id });
};
bot.on("stateChange", onStateChange);
bot.on("connected", onConnected);
bot.on("disconnected", onDisconnected);
attachedBots.set(bot.id, {
stateChange: onStateChange,
connected: onConnected,
disconnected: onDisconnected,
});
}
/** Attach listeners for any new bots that don't have them yet */
function ensureAllBotsAttached(): void {
for (const bot of botManager.getAllBots()) {
bot.removeAllListeners("stateChange");
bot.removeAllListeners("connected");
bot.removeAllListeners("disconnected");
bot.on("stateChange", () => {
broadcast({
type: "stateChange",
botId: bot.id,
status: bot.getStatus(),
queue: bot.getQueue(),
});
});
bot.on("connected", () => {
broadcast({
type: "botConnected",
botId: bot.id,
status: bot.getStatus(),
});
});
bot.on("disconnected", () => {
broadcast({ type: "botDisconnected", botId: bot.id });
});
attachBotListener(bot);
}
};
}
const intervalId = setInterval(attachBotListeners, 5000);
attachBotListeners();
// Check for newly added bots periodically
const intervalId = setInterval(ensureAllBotsAttached, 5000);
ensureAllBotsAttached();
return () => {
clearInterval(intervalId);
// Clean up named listeners
for (const bot of botManager.getAllBots()) {
const listeners = attachedBots.get(bot.id);
if (listeners) {
bot.removeListener("stateChange", listeners.stateChange);
bot.removeListener("connected", listeners.connected);
bot.removeListener("disconnected", listeners.disconnected);
}
}
attachedBots.clear();
};
}