Files
slop-game-opus5.5/app/static/js/systems/ApiClient.js
T
2026-09-24 17:57:54 +02:00

130 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.' };
}
}
}