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

+30 -17
View File
@@ -54,6 +54,7 @@ export class BotInstance extends EventEmitter {
private logger: Logger;
private connected = false;
private voteSkipUsers = new Set<string>();
private isAdvancing = false;
constructor(options: BotInstanceOptions) {
super();
@@ -142,9 +143,13 @@ export class BotInstance extends EventEmitter {
}
} catch (err) {
this.logger.error({ err, command: parsed.name }, "Command execution error");
await this.tsClient.sendTextMessage(
`Error: ${(err as Error).message}`
);
try {
await this.tsClient.sendTextMessage(
`Error: ${(err as Error).message}`
);
} catch (sendErr) {
this.logger.error({ err: sendErr }, "Failed to send error message to chat");
}
}
}
@@ -421,7 +426,9 @@ export class BotInstance extends EventEmitter {
if (votes >= needed) {
this.voteSkipUsers.clear();
this.playNext();
this.playNext().catch((err) => {
this.logger.error({ err }, "playNext failed after vote skip");
});
return `Vote passed (${votes}/${needed}). Skipping to next song.`;
}
return `Vote to skip: ${votes}/${needed} (need ${needed - votes} more)`;
@@ -472,23 +479,29 @@ export class BotInstance extends EventEmitter {
}
private async playNext(): Promise<void> {
this.voteSkipUsers.clear();
const next = this.queue.next();
if (next) {
const ok = await this.resolveAndPlay(next);
if (!ok) {
// Skip to next if URL resolve fails (up to 3 retries)
for (let i = 0; i < 3; i++) {
const retry = this.queue.next();
if (!retry) break;
if (await this.resolveAndPlay(retry)) return;
if (this.isAdvancing) return;
this.isAdvancing = true;
try {
this.voteSkipUsers.clear();
const next = this.queue.next();
if (next) {
const ok = await this.resolveAndPlay(next);
if (!ok) {
// Skip to next if URL resolve fails (up to 3 retries)
for (let i = 0; i < 3; i++) {
const retry = this.queue.next();
if (!retry) break;
if (await this.resolveAndPlay(retry)) return;
}
this.player.stop();
}
} else {
this.player.stop();
}
} else {
this.player.stop();
this.emit("stateChange");
} finally {
this.isAdvancing = false;
}
this.emit("stateChange");
}
private extractId(input: string): string {