import { mapCalculateScoresResponse, mapFinishGameResponse, mapHealthResponse, mapJoinSessionResponse, mapMixAnswersResponse, mapScoreboardResponse, mapSessionDetailResponse, mapShowQuestionResponse, mapStartNextRoundResponse, mapStartRoundResponse, mapSubmitGuessResponse, mapSubmitLieResponse } from './mappers'; import type { ApiFailure, ApiResult, CalculateScoresResponse, FinishGameResponse, HealthResponse, JoinSessionRequest, JoinSessionResponse, MixAnswersResponse, ScoreboardResponse, SessionDetailResponse, ShowQuestionResponse, StartNextRoundResponse, StartRoundRequest, StartRoundResponse, SubmitGuessRequest, SubmitGuessResponse, SubmitLieRequest, SubmitLieResponse } from './types'; export interface AngularHttpError { status?: number; message?: string; error?: unknown; } export interface AngularHttpClientLike { get(url: string, options?: { withCredentials?: boolean }): Promise; post(url: string, body: unknown, options?: { withCredentials?: boolean }): Promise; } export interface AngularApiClient { health(): Promise>; getSession(code: string): Promise>; joinSession(payload: JoinSessionRequest): Promise>; startRound(code: string, payload: StartRoundRequest): Promise>; showQuestion(code: string): Promise>; mixAnswers(code: string, roundQuestionId: number): Promise>; calculateScores(code: string, roundQuestionId: number): Promise>; getScoreboard(code: string): Promise>; startNextRound(code: string): Promise>; finishGame(code: string): Promise>; submitLie(code: string, roundQuestionId: number, payload: SubmitLieRequest): Promise>; submitGuess( code: string, roundQuestionId: number, payload: SubmitGuessRequest ): Promise>; } function toFailure(error: unknown): ApiFailure { const candidate = (error ?? {}) as AngularHttpError; const status = typeof candidate.status === 'number' ? candidate.status : 0; const payload = candidate.error; if (status === 0) { return { kind: 'network', status: 0, message: candidate.message ?? 'Network error while contacting API' }; } return { kind: 'http', status, message: candidate.message ?? `HTTP ${status}`, ...(payload === undefined ? {} : { payload }) }; } function normalizeCode(code: string): string { return code.trim().toUpperCase(); } function normalizeBaseUrl(baseUrl: string): string { return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; } function buildUrl(baseUrl: string, path: string): string { return `${normalizeBaseUrl(baseUrl)}${path}`; } async function wrap(call: () => Promise, mapper: (payload: unknown) => T): Promise> { let payload: unknown; try { payload = await call(); } catch (error: unknown) { return { ok: false, status: typeof (error as AngularHttpError)?.status === 'number' ? (error as AngularHttpError).status! : 0, error: toFailure(error) }; } try { return { ok: true, status: 200, data: mapper(payload) }; } catch (error: unknown) { return { ok: false, status: 200, error: { kind: 'parse', status: 200, message: error instanceof Error ? error.message : 'Invalid API response contract', payload } }; } } export function createAngularApiClient(http: AngularHttpClientLike, baseUrl = ''): AngularApiClient { return { health: () => wrap(() => http.get(buildUrl(baseUrl, '/healthz'), { withCredentials: true }), mapHealthResponse), getSession: (code: string) => wrap( () => http.get(buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}`), { withCredentials: true }), mapSessionDetailResponse ), joinSession: (payload: JoinSessionRequest) => wrap( () => http.post( buildUrl(baseUrl, '/lobby/sessions/join'), { code: normalizeCode(payload.code), nickname: payload.nickname.trim() }, { withCredentials: true } ), mapJoinSessionResponse ), startRound: (code: string, payload: StartRoundRequest) => wrap( () => http.post( buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/rounds/start`), payload, { withCredentials: true } ), mapStartRoundResponse ), showQuestion: (code: string) => wrap( () => http.post( buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/show`), {}, { withCredentials: true } ), mapShowQuestionResponse ), mixAnswers: (code: string, roundQuestionId: number) => wrap( () => http.post( buildUrl( baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/answers/mix` ), {}, { withCredentials: true } ), mapMixAnswersResponse ), calculateScores: (code: string, roundQuestionId: number) => wrap( () => http.post( buildUrl( baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/scores/calculate` ), {}, { withCredentials: true } ), mapCalculateScoresResponse ), getScoreboard: (code: string) => wrap( () => http.get( buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/scoreboard`), { withCredentials: true } ), mapScoreboardResponse ), startNextRound: (code: string) => wrap( () => http.post( buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/rounds/next`), {}, { withCredentials: true } ), mapStartNextRoundResponse ), finishGame: (code: string) => wrap( () => http.post( buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/finish`), {}, { withCredentials: true } ), mapFinishGameResponse ), submitLie: (code: string, roundQuestionId: number, payload: SubmitLieRequest) => wrap( () => http.post( buildUrl( baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/lies/submit` ), { player_id: payload.player_id, session_token: payload.session_token, text: payload.text }, { withCredentials: true } ), mapSubmitLieResponse ), submitGuess: (code: string, roundQuestionId: number, payload: SubmitGuessRequest) => wrap( () => http.post( buildUrl( baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/guesses/submit` ), { player_id: payload.player_id, session_token: payload.session_token, selected_text: payload.selected_text }, { withCredentials: true } ), mapSubmitGuessResponse ) }; }