v2 Phase 1: ingestion + SQLite foundation
- Persistence via Node's built-in node:sqlite (zero native deps) on a Docker volume: response cache, source health, fixture↔provider id map, per-fixture enrichment, ingest log. Runtime bumped to node:24-slim + --experimental-sqlite. - Resilient fetcher: DB cache + per-source rate-limit + jittered backoff + circuit-breaker (blocked sources trip + skip; UI reads DB, never breaks). - ESPN hidden API as the PRIMARY rich source (works from the VPS where SofaScore 403s): scoreboard (live scores w/ clock) + summary (venue, H2H, recent form, lineups, team stats). football-data / SofaScore are fallbacks. - Scheduler: maps all 72 group fixtures to ESPN event ids, polls live on a dynamic cadence, enriches imminent fixtures into match_ext. /api/sources/health. - Verified: DB populates (real H2H + form for opening matches), 22 tests pass, Docker image runs node:sqlite on the volume and persists across restart. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+7
-4
@@ -6,8 +6,9 @@ import fastifyWebsocket from '@fastify/websocket';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import type { WebSocket } from 'ws';
|
||||
import { TournamentState } from './tournament';
|
||||
import { startLiveLoop } from './liveLoop';
|
||||
import { startScheduler } from './ingest/scheduler';
|
||||
import { ModelEngine } from './model';
|
||||
import { db, healthAll } from './db/db';
|
||||
import type { MatchStatus, ServerMessage } from '../../src/lib/types';
|
||||
|
||||
const PORT = Number(process.env.PORT ?? 8787);
|
||||
@@ -27,6 +28,7 @@ export function buildServer() {
|
||||
catch { const err = new Error('Invalid JSON') as Error & { statusCode?: number }; err.statusCode = 400; done(err, undefined); }
|
||||
});
|
||||
|
||||
db(); // open + migrate SQLite before anything reads it
|
||||
const state = new TournamentState(STATIC_DIR);
|
||||
const model = new ModelEngine(STATIC_DIR);
|
||||
model.recompute(state.allFixtures());
|
||||
@@ -52,6 +54,7 @@ export function buildServer() {
|
||||
// ---- REST: snapshot + predictions (initial load + a no-WS fallback) ----
|
||||
app.get('/api/snapshot', async () => state.snapshot());
|
||||
app.get('/api/predictions', async () => model.current());
|
||||
app.get('/api/sources/health', async () => ({ sources: healthAll() }));
|
||||
app.get('/healthz', async () => ({ ok: true }));
|
||||
|
||||
// ---- dev-only: inject a score to exercise the live → standings → WS loop ----
|
||||
@@ -102,9 +105,9 @@ export function buildServer() {
|
||||
});
|
||||
});
|
||||
|
||||
// ---- live polling loop (no-op unless a source is configured) ----
|
||||
const stopLive = startLiveLoop(state, () => broadcast());
|
||||
app.addHook('onClose', async () => stopLive());
|
||||
// ---- ingestion scheduler (ESPN primary; football-data / SofaScore fallback) ----
|
||||
const stopIngest = startScheduler(state, () => broadcast());
|
||||
app.addHook('onClose', async () => stopIngest());
|
||||
|
||||
// ---- static SPA (prod) with history fallback ----
|
||||
if (existsSync(STATIC_DIR)) {
|
||||
|
||||
Reference in New Issue
Block a user