diff --git a/src/app/services/challenge-websocket.service.ts b/src/app/services/challenge-websocket.service.ts index 1a03907..2e4dcdb 100644 --- a/src/app/services/challenge-websocket.service.ts +++ b/src/app/services/challenge-websocket.service.ts @@ -6,110 +6,115 @@ 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 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; + 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; + connect(): void { + if (this.ws) return; - const token = localStorage.getItem('token'); - if (!token) return; + const token = localStorage.getItem('token'); + if (!token) return; - const url = `${environment.userWsBaseUrl}/api/user/ws?token=${encodeURIComponent(token)}`; + const url = `${environment.userWsBaseUrl}/api/user/ws`; - try { - this.intentionalClose = false; - this.ws = new WebSocket(url); + 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.ws.onopen = () => { this.reconnectAttempts = 0; - if (this.ws) { - this.ws.close(); - this.ws = null; + this.ws?.send(JSON.stringify({ type: 'auth', token })); + }; + + 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; } - 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; + } - 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; - } + 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; + } - private attemptReconnect(): void { - if (this.intentionalClose || this.reconnectAttempts >= this.maxReconnectAttempts) return; - this.reconnectAttempts++; - setTimeout(() => { this.connect(); }, this.reconnectDelay); + 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); + } } diff --git a/src/app/services/game-api.service.ts b/src/app/services/game-api.service.ts index 86fd951..4168c01 100644 --- a/src/app/services/game-api.service.ts +++ b/src/app/services/game-api.service.ts @@ -7,7 +7,7 @@ import { GameState, GameStreamEvent, LegalMovesResponse, - PlayerInfo + PlayerInfo, } from '../models/game.models'; import { AnalysisRequest, AnalysisResponse } from '../models/analysis.models'; import { StreamHandlerService } from './stream-handler.service'; @@ -29,11 +29,11 @@ export class GameApiService { const playerColor = Math.random() > 0.5 ? 'white' : 'black'; const playerInfo: PlayerInfo = { id: `player-${Date.now()}`, - displayName: 'You' + displayName: 'You', }; const botInfo: PlayerInfo = { id: `bot-${difficulty}`, - displayName: `Bot (${difficulty})` + displayName: `Bot (${difficulty})`, }; const payload = @@ -57,7 +57,9 @@ export class GameApiService { if (square) { params = params.set('square', square); } - return this.http.get(`${this.apiBase}${this.apiPath}/${gameId}/moves`, { params }); + return this.http.get(`${this.apiBase}${this.apiPath}/${gameId}/moves`, { + params, + }); } importFen(fen: string): Observable { @@ -90,11 +92,8 @@ export class GameApiService { } streamGame(gameId: string): Observable { - const token = localStorage.getItem('token'); - let wsUrl = `${this.resolveWsBase()}${this.apiPath}/${gameId}/ws`; - if (token) { - wsUrl += `?token=${encodeURIComponent(token)}`; - } - return this.streamHandler.createGameStream(wsUrl, gameId); + const wsUrl = `${this.resolveWsBase()}${this.apiPath}/${gameId}/ws`; + const token = localStorage.getItem('token') ?? ''; + return this.streamHandler.createGameStream(wsUrl, gameId, token); } } diff --git a/src/app/services/stream-handler.service.ts b/src/app/services/stream-handler.service.ts index 520a7fe..264a5be 100644 --- a/src/app/services/stream-handler.service.ts +++ b/src/app/services/stream-handler.service.ts @@ -6,7 +6,7 @@ const WS_CONNECT_TIMEOUT_MS = 3000; @Injectable({ providedIn: 'root' }) export class StreamHandlerService { - createGameStream(wsUrl: string, gameId: string): Observable { + createGameStream(wsUrl: string, gameId: string, token: string): Observable { return new Observable((observer) => { const ws = new WebSocket(wsUrl); let connected = false; @@ -14,7 +14,7 @@ export class StreamHandlerService { const emitErrorEvent = (message: string): void => { const errorEvent: ErrorEvent = { type: 'error', - error: { code: 'STREAM_ERROR', message } + error: { code: 'STREAM_ERROR', message }, }; observer.next(errorEvent); }; @@ -36,6 +36,7 @@ export class StreamHandlerService { connected = true; clearTimeout(connectionTimeoutId); console.log(`[StreamHandler] WebSocket connected for ${gameId}`); + ws.send(JSON.stringify({ type: 'auth', token })); }; ws.onmessage = (message) => {