130 lines
3.9 KiB
JavaScript
130 lines
3.9 KiB
JavaScript
/**
|
||
* Zugriff auf die JSON-API des Flask-Backends (ohne Phaser).
|
||
*
|
||
* Fehler werden nie geworfen, sondern als Ergebnisobjekt zurückgegeben: Ein
|
||
* ausgefallener Highscore-Server darf das Spiel nicht unterbrechen.
|
||
* @module systems/ApiClient
|
||
*/
|
||
|
||
import { HIGHSCORE } from '../config.js';
|
||
|
||
/**
|
||
* @typedef {Object} HighscoreEntry
|
||
* @property {number} rank
|
||
* @property {string} name
|
||
* @property {number} score
|
||
* @property {number} wave
|
||
* @property {string} created_at
|
||
*/
|
||
|
||
/**
|
||
* @typedef {Object} SubmitResult
|
||
* @property {boolean} ok
|
||
* @property {number} [rank]
|
||
* @property {HighscoreEntry[]} [highscores]
|
||
* @property {string} [error] Fehlercode der API oder „network“.
|
||
* @property {string} [message] Lesbare Meldung.
|
||
* @property {Record<string, string>} [fields] Feldfehler bei Validierung.
|
||
*/
|
||
|
||
export class ApiClient {
|
||
/**
|
||
* @param {{highscores: string, runs: string}} endpoints
|
||
* @param {typeof fetch} [fetchImpl] Für Tests austauschbar.
|
||
* @param {number} [timeoutMs]
|
||
*/
|
||
constructor(endpoints, fetchImpl = globalThis.fetch?.bind(globalThis), timeoutMs = HIGHSCORE.requestTimeoutMs) {
|
||
this.endpoints = endpoints;
|
||
this.fetchImpl = fetchImpl;
|
||
this.timeoutMs = timeoutMs;
|
||
}
|
||
|
||
/**
|
||
* @param {string} url
|
||
* @param {RequestInit} [init]
|
||
* @returns {Promise<{status: number, body: any}>}
|
||
*/
|
||
async request(url, init = {}) {
|
||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||
const timer = controller ? setTimeout(() => controller.abort(), this.timeoutMs) : null;
|
||
try {
|
||
const response = await this.fetchImpl(url, {
|
||
...init,
|
||
signal: controller?.signal,
|
||
credentials: 'same-origin',
|
||
headers: { Accept: 'application/json', ...(init.headers ?? {}) },
|
||
});
|
||
let body = null;
|
||
try {
|
||
body = await response.json();
|
||
} catch {
|
||
body = null;
|
||
}
|
||
return { status: response.status, body };
|
||
} finally {
|
||
if (timer) clearTimeout(timer);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Meldet eine neue Spielrunde an.
|
||
* @returns {Promise<string|null>} Run-Token oder null, wenn der Server nicht erreichbar ist.
|
||
*/
|
||
async startRun() {
|
||
try {
|
||
const { status, body } = await this.request(this.endpoints.runs, { method: 'POST' });
|
||
return status === 201 && typeof body?.run_token === 'string' ? body.run_token : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @returns {Promise<{ok: boolean, highscores: HighscoreEntry[]}>}
|
||
*/
|
||
async fetchHighscores() {
|
||
try {
|
||
const { status, body } = await this.request(this.endpoints.highscores);
|
||
if (status === 200 && Array.isArray(body?.highscores)) {
|
||
return { ok: true, highscores: body.highscores };
|
||
}
|
||
} catch {
|
||
// Netzwerkfehler oder Timeout – unten als Fehlschlag gemeldet.
|
||
}
|
||
return { ok: false, highscores: [] };
|
||
}
|
||
|
||
/**
|
||
* Reicht einen Highscore ein.
|
||
* @param {{name: string, score: number, wave: number, durationS: number, runToken: string}} entry
|
||
* @returns {Promise<SubmitResult>}
|
||
*/
|
||
async submitHighscore(entry) {
|
||
const payload = {
|
||
name: entry.name,
|
||
score: Math.max(0, Math.floor(entry.score)),
|
||
wave: Math.max(1, Math.floor(entry.wave)),
|
||
duration_s: Math.max(0, Math.floor(entry.durationS)),
|
||
run_token: entry.runToken,
|
||
};
|
||
try {
|
||
const { status, body } = await this.request(this.endpoints.highscores, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (status === 201) {
|
||
return { ok: true, rank: body?.rank, highscores: body?.highscores ?? [] };
|
||
}
|
||
return {
|
||
ok: false,
|
||
error: body?.error ?? `http_${status}`,
|
||
message: body?.message ?? 'Unbekannter Fehler.',
|
||
fields: body?.fields,
|
||
};
|
||
} catch {
|
||
return { ok: false, error: 'network', message: 'Keine Verbindung zum Server.' };
|
||
}
|
||
}
|
||
}
|