mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
Speed up championship hub loading
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/jpeg" href="/favicon.jpg" />
|
||||||
<title>box-box</title>
|
<title>box-box</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
frontend/public/favicon.jpg
Normal file
BIN
frontend/public/favicon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 392 KiB |
@@ -62,7 +62,9 @@ export async function fetchWeekend(meetingKey: number): Promise<Weekend> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchChampionshipHub(year?: number): Promise<ChampionshipHub> {
|
export async function fetchChampionshipHub(year?: number): Promise<ChampionshipHub> {
|
||||||
const url = year ? `/api/v1/championship/hub?year=${year}` : '/api/v1/championship/hub'
|
const params = new URLSearchParams({ source: 'auto' })
|
||||||
|
if (year) params.set('year', year.toString())
|
||||||
|
const url = `/api/v1/championship/hub?${params.toString()}`
|
||||||
const res = await fetch(url)
|
const res = await fetch(url)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`API ${res.status}: ${res.statusText}`)
|
throw new Error(`API ${res.status}: ${res.statusText}`)
|
||||||
|
|||||||
177
internal/query/championship.go
Normal file
177
internal/query/championship.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AmanTahiliani/box-box/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChampionshipRace is the local data needed by the web championship hub for one GP.
|
||||||
|
type ChampionshipRace struct {
|
||||||
|
Meeting models.Meeting
|
||||||
|
RaceSessionKey int
|
||||||
|
Results []models.SessionResult
|
||||||
|
Grid []models.StartingGrid
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChampionshipInputs are local-first inputs for the championship hub aggregation.
|
||||||
|
type ChampionshipInputs struct {
|
||||||
|
Races []ChampionshipRace
|
||||||
|
Champ []models.ChampionshipDriver
|
||||||
|
Teams []models.ChampionshipTeam
|
||||||
|
DriverMap map[int]models.Driver
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChampionshipInputs derives championship hub inputs from ingested season data.
|
||||||
|
func (s *Service) GetChampionshipInputs(year int) (ChampionshipInputs, error) {
|
||||||
|
meetings, err := s.ListMeetingsByYear(year)
|
||||||
|
if err != nil {
|
||||||
|
return ChampionshipInputs{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := ChampionshipInputs{
|
||||||
|
Races: []ChampionshipRace{},
|
||||||
|
Champ: []models.ChampionshipDriver{},
|
||||||
|
Teams: []models.ChampionshipTeam{},
|
||||||
|
DriverMap: map[int]models.Driver{},
|
||||||
|
}
|
||||||
|
|
||||||
|
pointsByDriver := map[int]float64{}
|
||||||
|
pointsByTeam := map[string]float64{}
|
||||||
|
latestSessionKey := 0
|
||||||
|
latestMeetingKey := 0
|
||||||
|
|
||||||
|
for _, meeting := range meetings {
|
||||||
|
sessions, err := s.ListSessionsByMeeting(int(meeting.MeetingKey))
|
||||||
|
if err != nil {
|
||||||
|
return ChampionshipInputs{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
raceKey := 0
|
||||||
|
for _, sess := range sessions {
|
||||||
|
if strings.EqualFold(sess.SessionName, "Race") {
|
||||||
|
raceKey = sess.SessionKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if raceKey == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.store.ListSessionResults(raceKey)
|
||||||
|
if err != nil {
|
||||||
|
return ChampionshipInputs{}, err
|
||||||
|
}
|
||||||
|
grid, err := s.store.ListStartingGrid(raceKey)
|
||||||
|
if err != nil {
|
||||||
|
return ChampionshipInputs{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
race := ChampionshipRace{
|
||||||
|
Meeting: meeting,
|
||||||
|
RaceSessionKey: raceKey,
|
||||||
|
Results: make([]models.SessionResult, 0, len(results)),
|
||||||
|
Grid: make([]models.StartingGrid, 0, len(grid)),
|
||||||
|
}
|
||||||
|
for _, result := range results {
|
||||||
|
race.Results = append(race.Results, resultToModel(result))
|
||||||
|
}
|
||||||
|
for _, entry := range grid {
|
||||||
|
race.Grid = append(race.Grid, gridToModel(entry))
|
||||||
|
}
|
||||||
|
inputs.Races = append(inputs.Races, race)
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
latestSessionKey = raceKey
|
||||||
|
latestMeetingKey = int(meeting.MeetingKey)
|
||||||
|
drivers, err := s.driversForSession(raceKey)
|
||||||
|
if err != nil {
|
||||||
|
return ChampionshipInputs{}, err
|
||||||
|
}
|
||||||
|
for _, d := range drivers {
|
||||||
|
inputs.DriverMap[d.DriverNumber] = d
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
pointsByDriver[result.DriverNumber] += result.Points
|
||||||
|
team := inputs.DriverMap[result.DriverNumber].TeamName
|
||||||
|
if team != "" {
|
||||||
|
pointsByTeam[team] += result.Points
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs.Champ = derivedDriverStandings(pointsByDriver, latestMeetingKey, latestSessionKey)
|
||||||
|
inputs.Teams = derivedTeamStandings(pointsByTeam, latestMeetingKey, latestSessionKey)
|
||||||
|
return inputs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) driversForSession(sessionKey int) ([]models.Driver, error) {
|
||||||
|
sess, err := s.store.GetSession(sessionKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
links, err := s.store.ListSessionDrivers(sessionKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
drivers := make([]models.Driver, 0, len(links))
|
||||||
|
for _, link := range links {
|
||||||
|
d, err := s.store.GetDriver(link.DriverNumber)
|
||||||
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
drivers = append(drivers, driverToModel(sessionKey, sess.MeetingKey, link, d))
|
||||||
|
}
|
||||||
|
return drivers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func derivedDriverStandings(points map[int]float64, meetingKey, sessionKey int) []models.ChampionshipDriver {
|
||||||
|
drivers := make([]models.ChampionshipDriver, 0, len(points))
|
||||||
|
for num, pts := range points {
|
||||||
|
drivers = append(drivers, models.ChampionshipDriver{
|
||||||
|
DriverNumber: num,
|
||||||
|
MeetingKey: meetingKey,
|
||||||
|
PointsCurrent: pts,
|
||||||
|
SessionKey: sessionKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(drivers, func(i, j int) bool {
|
||||||
|
if drivers[i].PointsCurrent == drivers[j].PointsCurrent {
|
||||||
|
return drivers[i].DriverNumber < drivers[j].DriverNumber
|
||||||
|
}
|
||||||
|
return drivers[i].PointsCurrent > drivers[j].PointsCurrent
|
||||||
|
})
|
||||||
|
for i := range drivers {
|
||||||
|
drivers[i].PositionCurrent = i + 1
|
||||||
|
}
|
||||||
|
return drivers
|
||||||
|
}
|
||||||
|
|
||||||
|
func derivedTeamStandings(points map[string]float64, meetingKey, sessionKey int) []models.ChampionshipTeam {
|
||||||
|
teams := make([]models.ChampionshipTeam, 0, len(points))
|
||||||
|
for name, pts := range points {
|
||||||
|
teams = append(teams, models.ChampionshipTeam{
|
||||||
|
TeamName: name,
|
||||||
|
MeetingKey: meetingKey,
|
||||||
|
PointsCurrent: pts,
|
||||||
|
SessionKey: sessionKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(teams, func(i, j int) bool {
|
||||||
|
if teams[i].PointsCurrent == teams[j].PointsCurrent {
|
||||||
|
return teams[i].TeamName < teams[j].TeamName
|
||||||
|
}
|
||||||
|
return teams[i].PointsCurrent > teams[j].PointsCurrent
|
||||||
|
})
|
||||||
|
for i := range teams {
|
||||||
|
teams[i].PositionCurrent = i + 1
|
||||||
|
}
|
||||||
|
return teams
|
||||||
|
}
|
||||||
@@ -166,6 +166,68 @@ func TestGetRaceHubCompleteData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetChampionshipInputsDerivesLocalStandings(t *testing.T) {
|
||||||
|
svc := openTestService(t)
|
||||||
|
seedRaceHubData(t, svc.store)
|
||||||
|
|
||||||
|
if err := svc.store.UpsertDriver(store.Driver{
|
||||||
|
DriverNumber: 44,
|
||||||
|
FullName: "Lewis Hamilton",
|
||||||
|
NameAcronym: "HAM",
|
||||||
|
TeamName: "Ferrari",
|
||||||
|
TeamColour: "E8002D",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("UpsertDriver() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := svc.store.UpsertSessionDriver(store.SessionDriver{
|
||||||
|
SessionKey: 9472,
|
||||||
|
DriverNumber: 44,
|
||||||
|
MeetingKey: 1229,
|
||||||
|
TeamName: "Ferrari",
|
||||||
|
TeamColour: "E8002D",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("UpsertSessionDriver() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := svc.store.UpsertSessionResult(store.SessionResult{
|
||||||
|
SessionKey: 9472,
|
||||||
|
DriverNumber: 1,
|
||||||
|
MeetingKey: 1229,
|
||||||
|
Position: 1,
|
||||||
|
Points: 25,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("UpsertSessionResult() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := svc.store.UpsertSessionResult(store.SessionResult{
|
||||||
|
SessionKey: 9472,
|
||||||
|
DriverNumber: 44,
|
||||||
|
MeetingKey: 1229,
|
||||||
|
Position: 2,
|
||||||
|
Points: 18,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("UpsertSessionResult() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs, err := svc.GetChampionshipInputs(2025)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetChampionshipInputs() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(inputs.Races) != 1 || inputs.Races[0].RaceSessionKey != 9472 {
|
||||||
|
t.Fatalf("Races = %+v, want Monaco race", inputs.Races)
|
||||||
|
}
|
||||||
|
if len(inputs.Champ) != 2 {
|
||||||
|
t.Fatalf("Champ len = %d, want 2", len(inputs.Champ))
|
||||||
|
}
|
||||||
|
if inputs.Champ[0].DriverNumber != 1 || inputs.Champ[0].PointsCurrent != 25 || inputs.Champ[0].PositionCurrent != 1 {
|
||||||
|
t.Fatalf("leader = %+v, want VER 25 P1", inputs.Champ[0])
|
||||||
|
}
|
||||||
|
if got := inputs.DriverMap[44]; got.NameAcronym != "HAM" || got.TeamName != "Ferrari" {
|
||||||
|
t.Fatalf("DriverMap[44] = %+v, want HAM Ferrari", got)
|
||||||
|
}
|
||||||
|
if len(inputs.Teams) != 2 || inputs.Teams[0].TeamName != "Red Bull Racing" || inputs.Teams[0].PointsCurrent != 25 {
|
||||||
|
t.Fatalf("Teams = %+v, want Red Bull Racing leading with 25", inputs.Teams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListMeetingsByYear(t *testing.T) {
|
func TestListMeetingsByYear(t *testing.T) {
|
||||||
svc := openTestService(t)
|
svc := openTestService(t)
|
||||||
seedRaceHubData(t, svc.store)
|
seedRaceHubData(t, svc.store)
|
||||||
|
|||||||
@@ -737,26 +737,77 @@ func (s *Server) handleChampionshipHub(w http.ResponseWriter, r *http.Request) {
|
|||||||
if year == 0 {
|
if year == 0 {
|
||||||
year = time.Now().Year()
|
year = time.Now().Year()
|
||||||
}
|
}
|
||||||
|
mode := parseSourceMode(r)
|
||||||
|
|
||||||
if resp, ok := s.hubCache.get(year, time.Now()); ok {
|
if mode != sourceLocal {
|
||||||
writeJSON(w, resp)
|
if resp, ok := s.hubCache.get(year, time.Now()); ok {
|
||||||
return
|
writeJSON(w, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meetings, err := s.client.GetMeetingsForYear(year)
|
if mode == sourceLocal || mode == sourceAuto {
|
||||||
|
resp, ok, err := s.localChampionshipHub(year)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err, http.StatusInternalServerError, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
s.hubCache.put(year, resp, time.Now(), champHubTTL(year, time.Now()))
|
||||||
|
writeJSON(w, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if mode == sourceLocal {
|
||||||
|
writeJSON(w, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.openF1ChampionshipHub(year)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, err, http.StatusInternalServerError, s.client.LastResponseWasStale())
|
writeError(w, err, http.StatusInternalServerError, s.client.LastResponseWasStale())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
writeJSON(w, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) localChampionshipHub(year int) (champHubResponse, bool, error) {
|
||||||
|
if !s.hasLocalQuery() {
|
||||||
|
return champHubResponse{Season: year, RoundLabels: []string{}, Drivers: []champHubDriver{}, Teams: []champHubTeam{}}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs, err := s.query.GetChampionshipInputs(year)
|
||||||
|
if err != nil {
|
||||||
|
return champHubResponse{}, false, err
|
||||||
|
}
|
||||||
|
if len(inputs.Champ) == 0 {
|
||||||
|
return champHubResponse{Season: year, RoundLabels: []string{}, Drivers: []champHubDriver{}, Teams: []champHubTeam{}}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
races := make([]meetingRace, 0, len(inputs.Races))
|
||||||
|
for _, race := range inputs.Races {
|
||||||
|
races = append(races, meetingRace{
|
||||||
|
Meeting: race.Meeting,
|
||||||
|
RaceSessionKey: race.RaceSessionKey,
|
||||||
|
Results: race.Results,
|
||||||
|
Grid: race.Grid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return aggregateChampionshipHub(year, races, inputs.Champ, inputs.Teams, inputs.DriverMap), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) openF1ChampionshipHub(year int) (champHubResponse, error) {
|
||||||
|
meetings, err := s.client.GetMeetingsForYear(year)
|
||||||
|
if err != nil {
|
||||||
|
return champHubResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
champ, err := s.client.GetDriverChampionshipForYear(year)
|
champ, err := s.client.GetDriverChampionshipForYear(year)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, err, http.StatusInternalServerError, s.client.LastResponseWasStale())
|
return champHubResponse{}, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if len(champ) == 0 {
|
if len(champ) == 0 {
|
||||||
writeJSON(w, champHubResponse{Season: year, RoundLabels: []string{}, Drivers: []champHubDriver{}, Teams: []champHubTeam{}})
|
return champHubResponse{Season: year, RoundLabels: []string{}, Drivers: []champHubDriver{}, Teams: []champHubTeam{}}, nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
teams, _ := s.client.GetTeamChampionshipForYear(year)
|
teams, _ := s.client.GetTeamChampionshipForYear(year)
|
||||||
|
|
||||||
@@ -801,7 +852,7 @@ func (s *Server) handleChampionshipHub(w http.ResponseWriter, r *http.Request) {
|
|||||||
ttl = champHubIncompleteTTL
|
ttl = champHubIncompleteTTL
|
||||||
}
|
}
|
||||||
s.hubCache.put(year, resp, time.Now(), ttl)
|
s.hubCache.put(year, resp, time.Now(), ttl)
|
||||||
writeJSON(w, resp)
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// aggregateChampionshipHub is the pure aggregation core (no network) so it can be
|
// aggregateChampionshipHub is the pure aggregation core (no network) so it can be
|
||||||
|
|||||||
Reference in New Issue
Block a user