import { Injectable, inject } from '@angular/core'; import { Router } from '@angular/router'; import { environment } from '../../environments/environment'; import { ChallengeEventService } from './challenge-event.service'; import { ChallengeService } from './challenge.service'; @Injectable({ providedIn: 'root' }) export class ChallengeWebSocketService { private readonly challengeEventService = inject(ChallengeEventService); private readonly challengeService = inject(ChallengeService); private readonly router = inject(Router); private ws: WebSocket | null = null; private reconnectAttempts = 0; private readonly maxReconnectAttempts = 5; private readonly reconnectDelay = 3000; private intentionalClose = false; connect(): void { if (this.ws) return; const token = localStorage.getItem('token'); if (!token) return; const url = `${environment.userWsBaseUrl}/api/user/ws?token=${encodeURIComponent(token)}`; try { this.intentionalClose = false; this.ws = new WebSocket(url); this.ws.onopen = () => { this.reconnectAttempts = 0; }; this.ws.onmessage = (event) => { this.handleMessage(event.data as string); }; this.ws.onerror = () => { // onclose fires right after, handles reconnect }; this.ws.onclose = () => { this.ws = null; if (!this.intentionalClose) { this.attemptReconnect(); } }; } catch { this.attemptReconnect(); } } disconnect(): void { this.intentionalClose = true; this.reconnectAttempts = 0; if (this.ws) { this.ws.close(); this.ws = null; } } private handleMessage(data: string): void { let message: Record; try { message = JSON.parse(data) as Record; } catch { return; } switch (message['type']) { case 'CONNECTED': break; case 'challengeCreated': { const challengeId = message['challengeId'] as string | undefined; if (challengeId) { this.challengeService.getChallenge(challengeId).subscribe({ next: challenge => this.challengeEventService.onChallengeReceived(challenge), error: () => { /* challenge may have already expired */ } }); } break; } case 'challengeAccepted': { const challengeId = message['challengeId'] as string | undefined; const gameId = message['gameId'] as string | undefined; if (challengeId) { this.challengeEventService.removeChallenge(challengeId); } if (gameId) { void this.router.navigate(['/game', gameId]); } break; } case 'challengeDeclined': case 'challengeExpired': case 'challengeCancelled': { const challengeId = message['challengeId'] as string | undefined; if (challengeId) { this.challengeEventService.removeChallenge(challengeId); } break; } } } private attemptReconnect(): void { if (this.intentionalClose || this.reconnectAttempts >= this.maxReconnectAttempts) return; this.reconnectAttempts++; setTimeout(() => { this.connect(); }, this.reconnectDelay); } }