4.0 KiB
4.0 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| guide | Scoreboard Stream | How fresh solves are broadcast to clients — both the public unauthenticated scoreboard SSE stream and the authenticated combined /api/v1/events stream that the /scoreboard page consumes. |
|
2026-07-23T05:41:00Z |
Overview
There are two scoreboard-related SSE streams in the codebase:
- The public stream
GET /api/v1/scoreboard/stream, owned bySystemController(backend/src/modules/system/system.controller.ts), is the@Public()@Sse('scoreboard/stream')handler that pushes a flattened solve payload every time the scoreboard hub publishes a new solve. It is meant for unauthenticated spectators (e.g. landing-page leaderboard widgets). - The authenticated combined stream
GET /api/v1/events(backend/src/modules/challenges/events.controller.ts) mergesstatusticks,generalsettings pushes, andsolveframes into a single JWT-gated transport. The Scoreboard Page subscribes here and reacts to thesolveevent type to mutate all four tabs in real time.
Public endpoint — GET /api/v1/scoreboard/stream
The handler
(backend/src/modules/system/system.controller.ts) opens an SSE
stream that pushes a flattened solve payload every time a new solve
is published to the scoreboard hub.
Push path (both streams)
- The challenge solve path (admin validation, player submit via
ChallengesService.submitFlag) inserts a newsolverow. - The same code path calls
SseHubService.publish('scoreboard', payload)(backend/src/common/services/sse-hub.service.ts). - Subscribers to either stream receive the payload:
/api/v1/scoreboard/streamemits a flattened shape (challengeId,userId,pointsAwarded,rankBonus,solvedAt) for anonymous spectators./api/v1/eventsemits a richerSolveEventPayload(with both snake_case and camelCase aliases, pluschallenge_name,category_abbreviation,position,live_points,solve_count, etc.) for the authenticated SPA. The display metadata fields are sourced from the samechallenge+categoryjoin that backs the/api/v1/scoreboard/event-logREST projection, so the Event Log tab can render an SSE-pushed row with the challenge name and category abbreviation without a second lookup.
The hub is in-process; multi-replica deployments need a shared pub/sub to fan out across pods (not in scope for this repo).
Authenticated consumer — /scoreboard page
The ScoreboardPage smart component subscribes to /api/v1/events
(not the public stream) on ngOnInit:
store.loadAll()fetches the four projections from/api/v1/scoreboard/{ranking,matrix,event-log,graph}in parallel and populates the initial state.store.wireSse(() => authenticatedEventSource.open('/api/v1/events'))opens the authenticated SSE transport viaAuthenticatedEventSourceService(which adds the Bearer token and the'unauthorized'401/403channel).- The store listens for the
solveevent type and mutates all four tabs (Ranking, Matrix, Event Log, Score Graph) using pure helpers inscoreboard.pure.ts(parseSolveEventIntoRanking,mutateMatrixFromSolve,applySolveToGraph,dedupEventLogBySolveId). - On transport error the store schedules a reconnect with an
exponential backoff (1s → 30s cap); the
solveevent type resumes mutation as soon as the stream is back. - On
ngOnDestroy,store.stop()aborts the transport and clears the reconnect timer.
See Scoreboard Page for the full tester-facing contract (tabs, test IDs, empty states, error states) and Challenges Endpoints for the request and response shapes.