Resilient prepare, Twitch chat echo, parallel chat startup

- Prepare endpoint wraps each destination in try/catch; partial success
  if at least one destination is ready (e.g., Twitch works when YouTube
  is rate-limited)
- Echo sent Twitch messages back to app WebSocket (IRC doesn't echo
  your own PRIVMSGs)
- Start YouTube and Twitch chat clients in parallel via Promise.allSettled
- Fix Twitch auth failure detection (Login unsuccessful + Login
  authentication failed)
- Add Twitch IRC debug logging
This commit is contained in:
2026-03-02 09:40:15 +01:00
parent cc8ab2320b
commit 6931670a1f
3 changed files with 107 additions and 56 deletions

View File

@@ -111,13 +111,15 @@ export class ChatManager {
this.sessions.set(sessionKey, session);
const chatPromises: Promise<void>[] = [];
for (const dest of plan.destinations) {
if (dest.serviceId === 'YOUTUBE' && dest.linkedAccountId) {
await this.startYouTubeChat(session, dest);
chatPromises.push(this.startYouTubeChat(session, dest));
} else if (dest.serviceId === 'TWITCH' && dest.linkedAccountId) {
await this.startTwitchChat(session, dest);
chatPromises.push(this.startTwitchChat(session, dest));
}
}
await Promise.allSettled(chatPromises);
}
async handleSendMessage(
@@ -146,9 +148,42 @@ export class ChatManager {
const twitchClient = session.twitchClients.get(destinationId);
if (twitchClient) {
twitchClient.sendMessage(text);
// Echo sent message back to app (Twitch IRC doesn't echo your own PRIVMSGs)
try {
const { account } = await getDecryptedToken(this.prisma, userId, destinationId);
this.sendEcho(session, 'TWITCH', destinationId, account.displayName, text);
} catch {
// Still echo with fallback name
this.sendEcho(session, 'TWITCH', destinationId, 'You', text);
}
}
}
private sendEcho(
session: ChatSession,
service: string,
destinationId: string,
authorName: string,
text: string,
): void {
this.sendToSocket(session.socket, {
type: 'chat_message',
planId: session.planId,
service,
destinationId,
message: {
id: `echo-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`,
authorName,
authorImageUrl: null,
text,
timestamp: Date.now(),
isModerator: false,
isBroadcaster: true,
color: null,
},
});
}
async stopChat(planId: string, userId: string): Promise<void> {
const sessionKey = `${userId}:${planId}`;
const session = this.sessions.get(sessionKey);
@@ -345,6 +380,7 @@ export class ChatManager {
}
private async startTwitchChat(session: ChatSession, dest: any): Promise<void> {
this.logger.info({ planId: session.planId, destId: dest.linkedAccountId }, 'startTwitchChat called');
try {
const { account, accessToken } = await getDecryptedToken(
this.prisma,