161 lines
4.4 KiB
TypeScript
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();
|
|
};
|
|
});
|
|
}
|
|
}
|