import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; import { ErrorEvent, GameFull, GameState, GameStreamEvent, LegalMovesResponse } from '../models/game.models'; @Injectable({ providedIn: 'root' }) export class GameApiService { private readonly apiBase = environment.apiBaseUrl; private readonly wsBase = environment.wsBaseUrl; constructor(private readonly http: HttpClient) {} createGame(): Observable { return this.http.post(`${this.apiBase}/api/board/game`, {}); } getGame(gameId: string): Observable { return this.http.get(`${this.apiBase}/api/board/game/${gameId}`); } makeMove(gameId: string, uci: string): Observable { return this.http.post(`${this.apiBase}/api/board/game/${gameId}/move/${uci}`, {}); } getLegalMoves(gameId: string, square?: string): Observable { let params = new HttpParams(); if (square) { params = params.set('square', square); } return this.http.get(`${this.apiBase}/api/board/game/${gameId}/moves`, { params }); } importFen(fen: string): Observable { return this.http.post(`${this.apiBase}/api/board/game/import/fen`, { fen }); } importPgn(pgn: string): Observable { return this.http.post(`${this.apiBase}/api/board/game/import/pgn`, { pgn }); } streamGame(gameId: string): Observable { return new Observable((observer) => { const wsUrl = `${this.wsBase}/api/board/game/${gameId}/stream`; const streamUrl = `${this.apiBase}/api/board/game/${gameId}/stream`; const ws = new WebSocket(wsUrl); const abortController = new AbortController(); let connected = false; let fallbackActive = false; const parseEvent = (raw: string): GameStreamEvent | null => { if (!raw.trim()) { return null; } try { const parsed = JSON.parse(raw) as GameStreamEvent; return parsed; } catch { return null; } }; const emitErrorEvent = (message: string): void => { const errorEvent: ErrorEvent = { type: 'error', error: { code: 'STREAM_ERROR', message } }; observer.next(errorEvent); }; const startNdjsonFallback = async (): Promise => { if (fallbackActive) { return; } fallbackActive = true; try { const response = await fetch(streamUrl, { headers: { Accept: 'application/x-ndjson' }, signal: abortController.signal }); if (!response.ok || !response.body) { emitErrorEvent(`Unable to open stream: HTTP ${response.status}`); observer.complete(); return; } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { value, done } = await reader.read(); if (done) { break; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? ''; for (const line of lines) { const event = parseEvent(line); if (event) { observer.next(event); } } } observer.complete(); } catch (error) { if ((error as Error).name !== 'AbortError') { emitErrorEvent((error as Error).message); observer.error(error); } } }; ws.onopen = () => { connected = true; }; ws.onmessage = (message) => { const payload = typeof message.data === 'string' ? message.data : ''; const event = parseEvent(payload); if (event) { observer.next(event); } }; ws.onerror = () => { if (!connected) { void startNdjsonFallback(); } }; ws.onclose = () => { if (!connected) { void startNdjsonFallback(); } else { observer.complete(); } }; return () => { abortController.abort(); ws.close(); }; }); } }