import Combine import Foundation @MainActor final class HermesRepository: ObservableObject { @Published private(set) var currentSession: HermesSessionResponse? @Published private(set) var currentRound: HermesRound? @Published private(set) var isLoading = true @Published private(set) var errorCause: Error? @Published private(set) var serverClockOffset: TimeInterval? private let apiClient: HermesAPIClient init(apiClient: HermesAPIClient) { self.apiClient = apiClient } func bootstrap(_ request: HermesSessionStartRequest) async throws -> HermesSessionResponse { isLoading = true errorCause = nil do { await syncClock() let session: HermesSessionResponse if let existingSession = currentSession { session = existingSession } else { session = try await startSession(request) } if currentRound == nil { currentRound = try await loadRoundFromNetwork() } isLoading = false return session } catch { errorCause = error isLoading = false throw error } } func refreshRoundFromNetwork() async throws -> HermesRound { isLoading = true errorCause = nil do { await syncClock() let round = try await loadRoundFromNetwork() currentRound = round isLoading = false return round } catch { errorCause = error isLoading = false throw error } } func startSession(_ request: HermesSessionStartRequest) async throws -> HermesSessionResponse { let session = try await apiClient.startSession(request) currentSession = session return session } func endSession() async throws -> HermesSessionResponse { let session = try await apiClient.endSession() currentSession = session return session } func submitBetIntent(_ request: HermesBetIntentRequest) async throws -> HermesBetIntentResponse { try await apiClient.submitBetIntent(request) } func currentOdds(marketID: UUID) async throws -> HermesOddsVersion { try await apiClient.currentOdds(marketID: marketID) } func settlement(eventID: UUID) async throws -> HermesSettlement { try await apiClient.settlement(eventID: eventID) } func experimentConfig() async throws -> HermesExperimentConfig { try await apiClient.experimentConfig() } func localization(localeCode: String) async throws -> HermesLocalizationBundle { try await apiClient.localization(localeCode: localeCode) } func submitAnalyticsBatch(_ payload: HermesAnalyticsBatchRequest) async throws { try await apiClient.submitAnalyticsBatch(payload) } func serverNow() -> Date { guard let serverClockOffset else { return Date() } return Date().addingTimeInterval(serverClockOffset) } private func syncClock() async { do { let health = try await apiClient.health() serverClockOffset = health.serverTime.timeIntervalSince(Date()) } catch { return } } private func loadRoundFromNetwork() async throws -> HermesRound { let event = try await apiClient.nextEvent() let manifest = try await apiClient.eventManifest(eventID: event.id) guard let media = manifest.media.first(where: { $0.mediaType == "hls_main" }) ?? manifest.media.first else { throw HermesAPIError.invalidResponse } let market: HermesMarket if let manifestMarket = manifest.markets.first { market = manifestMarket } else { let markets = try await apiClient.markets(eventID: event.id) guard let fallbackMarket = markets.first else { throw HermesAPIError.invalidResponse } market = fallbackMarket } let oddsVersion = try await apiClient.currentOdds(marketID: market.id) let settlement = try await apiClient.settlement(eventID: event.id) return HermesRound( event: event, media: media, market: market, oddsVersion: oddsVersion, settlement: settlement ) } }