92 lines
4.0 KiB
Markdown
92 lines
4.0 KiB
Markdown
---
|
|
type: guide
|
|
title: Scoreboard Stream
|
|
description: 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.
|
|
tags: [guide, scoreboard, sse, stream, public, authenticated]
|
|
timestamp: 2026-07-23T05:41:00Z
|
|
---
|
|
|
|
# Overview
|
|
|
|
There are **two** scoreboard-related SSE streams in the codebase:
|
|
|
|
1. The **public** stream `GET /api/v1/scoreboard/stream`, owned by
|
|
`SystemController`
|
|
(`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).
|
|
2. The **authenticated** combined stream `GET /api/v1/events`
|
|
(`backend/src/modules/challenges/events.controller.ts`) merges
|
|
`status` ticks, `general` settings pushes, and `solve` frames
|
|
into a single JWT-gated transport. The
|
|
[Scoreboard Page](/guides/scoreboard-page.md) subscribes here
|
|
and reacts to the `solve` event 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)
|
|
|
|
1. The challenge solve path (admin validation, player submit via
|
|
`ChallengesService.submitFlag`) inserts a new `solve` row.
|
|
2. The same code path calls
|
|
`SseHubService.publish('scoreboard', payload)`
|
|
(`backend/src/common/services/sse-hub.service.ts`).
|
|
3. Subscribers to **either** stream receive the payload:
|
|
* `/api/v1/scoreboard/stream` emits a flattened shape
|
|
(`challengeId`, `userId`, `pointsAwarded`, `rankBonus`,
|
|
`solvedAt`) for anonymous spectators.
|
|
* `/api/v1/events` emits a richer `SolveEventPayload` (with both
|
|
snake_case and camelCase aliases, plus `challenge_name`,
|
|
`category_abbreviation`, `position`, `live_points`,
|
|
`solve_count`, etc.) for the authenticated SPA. The display
|
|
metadata fields are sourced from the same `challenge` +
|
|
`category` join that backs the `/api/v1/scoreboard/event-log`
|
|
REST 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`:
|
|
|
|
1. `store.loadAll()` fetches the four projections from
|
|
`/api/v1/scoreboard/{ranking,matrix,event-log,graph}` in
|
|
parallel and populates the initial state.
|
|
2. `store.wireSse(() => authenticatedEventSource.open('/api/v1/events'))`
|
|
opens the authenticated SSE transport via
|
|
`AuthenticatedEventSourceService` (which adds the Bearer token
|
|
and the `'unauthorized'` `401`/`403` channel).
|
|
3. The store listens for the `solve` event type and mutates all
|
|
four tabs (Ranking, Matrix, Event Log, Score Graph) using pure
|
|
helpers in `scoreboard.pure.ts`
|
|
(`parseSolveEventIntoRanking`, `mutateMatrixFromSolve`,
|
|
`applySolveToGraph`, `dedupEventLogBySolveId`).
|
|
4. On transport error the store schedules a reconnect with an
|
|
exponential backoff (1s → 30s cap); the `solve` event type
|
|
resumes mutation as soon as the stream is back.
|
|
5. On `ngOnDestroy`, `store.stop()` aborts the transport and
|
|
clears the reconnect timer.
|
|
|
|
See [Scoreboard Page](/guides/scoreboard-page.md) for the full
|
|
tester-facing contract (tabs, test IDs, empty states, error states)
|
|
and [Challenges Endpoints](/api/challenges.md) for the request and
|
|
response shapes.
|
|
|
|
# See also
|
|
|
|
- [Scoreboard Page](/guides/scoreboard-page.md)
|
|
- [Event Window](/guides/event-window.md)
|
|
- [System Endpoints](/api/system.md)
|
|
- [Challenges Endpoints](/api/challenges.md) |