fix(voice): resolve Redis subscriber corruption, transmitter races, and DB error logging

Redis root cause: VoiceHandler passed redisPub to transmitter.start(),
which called .subscribe() on it — permanently converting the publish
connection to subscriber mode. Every subsequent command reply and status
update failed.

Fixes:
1. Transmitter now creates its own Redis client via new IORedis()
2. Mutual exclusion gate serialises start/stop to prevent null-deref races
3. PassThrough drain listeners cleaned up to stop MaxListenersExceeded
4. FFmpeg SIGTERM flagged as expected exit (no more false level-50 errors)
5. Voice recording repo captures PG error code/detail for diagnostics

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-14 04:23:04 +07:00
co-authored by Claude
parent 32b0b20ca7
commit dbc2879e16
4 changed files with 121 additions and 130 deletions
@@ -81,7 +81,6 @@ export class CommandHandler {
this.voiceHandler = new VoiceHandler( this.voiceHandler = new VoiceHandler(
client, client,
voiceController, voiceController,
this.redisPub,
); );
this.mediaHandler = new MediaHandler(); this.mediaHandler = new MediaHandler();
this.guildHandler = new GuildHandler(client); this.guildHandler = new GuildHandler(client);
@@ -5,7 +5,6 @@ import {
} from "@bete/shared"; } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger"; import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13"; import type { Client } from "discord.js-selfbot-v13";
import type Redis from "ioredis";
import { config } from "../../shared/config/config.js"; import { config } from "../../shared/config/config.js";
import { discordPlayer } from "../voice-recording/player.js"; import { discordPlayer } from "../voice-recording/player.js";
import { voiceTransmitter } from "../voice-recording/transmitter.js"; import { voiceTransmitter } from "../voice-recording/transmitter.js";
@@ -21,7 +20,6 @@ export class VoiceHandler {
constructor( constructor(
private client: Client | null, private client: Client | null,
private voiceController: VoiceController | null, private voiceController: VoiceController | null,
private sharedRedis: Redis | null = null,
) {} ) {}
setClient(client: Client): void { setClient(client: Client): void {
@@ -40,19 +40,13 @@ export async function insertVoiceRecording(
.values(recording) .values(recording)
.onConflictDoNothing(); .onConflictDoNothing();
} catch (error) { } catch (error) {
const detail = const err = error as Record<string, unknown>;
error instanceof Error const detail: Record<string, unknown> = {
? { message: error instanceof Error ? error.message : String(error),
message: error.message, name: error instanceof Error ? error.name : undefined,
name: error.name, };
...((error as Record<string, unknown>).code !== undefined if (err.code !== undefined) detail.code = err.code;
? { code: (error as Record<string, unknown>).code } if (err.detail !== undefined) detail.detail = err.detail;
: {}),
...((error as Record<string, unknown>).detail !== undefined
? { detail: (error as Record<string, unknown>).detail }
: {}),
}
: String(error);
logger.error( logger.error(
{ id: recording.id, error: detail }, { id: recording.id, error: detail },
"Failed to insert voice recording", "Failed to insert voice recording",