Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions latest.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ func matchOpenF1Session(sessions []openF1Session, key string, at time.Time) *ope
return match
}

func latestFinishedSession(races []race, now time.Time) (*race, string, time.Time) {
var found *race
var foundKey string
var foundAt time.Time
type finishedSession struct {
race *race
key string
at time.Time
}

// finishedSessions lists the weekend's finished non-race sessions, newest first.
func finishedSessions(races []race, now time.Time) []finishedSession {
var found []finishedSession
for i := range races {
for _, key := range sessionKeys {
if key == "race" {
Expand All @@ -52,12 +57,19 @@ func latestFinishedSession(races []race, now time.Time) (*race, string, time.Tim
if at.IsZero() || at.Add(sessionWindows[key]).After(now) {
continue
}
if foundAt.IsZero() || at.After(foundAt) {
found, foundKey, foundAt = &races[i], key, at
}
found = append(found, finishedSession{&races[i], key, at})
}
}
return found, foundKey, foundAt
sort.Slice(found, func(i, j int) bool { return found[i].at.After(found[j].at) })
return found
}

func sessionResultsKey(year int, s finishedSession) string {
return fmt.Sprintf("session_results:%d:%d:%s", year, s.race.Round, s.key)
}

func (a *app) sessionResponse(year int, s finishedSession, results any) map[string]any {
return map[string]any{"season": year, "round": s.race.Round, "raceName": s.race.RaceName, "url": s.race.URL, "session": sessionLabels[s.key], "key": s.key, "date": formatRFC3339(s.at.In(a.config.timezone)), "results": results}
}

func (a *app) latestSession(c echo.Context) error {
Expand All @@ -70,25 +82,39 @@ func (a *app) latestSession(c echo.Context) error {
if err != nil {
return c.JSON(http.StatusOK, map[string]string{"error": "Exception while fetching: " + err.Error()})
}
r, key, at := latestFinishedSession(races, now)
if r == nil {
sessions := finishedSessions(races, now)
if len(sessions) == 0 {
return c.JSON(http.StatusOK, map[string]string{"message": "No completed session yet"})
}
result := map[string]any{"season": year, "round": r.Round, "raceName": r.RaceName, "url": r.URL, "session": sessionLabels[key], "key": key, "date": formatRFC3339(at.In(a.config.timezone)), "results": []any{}}
ttl := 2 * time.Minute
latest := sessions[0]
result := a.sessionResponse(year, latest, []any{})
ttl, have := 2*time.Minute, false
// A finished session's classification never changes, so it's kept for
// good - OpenF1's free tier locks out all access while any session is live.
cacheKey := fmt.Sprintf("session_results:%d:%d:%s", year, r.Round, key)
cacheKey := sessionResultsKey(year, latest)
if cached, ok := a.cache.getDurable(cacheKey, now); ok {
result["results"], ttl = cached, 5*time.Minute
} else if rows, ended, fetchErr := a.fetchSessionResults(key, at, now); fetchErr != nil {
result["results"], ttl, have = cached, 5*time.Minute, true
} else if rows, ended, fetchErr := a.fetchSessionResults(latest.key, latest.at, now); fetchErr != nil {
result["upstream_error"] = fetchErr.Error()
} else {
result["results"], ttl = rows, 5*time.Minute
if ended && len(rows) > 0 {
have = len(rows) > 0
if ended && have {
a.keep(cacheKey, rows, now)
}
}
if !have {
// Latest results aren't available yet (typically the lockout right after a
// session): show the newest earlier session we still hold, and say what's pending.
for _, s := range sessions[1:] {
if cached, ok := a.cache.getDurable(sessionResultsKey(year, s), now); ok {
fallback := a.sessionResponse(year, s, cached)
fallback["pending"] = result["session"]
result, ttl = fallback, 2*time.Minute
break
}
}
}
a.cache.set("f1:latest_session", result, now.Add(ttl))
return c.JSON(http.StatusOK, result)
}
Expand Down
17 changes: 17 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,20 @@ func TestFinishedSessionsSurviveARestartWhenCacheDirIsSet(t *testing.T) {
t.Fatalf("expected an upstream error without a cache dir: %#v", got)
}
}

func TestLatestSessionFallsBackToLastCompleteSessionDuringLockout(t *testing.T) {
mock := newUpstreamMock(t)
a := testApp(t, mock)
server := newServer(a)
a.now = func() time.Time { return time.Date(2026, 3, 6, 15, 0, 0, 0, time.UTC) }
before := decode(t, request(t, server, "/f1/latest_session/"))
mock.mu.Lock()
mock.locked = true
mock.mu.Unlock()
// A later session has finished but OpenF1 is locked out: keep showing the cached one.
a.now = func() time.Time { return time.Date(2026, 3, 7, 17, 0, 0, 0, time.UTC) }
result := decode(t, request(t, server, "/f1/latest_session/"))
if result["pending"] == nil || result["session"] == result["pending"] || len(result["results"].([]any)) != 3 {
t.Fatalf("expected earlier cached session plus pending note (before=%v): %#v", before["session"], result)
}
}
5 changes: 4 additions & 1 deletion widgets/latest-session/f1_latest_session.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
# (practice, sprint qualifying, qualifying, sprint) - the Grand Prix itself
# is Last Race Results' job. Practice/qualifying results only exist on
# OpenF1, which locks out free access while any session is live, so a
# just-finished session can take a while to show up.
# just-finished session can take a while to show up - until it does, the
# previous session's results are shown with a note.
template: |
{{ $session := .JSON.String "session" }}
{{ $rows := .JSON.Array "results" }}
Expand All @@ -15,6 +16,8 @@
{{ else }}
<div class="flex flex-column gap-10">
<p class="size-h5">{{ $session }}</p>
{{ $pending := .JSON.String "pending" }}
{{ if $pending }}<p class="size-h6 color-negative">{{ $pending }} results not available yet - showing the last complete session.</p>{{ end }}
<p class="size-h6 color-base">
{{ $url := .JSON.String "url" }}
{{ if $url }}<a href="{{ $url }}" target="_blank" rel="noreferrer">{{ .JSON.String "raceName" }}</a>{{ else }}{{ .JSON.String "raceName" }}{{ end }}
Expand Down
Loading