feat(spa): add typed API response mappers and contract guards
CI / test-and-quality (push) Successful in 2m29s
CI / test-and-quality (pull_request) Successful in 2m20s

This commit is contained in:
2026-03-01 15:32:26 +00:00
parent 634bd617e7
commit de5007943e
5 changed files with 319 additions and 31 deletions
+51 -23
View File
@@ -1,3 +1,9 @@
import {
mapHealthResponse,
mapJoinSessionResponse,
mapSessionDetailResponse,
mapStartRoundResponse
} from './mappers';
import type {
ApiFailure,
ApiResult,
@@ -60,10 +66,10 @@ function buildUrl(baseUrl: string, path: string): string {
return `${normalizeBaseUrl(baseUrl)}${path}`;
}
async function wrap<T>(call: () => Promise<T>): Promise<ApiResult<T>> {
async function wrap<T>(call: () => Promise<unknown>, mapper: (payload: unknown) => T): Promise<ApiResult<T>> {
let payload: unknown;
try {
const data = await call();
return { ok: true, status: 200, data };
payload = await call();
} catch (error: unknown) {
return {
ok: false,
@@ -71,35 +77,57 @@ async function wrap<T>(call: () => Promise<T>): Promise<ApiResult<T>> {
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<HealthResponse>(buildUrl(baseUrl, '/healthz'), { withCredentials: true })),
health: () =>
wrap(() => http.get<HealthResponse>(buildUrl(baseUrl, '/healthz'), { withCredentials: true }), mapHealthResponse),
getSession: (code: string) =>
wrap(() =>
http.get<SessionDetailResponse>(buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}`), {
withCredentials: true
})
wrap(
() =>
http.get<SessionDetailResponse>(buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}`), {
withCredentials: true
}),
mapSessionDetailResponse
),
joinSession: (payload: JoinSessionRequest) =>
wrap(() =>
http.post<JoinSessionResponse>(
buildUrl(baseUrl, '/lobby/sessions/join'),
{
code: normalizeCode(payload.code),
nickname: payload.nickname.trim()
},
{ withCredentials: true }
)
wrap(
() =>
http.post<JoinSessionResponse>(
buildUrl(baseUrl, '/lobby/sessions/join'),
{
code: normalizeCode(payload.code),
nickname: payload.nickname.trim()
},
{ withCredentials: true }
),
mapJoinSessionResponse
),
startRound: (code: string, payload: StartRoundRequest) =>
wrap(() =>
http.post<StartRoundResponse>(
buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/rounds/start`),
payload,
{ withCredentials: true }
)
wrap(
() =>
http.post<StartRoundResponse>(
buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/rounds/start`),
payload,
{ withCredentials: true }
),
mapStartRoundResponse
)
};
}