1d2c217da8
Remove token from WebSocket URL query parameters in ChallengeWebSocketService
and GameApiService. Instead, send {"type":"auth","token":"..."} as the first
text frame after the connection opens, matching the new backend auth protocol.
---------
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #13
121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
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`;
|
|
|
|
try {
|
|
this.intentionalClose = false;
|
|
this.ws = new WebSocket(url);
|
|
|
|
this.ws.onopen = () => {
|
|
this.reconnectAttempts = 0;
|
|
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<string, unknown>;
|
|
try {
|
|
message = JSON.parse(data) as Record<string, unknown>;
|
|
} 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);
|
|
}
|
|
}
|