import Foundation protocol SystemStatusServing: Sendable { func fetchSnapshotHTML() async throws -> String func fetchIncidentFeedData() async throws -> Data } struct CachedSystemStatusValue: Sendable { let value: Value let lastSuccessfulAt: Date let isStale: Bool let refreshErrorMessage: String? } actor SystemStatusRepository { private let service: any SystemStatusServing private let ttl: TimeInterval private let cacheStore: SystemStatusCacheStore private let now: @Sendable () -> Date private var snapshotCache: CacheEntry? private var incidentsCache: CacheEntry<[StatusIncident]>? private var hasLoadedPersistentCache = false init( service: any SystemStatusServing = SystemStatusService(), ttl: TimeInterval = 10 * 60, cacheStore: SystemStatusCacheStore = SystemStatusCacheStore(), now: @escaping @Sendable () -> Date = Date.init ) { self.service = service self.ttl = ttl self.cacheStore = cacheStore self.now = now } func snapshot(forceRefresh: Bool = false) async throws -> SystemStatusSnapshot { try await snapshotResult(forceRefresh: forceRefresh).value } func recentIncidents(forceRefresh: Bool = false) async throws -> [StatusIncident] { try await recentIncidentsResult(forceRefresh: forceRefresh).value } func snapshotResult(forceRefresh: Bool = false) async throws -> CachedSystemStatusValue { await loadPersistentCacheIfNeeded() if let cached = snapshotCache, !forceRefresh, !cached.isExpired(ttl: ttl, now: now) { return CachedSystemStatusValue( value: cached.value, lastSuccessfulAt: cached.timestamp, isStale: false, refreshErrorMessage: nil ) } do { let html = try await service.fetchSnapshotHTML() let snapshot = try SystemStatusService.parseSnapshotHTML(html, fetchedAt: now()) let entry = CacheEntry(value: snapshot, timestamp: now()) snapshotCache = entry await cacheStore.saveSnapshotHTML(html, timestamp: entry.timestamp) return CachedSystemStatusValue( value: snapshot, lastSuccessfulAt: entry.timestamp, isStale: false, refreshErrorMessage: nil ) } catch { if let cached = snapshotCache { return CachedSystemStatusValue( value: cached.value, lastSuccessfulAt: cached.timestamp, isStale: true, refreshErrorMessage: refreshErrorMessage(from: error) ) } throw error } } func recentIncidentsResult(forceRefresh: Bool = false) async throws -> CachedSystemStatusValue<[StatusIncident]> { await loadPersistentCacheIfNeeded() if let cached = incidentsCache, !forceRefresh, !cached.isExpired(ttl: ttl, now: now) { return CachedSystemStatusValue( value: cached.value, lastSuccessfulAt: cached.timestamp, isStale: false, refreshErrorMessage: nil ) } do { let feedData = try await service.fetchIncidentFeedData() let incidents = try await SystemStatusService.parseIncidentFeedXML(feedData) let entry = CacheEntry(value: incidents, timestamp: now()) incidentsCache = entry await cacheStore.saveIncidentFeedData(feedData, timestamp: entry.timestamp) return CachedSystemStatusValue( value: incidents, lastSuccessfulAt: entry.timestamp, isStale: false, refreshErrorMessage: nil ) } catch { if let cached = incidentsCache { return CachedSystemStatusValue( value: cached.value, lastSuccessfulAt: cached.timestamp, isStale: true, refreshErrorMessage: refreshErrorMessage(from: error) ) } throw error } } private func loadPersistentCacheIfNeeded() async { guard !hasLoadedPersistentCache else { return } if let persistedSnapshot = await cacheStore.loadSnapshotHTML(), let snapshot = try? SystemStatusService.parseSnapshotHTML(persistedSnapshot.html, fetchedAt: persistedSnapshot.timestamp) { snapshotCache = CacheEntry(value: snapshot, timestamp: persistedSnapshot.timestamp) } if let persistedFeed = await cacheStore.loadIncidentFeedData(), let incidents = try? await SystemStatusService.parseIncidentFeedXML(persistedFeed.data) { incidentsCache = CacheEntry(value: incidents, timestamp: persistedFeed.timestamp) } hasLoadedPersistentCache = true } private func refreshErrorMessage(from error: any Error) -> String { error.userFacingMessage } } struct CacheEntry: Sendable { let value: Value let timestamp: Date nonisolated func isExpired(ttl: TimeInterval, now: @escaping @Sendable () -> Date = Date.init) -> Bool { now().timeIntervalSince(timestamp) > ttl } }