games: automatic CS2 / PUBG / R6 match history — games.matches, match_players, seasons, snapshots, accounts; in-app poller (Leetify, FACEIT, Steam share codes, PUBG API, Ubisoft) every 20 min plus scripts/games-ingest.ts for backfills; match history, stats, maps and seasons on the per-game pages
All checks were successful
Deploy production / deploy (push) Successful in 1m6s

This commit is contained in:
2026-10-25 16:00:48 +00:00
parent 9114b5433b
commit 80803ef5ac
24 changed files with 30747 additions and 5 deletions

View File

@@ -0,0 +1,39 @@
import { SourceError, sleep } from './types';
const UA = 'stanrunge.dev games tracker (personal, self-hosted)';
/** GET/POST JSON with a timeout, retrying 429/5xx a few times (honouring Retry-After). */
export async function fetchJson<T = unknown>(
url: string,
init: RequestInit & { retries?: number; allow?: number[] } = {}
): Promise<{ status: number; body: T }> {
const { retries = 3, allow = [], ...rest } = init;
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, {
...rest,
headers: { 'User-Agent': UA, Accept: 'application/json', ...(rest.headers ?? {}) },
signal: AbortSignal.timeout(30_000)
});
if ((res.status === 429 || res.status >= 500) && attempt < retries) {
const after = Number(res.headers.get('retry-after'));
await sleep(
Math.min(90, Number.isFinite(after) && after > 0 ? after : 5 * 2 ** attempt) * 1000
);
continue;
}
const text = await res.text();
if (!res.ok && !allow.includes(res.status)) {
throw new SourceError(
`${res.status} from ${new URL(url).host}${new URL(url).pathname}: ${text.slice(0, 160)}`,
res.status
);
}
let body: unknown;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = text;
}
return { status: res.status, body: body as T };
}
}