Files
NowChess-Frontend/src/app/services/game-api.service.ts
T
2026-04-18 01:03:37 +02:00

161 lines
4.4 KiB
TypeScript

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<GameFull> {
return this.http.post<GameFull>(`${this.apiBase}/api/board/game`, {});
}
getGame(gameId: string): Observable<GameFull> {
return this.http.get<GameFull>(`${this.apiBase}/api/board/game/${gameId}`);
}
makeMove(gameId: string, uci: string): Observable<GameState> {
return this.http.post<GameState>(`${this.apiBase}/api/board/game/${gameId}/move/${uci}`, {});
}
getLegalMoves(gameId: string, square?: string): Observable<LegalMovesResponse> {
let params = new HttpParams();
if (square) {
params = params.set('square', square);
}
return this.http.get<LegalMovesResponse>(`${this.apiBase}/api/board/game/${gameId}/moves`, { params });
}
importFen(fen: string): Observable<GameFull> {
return this.http.post<GameFull>(`${this.apiBase}/api/board/game/import/fen`, { fen });
}
importPgn(pgn: string): Observable<GameFull> {
return this.http.post<GameFull>(`${this.apiBase}/api/board/game/import/pgn`, { pgn });
}
streamGame(gameId: string): Observable<GameStreamEvent> {
return new Observable<GameStreamEvent>((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<void> => {
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();
};
});
}
}