// // NHLDataCache.swift // IceGlass // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import Foundation /// Snapshot of API state persisted between app launches so cold-launch shows /// last-known data with an "as-of" timestamp instead of an empty page. struct CachedSnapshot: Codable, Sendable { let lastUpdated: Date let scoreboard: Scoreboard? let standings: Standings? let bracket: PlayoffBracket? } /// Single-file Codable cache in Application Support. Atomic writes; fail-soft /// reads — corrupt or version-mismatched payloads return nil and the next /// fetch overwrites with fresh data. actor NHLDataCache { private let logger = IceGlassLogger( subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.IceGlass", category: "NHLDataCache" ) private let fileURL: URL init(filename: String = "nhl-snapshot.json") { let fm = FileManager.default let supportDir = (try? fm.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true )) ?? fm.temporaryDirectory let bundleId = Bundle.main.bundleIdentifier ?? "dev.rzen.indie.IceGlass" let appDir = supportDir.appendingPathComponent(bundleId, isDirectory: true) try? fm.createDirectory(at: appDir, withIntermediateDirectories: true) self.fileURL = appDir.appendingPathComponent(filename) } func load() -> CachedSnapshot? { guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil } do { let data = try Data(contentsOf: fileURL) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return try decoder.decode(CachedSnapshot.self, from: data) } catch { logger.warning("Cache load failed (will refetch): \(error.localizedDescription)") return nil } } func save(_ snapshot: CachedSnapshot) { do { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let data = try encoder.encode(snapshot) try data.write(to: fileURL, options: .atomic) } catch { logger.error("Cache save failed: \(error.localizedDescription)") } } }