Stabilize F1 Live scrolling and news reader, and speed up the dashboard fetch.
This commit is contained in:
parent
b911a4a7a7
commit
00efebdf84
5 changed files with 155 additions and 59 deletions
|
|
@ -358,6 +358,12 @@ actor ApiClient {
|
||||||
try await get("/api/v1/client/f1/live")
|
try await get("/api/v1/client/f1/live")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func f1News() async throws -> [F1NewsItem] {
|
||||||
|
struct Wrap: Decodable { let items: [F1NewsItem]? }
|
||||||
|
let wrap: Wrap = try await get("/api/v1/client/f1/news")
|
||||||
|
return wrap.items ?? []
|
||||||
|
}
|
||||||
|
|
||||||
func f1NewsArticle(id: String) async throws -> F1NewsArticle {
|
func f1NewsArticle(id: String) async throws -> F1NewsArticle {
|
||||||
let enc = Self.encodePathSegment(id)
|
let enc = Self.encodePathSegment(id)
|
||||||
return try await get("/api/v1/client/f1/news/\(enc)")
|
return try await get("/api/v1/client/f1/news/\(enc)")
|
||||||
|
|
|
||||||
|
|
@ -250,8 +250,9 @@ struct F1HubScreen: View {
|
||||||
.buttonStyle(HubPrimaryButtonStyle())
|
.buttonStyle(HubPrimaryButtonStyle())
|
||||||
NavigationLink(value: EventHubRoute.f1Live) {
|
NavigationLink(value: EventHubRoute.f1Live) {
|
||||||
Label("Timing", systemImage: "timer")
|
Label("Timing", systemImage: "timer")
|
||||||
|
.modifier(HubNavCapsule(kind: .glass))
|
||||||
}
|
}
|
||||||
.buttonStyle(HubGlassButtonStyle())
|
.buttonStyle(.plain)
|
||||||
} else if let meeting {
|
} else if let meeting {
|
||||||
NavigationLink(value: EventHubRoute.f1Meeting(meeting.meetingKey)) {
|
NavigationLink(value: EventHubRoute.f1Meeting(meeting.meetingKey)) {
|
||||||
Label("Bekijk weekend", systemImage: "calendar")
|
Label("Bekijk weekend", systemImage: "calendar")
|
||||||
|
|
@ -259,13 +260,15 @@ struct F1HubScreen: View {
|
||||||
.buttonStyle(HubPrimaryButtonStyle())
|
.buttonStyle(HubPrimaryButtonStyle())
|
||||||
NavigationLink(value: EventHubRoute.f1Live) {
|
NavigationLink(value: EventHubRoute.f1Live) {
|
||||||
Label("Timing", systemImage: "timer")
|
Label("Timing", systemImage: "timer")
|
||||||
|
.modifier(HubNavCapsule(kind: .glass))
|
||||||
}
|
}
|
||||||
.buttonStyle(HubGlassButtonStyle())
|
.buttonStyle(.plain)
|
||||||
} else {
|
} else {
|
||||||
NavigationLink(value: EventHubRoute.f1Live) {
|
NavigationLink(value: EventHubRoute.f1Live) {
|
||||||
Label("Live timing", systemImage: "timer")
|
Label("Live timing", systemImage: "timer")
|
||||||
|
.modifier(HubNavCapsule(kind: .primary))
|
||||||
}
|
}
|
||||||
.buttonStyle(HubPrimaryButtonStyle())
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let meeting {
|
} else if let meeting {
|
||||||
|
|
@ -286,8 +289,9 @@ struct F1HubScreen: View {
|
||||||
.buttonStyle(HubPrimaryButtonStyle())
|
.buttonStyle(HubPrimaryButtonStyle())
|
||||||
NavigationLink(value: EventHubRoute.f1Live) {
|
NavigationLink(value: EventHubRoute.f1Live) {
|
||||||
Label("Timing", systemImage: "timer")
|
Label("Timing", systemImage: "timer")
|
||||||
|
.modifier(HubNavCapsule(kind: .glass))
|
||||||
}
|
}
|
||||||
.buttonStyle(HubGlassButtonStyle())
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HubHero(
|
HubHero(
|
||||||
|
|
@ -301,8 +305,9 @@ struct F1HubScreen: View {
|
||||||
) {
|
) {
|
||||||
NavigationLink(value: EventHubRoute.f1Live) {
|
NavigationLink(value: EventHubRoute.f1Live) {
|
||||||
Label("Live timing", systemImage: "timer")
|
Label("Live timing", systemImage: "timer")
|
||||||
|
.modifier(HubNavCapsule(kind: .primary))
|
||||||
}
|
}
|
||||||
.buttonStyle(HubPrimaryButtonStyle())
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +352,7 @@ struct F1HubScreen: View {
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
}
|
}
|
||||||
.buttonStyle(HubPressStyle())
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func seasonSection(_ p: F1HubPayload) -> some View {
|
private func seasonSection(_ p: F1HubPayload) -> some View {
|
||||||
|
|
@ -1579,6 +1584,22 @@ struct HubPressStyle: ButtonStyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct HubNavCapsule: ViewModifier {
|
||||||
|
enum Kind { case glass, primary }
|
||||||
|
let kind: Kind
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundStyle(kind == .primary ? Color.black : Color.white)
|
||||||
|
.padding(.horizontal, kind == .primary ? 22 : 20)
|
||||||
|
.frame(height: 46)
|
||||||
|
.background(kind == .primary ? Color.white : Color.white.opacity(0.16), in: Capsule())
|
||||||
|
.overlay(Capsule().strokeBorder(.white.opacity(kind == .primary ? 0 : 0.18), lineWidth: 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct HubPrimaryButtonStyle: ButtonStyle {
|
private struct HubPrimaryButtonStyle: ButtonStyle {
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
configuration.label
|
configuration.label
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,17 @@ struct F1LiveScreen: View {
|
||||||
|
|
||||||
@EnvironmentObject private var app: AppModel
|
@EnvironmentObject private var app: AppModel
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@State private var tab: Tab = .overview
|
@State private var tab: Tab = .overview
|
||||||
@State private var payload: F1LiveDashboard?
|
@State private var payload: F1LiveDashboard?
|
||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
@State private var speedTrail: [Double] = []
|
@State private var speedTrail: [Double] = []
|
||||||
|
@State private var selectedNews: F1NewsItem?
|
||||||
|
@State private var loading = false
|
||||||
|
@State private var newsItems: [F1NewsItem] = []
|
||||||
|
|
||||||
private let clock = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
|
private let clock = Timer.publish(every: 8, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
|
|
@ -63,8 +67,18 @@ struct F1LiveScreen: View {
|
||||||
.environment(\.colorScheme, .dark)
|
.environment(\.colorScheme, .dark)
|
||||||
.navigationBarBackButtonHidden(true)
|
.navigationBarBackButtonHidden(true)
|
||||||
.toolbar(.hidden, for: .navigationBar)
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
.task { await load() }
|
.safeAreaPadding(.top)
|
||||||
.onReceive(clock) { _ in Task { await load(silent: true) } }
|
.onAppear {
|
||||||
|
if payload == nil { Task { await load() } }
|
||||||
|
}
|
||||||
|
.onReceive(clock) { _ in
|
||||||
|
guard scenePhase == .active, selectedNews == nil, payload != nil else { return }
|
||||||
|
Task { await load(silent: true) }
|
||||||
|
}
|
||||||
|
.fullScreenCover(item: $selectedNews) { item in
|
||||||
|
F1NewsArticleScreen(preview: item)
|
||||||
|
.environmentObject(app)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
@ -80,20 +94,21 @@ struct F1LiveScreen: View {
|
||||||
].compactMap { $0 }.joined(separator: " · ")
|
].compactMap { $0 }.joined(separator: " · ")
|
||||||
)
|
)
|
||||||
tabBar
|
tabBar
|
||||||
ScrollView {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
Group {
|
Group {
|
||||||
switch tab {
|
switch tab {
|
||||||
case .overview: overview(p)
|
case .overview: overview(p)
|
||||||
case .timing: timingTable(p.timing)
|
case .timing: timingTable(p.timing)
|
||||||
case .map: mapBlock(p)
|
case .map: mapBlock(p)
|
||||||
case .telemetry: telemetryBlock(p)
|
case .telemetry: telemetryBlock(p)
|
||||||
case .news: newsList(p.news)
|
case .news: newsList(newsItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 14)
|
.padding(.top, 14)
|
||||||
.padding(.bottom, 40)
|
.padding(.bottom, 40)
|
||||||
}
|
}
|
||||||
|
.scrollBounceBehavior(.basedOnSize, axes: .horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,6 +172,7 @@ struct F1LiveScreen: View {
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
|
.frame(height: 38)
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,9 +207,9 @@ struct F1LiveScreen: View {
|
||||||
if let t = p.telemetry {
|
if let t = p.telemetry {
|
||||||
telemetryCard(t)
|
telemetryCard(t)
|
||||||
}
|
}
|
||||||
if !p.news.isEmpty {
|
if !newsItems.isEmpty {
|
||||||
sectionTitle("Nieuws")
|
sectionTitle("Nieuws")
|
||||||
ForEach(p.news.prefix(4)) { item in
|
ForEach(newsItems.prefix(4)) { item in
|
||||||
newsCard(item)
|
newsCard(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -465,8 +481,8 @@ struct F1LiveScreen: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func newsCard(_ item: F1NewsItem) -> some View {
|
private func newsCard(_ item: F1NewsItem) -> some View {
|
||||||
NavigationLink {
|
Button {
|
||||||
F1NewsArticleScreen(preview: item)
|
selectedNews = item
|
||||||
} label: {
|
} label: {
|
||||||
HStack(alignment: .top, spacing: 12) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
RemoteImage(url: item.imageUrl, contentMode: .fill)
|
RemoteImage(url: item.imageUrl, contentMode: .fill)
|
||||||
|
|
@ -574,15 +590,30 @@ struct F1LiveScreen: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func load(silent: Bool = false) async {
|
private func load(silent: Bool = false) async {
|
||||||
|
if loading { return }
|
||||||
|
loading = true
|
||||||
|
defer { loading = false }
|
||||||
do {
|
do {
|
||||||
let next = try await app.api.f1Live()
|
let next = try await app.api.f1Live()
|
||||||
|
if Task.isCancelled { return }
|
||||||
payload = next
|
payload = next
|
||||||
error = nil
|
error = nil
|
||||||
|
if newsItems.isEmpty { newsItems = next.news }
|
||||||
if let speed = next.telemetry?.speed {
|
if let speed = next.telemetry?.speed {
|
||||||
speedTrail.append(speed)
|
speedTrail.append(speed)
|
||||||
if speedTrail.count > 40 { speedTrail.removeFirst(speedTrail.count - 40) }
|
if speedTrail.count > 40 { speedTrail.removeFirst(speedTrail.count - 40) }
|
||||||
}
|
}
|
||||||
|
if newsItems.isEmpty {
|
||||||
|
if let items = try? await app.api.f1News(), !items.isEmpty, !Task.isCancelled {
|
||||||
|
newsItems = items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch is CancellationError {
|
||||||
|
return
|
||||||
|
} catch let err as URLError where err.code == .cancelled {
|
||||||
|
return
|
||||||
} catch {
|
} catch {
|
||||||
|
if Task.isCancelled { return }
|
||||||
if payload == nil && !silent { self.error = error.localizedDescription }
|
if payload == nil && !silent { self.error = error.localizedDescription }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -710,14 +741,14 @@ private struct F1NewsArticleScreen: View {
|
||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .top) {
|
||||||
F1LiveLook.bg.ignoresSafeArea()
|
F1LiveLook.bg.ignoresSafeArea()
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
hero
|
hero
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
Text(article?.title ?? preview.title)
|
Text(article?.title ?? preview.title)
|
||||||
.font(.system(size: 28, weight: .bold))
|
.font(.system(size: 26, weight: .bold))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
metaRow
|
metaRow
|
||||||
|
|
@ -741,22 +772,34 @@ private struct F1NewsArticleScreen: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.top, 22)
|
.padding(.top, 18)
|
||||||
.padding(.bottom, 56)
|
.padding(.bottom, 40)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
backButton
|
}
|
||||||
|
.safeAreaInset(edge: .top, spacing: 0) {
|
||||||
|
HStack {
|
||||||
|
Button { dismiss() } label: {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 14, weight: .bold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
.background(.white.opacity(0.14), in: Circle())
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(F1LiveLook.bg.opacity(0.92))
|
||||||
}
|
}
|
||||||
.environment(\.colorScheme, .dark)
|
.environment(\.colorScheme, .dark)
|
||||||
.navigationBarBackButtonHidden(true)
|
|
||||||
.toolbar(.hidden, for: .navigationBar)
|
|
||||||
.task { await load() }
|
.task { await load() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hero: some View {
|
private var hero: some View {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
RemoteImage(url: article?.imageUrl ?? preview.imageUrl, contentMode: .fill)
|
RemoteImage(url: article?.imageUrl ?? preview.imageUrl, contentMode: .fill)
|
||||||
.frame(height: 280)
|
.frame(height: 220)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.clipped()
|
.clipped()
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
|
|
@ -766,7 +809,7 @@ private struct F1NewsArticleScreen: View {
|
||||||
)
|
)
|
||||||
.frame(height: 140)
|
.frame(height: 140)
|
||||||
}
|
}
|
||||||
.frame(height: 280)
|
.frame(height: 220)
|
||||||
.clipped()
|
.clipped()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -825,24 +868,14 @@ private struct F1NewsArticleScreen: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var backButton: some View {
|
|
||||||
Button { dismiss() } label: {
|
|
||||||
Image(systemName: "chevron.left")
|
|
||||||
.font(.system(size: 15, weight: .semibold))
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.frame(width: 36, height: 36)
|
|
||||||
.background(.black.opacity(0.45), in: Circle())
|
|
||||||
.overlay(Circle().strokeBorder(Color.white.opacity(0.12), lineWidth: 1))
|
|
||||||
}
|
|
||||||
.padding(.leading, 16)
|
|
||||||
.padding(.top, 6)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func load() async {
|
private func load() async {
|
||||||
do {
|
do {
|
||||||
article = try await app.api.f1NewsArticle(id: preview.id)
|
article = try await app.api.f1NewsArticle(id: preview.id)
|
||||||
error = nil
|
error = nil
|
||||||
|
} catch is CancellationError {
|
||||||
|
return
|
||||||
} catch {
|
} catch {
|
||||||
|
if Task.isCancelled { return }
|
||||||
self.error = "Artikel kon niet worden geladen."
|
self.error = "Artikel kon niet worden geladen."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function buildUrl(path: string, params: OpenF1Params): string {
|
||||||
|
|
||||||
export async function openF1Get<T = unknown>(path: string, params: OpenF1Params = {}, timeoutMs = 18_000): Promise<T> {
|
export async function openF1Get<T = unknown>(path: string, params: OpenF1Params = {}, timeoutMs = 18_000): Promise<T> {
|
||||||
let lastErr: Error | null = null;
|
let lastErr: Error | null = null;
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
for (let attempt = 0; attempt < 2; attempt++) {
|
||||||
const token = await getOpenF1AccessToken();
|
const token = await getOpenF1AccessToken();
|
||||||
const ac = new AbortController();
|
const ac = new AbortController();
|
||||||
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,8 @@ const TYRE: Record<string, string> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
let snapCache: { at: number; data: F1LiveDashboard } | null = null;
|
let snapCache: { at: number; data: F1LiveDashboard } | null = null;
|
||||||
|
let inflight: Promise<F1LiveDashboard> | null = null;
|
||||||
|
const OF1_MS = 8_000;
|
||||||
|
|
||||||
function formatLap(v: unknown): string {
|
function formatLap(v: unknown): string {
|
||||||
const n = num(v);
|
const n = num(v);
|
||||||
|
|
@ -151,9 +153,35 @@ export async function getF1LiveDashboard(): Promise<F1LiveDashboard> {
|
||||||
if (!(await isOpenF1Configured())) {
|
if (!(await isOpenF1Configured())) {
|
||||||
throw new AppError("NOT_CONFIGURED", "OpenF1 is niet geconfigureerd", 503);
|
throw new AppError("NOT_CONFIGURED", "OpenF1 is niet geconfigureerd", 503);
|
||||||
}
|
}
|
||||||
if (snapCache && Date.now() - snapCache.at < 2500) return snapCache.data;
|
const now = Date.now();
|
||||||
|
const ttl = snapCache?.data.live ? 2_000 : 10_000;
|
||||||
|
if (snapCache && now - snapCache.at < ttl) return snapCache.data;
|
||||||
|
if (snapCache && now - snapCache.at < 45_000) {
|
||||||
|
if (!inflight) {
|
||||||
|
inflight = buildDashboard().finally(() => {
|
||||||
|
inflight = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return snapCache.data;
|
||||||
|
}
|
||||||
|
if (inflight) return inflight;
|
||||||
|
inflight = buildDashboard().finally(() => {
|
||||||
|
inflight = null;
|
||||||
|
});
|
||||||
|
return inflight;
|
||||||
|
}
|
||||||
|
|
||||||
const sessions = asRows(await openF1Get("sessions", { session_key: "latest" }));
|
async function buildDashboard(): Promise<F1LiveDashboard> {
|
||||||
|
try {
|
||||||
|
return await buildDashboardInner();
|
||||||
|
} catch (err) {
|
||||||
|
if (snapCache) return snapCache.data;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildDashboardInner(): Promise<F1LiveDashboard> {
|
||||||
|
const sessions = asRows(await openF1Get("sessions", { session_key: "latest" }, OF1_MS));
|
||||||
const session = rec(sessions[0]);
|
const session = rec(sessions[0]);
|
||||||
const sessionKey = num(session.session_key);
|
const sessionKey = num(session.session_key);
|
||||||
const meetingKey = num(session.meeting_key);
|
const meetingKey = num(session.meeting_key);
|
||||||
|
|
@ -171,9 +199,8 @@ export async function getF1LiveDashboard(): Promise<F1LiveDashboard> {
|
||||||
const year = num(session.year) ?? new Date().getUTCFullYear();
|
const year = num(session.year) ?? new Date().getUTCFullYear();
|
||||||
const circuitKey = num(session.circuit_key);
|
const circuitKey = num(session.circuit_key);
|
||||||
|
|
||||||
const driverRows = asRows(await openF1Get("drivers", { session_key: sessionKey }));
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
driverRows,
|
||||||
meetingRows,
|
meetingRows,
|
||||||
positionRows,
|
positionRows,
|
||||||
intervalRows,
|
intervalRows,
|
||||||
|
|
@ -184,29 +211,38 @@ export async function getF1LiveDashboard(): Promise<F1LiveDashboard> {
|
||||||
locationRows,
|
locationRows,
|
||||||
scheduleRows,
|
scheduleRows,
|
||||||
circuitPack,
|
circuitPack,
|
||||||
news,
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
settled(openF1Get("meetings", { meeting_key: meetingKey ?? "latest" }).then(asRows), []),
|
settled(openF1Get("drivers", { session_key: sessionKey }, OF1_MS).then(asRows), []),
|
||||||
settled(openF1Get("position", { session_key: sessionKey }).then(asRows), []),
|
settled(openF1Get("meetings", { meeting_key: meetingKey ?? "latest" }, OF1_MS).then(asRows), []),
|
||||||
settled(openF1Get("intervals", { session_key: sessionKey }).then(asRows), []),
|
settled(openF1Get("position", { session_key: sessionKey }, OF1_MS).then(asRows), []),
|
||||||
settled(openF1Get("laps", { session_key: sessionKey }).then(asRows), []),
|
settled(openF1Get("intervals", { session_key: sessionKey }, OF1_MS).then(asRows), []),
|
||||||
settled(openF1Get("stints", { session_key: sessionKey }).then(asRows), []),
|
settled(openF1Get("laps", { session_key: sessionKey }, OF1_MS).then(asRows), []),
|
||||||
settled(openF1Get("weather", { session_key: sessionKey }).then(asRows), []),
|
settled(openF1Get("stints", { session_key: sessionKey }, OF1_MS).then(asRows), []),
|
||||||
settled(openF1Get("race_control", { session_key: sessionKey }).then(asRows), []),
|
settled(openF1Get("weather", { session_key: sessionKey }, OF1_MS).then(asRows), []),
|
||||||
|
settled(openF1Get("race_control", { session_key: sessionKey }, OF1_MS).then(asRows), []),
|
||||||
settled(
|
settled(
|
||||||
openF1Get("location", {
|
openF1Get(
|
||||||
|
"location",
|
||||||
|
{
|
||||||
session_key: sessionKey,
|
session_key: sessionKey,
|
||||||
"date>=": locFrom,
|
"date>=": locFrom,
|
||||||
...(locTo ? { "date<": locTo } : {}),
|
...(locTo ? { "date<": locTo } : {}),
|
||||||
}).then(asRows),
|
},
|
||||||
|
OF1_MS
|
||||||
|
).then(asRows),
|
||||||
[]
|
[]
|
||||||
),
|
),
|
||||||
meetingKey
|
meetingKey
|
||||||
? settled(openF1Get("sessions", { meeting_key: meetingKey }).then(asRows), [])
|
? settled(openF1Get("sessions", { meeting_key: meetingKey }, OF1_MS).then(asRows), [])
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
circuitKey ? loadCircuitMap(circuitKey, year) : Promise.resolve(null),
|
circuitKey ? loadCircuitMap(circuitKey, year) : Promise.resolve(null),
|
||||||
settled(getF1News(12), []),
|
|
||||||
]);
|
]);
|
||||||
|
const news = snapCache?.data.news ?? [];
|
||||||
|
void getF1News(12)
|
||||||
|
.then((items) => {
|
||||||
|
if (snapCache) snapCache.data = { ...snapCache.data, news: items };
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
|
||||||
const meeting = rec(meetingRows[0]);
|
const meeting = rec(meetingRows[0]);
|
||||||
const drivers = driverRows.map(rec);
|
const drivers = driverRows.map(rec);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue