import Foundation import UIKit typealias PlatformImage = UIImage enum AppCacheTTL { static let twoHours: TimeInterval = 2 * 60 * 60 static let homeStores: TimeInterval = 5 * 60 } enum AppCacheKey { static let homeStoresLatestSnapshot = "home-stores.latest.snapshot" } final class AppContentCache: @unchecked Sendable { static let shared = AppContentCache() private struct Entry { let value: Any let expiry: Date } private var entries: [String: Entry] = [:] private let queue = DispatchQueue(label: "com.pedifoods.content-cache", qos: .userInitiated) private init() {} func value(for key: String, as type: T.Type = T.self) -> T? { queue.sync { guard let entry = entries[key] else { return nil } if entry.expiry <= Date() { entries.removeValue(forKey: key) return nil } return entry.value as? T } } func set(_ value: T, for key: String, ttl: TimeInterval) { queue.sync { entries[key] = Entry(value: value, expiry: Date().addingTimeInterval(ttl)) } } func invalidate(prefix: String? = nil) { queue.sync { guard let prefix, prefix.isEmpty == false else { entries.removeAll() return } let keys = entries.keys.filter { $0.hasPrefix(prefix) } for key in keys { entries.removeValue(forKey: key) } } } } final class AppImageCache: @unchecked Sendable { static let shared = AppImageCache() private struct Entry { let image: PlatformImage let expiry: Date } private var entries: [String: Entry] = [:] private let queue = DispatchQueue(label: "com.pedifoods.image-cache", qos: .userInitiated) private init() { configureURLCacheIfNeeded() } func image(for url: URL, ttl: TimeInterval, forceRefresh: Bool = false) async -> PlatformImage? { let key = url.absoluteString let now = Date() if forceRefresh == false { let cached = queue.sync { entries[key] } if let cached, cached.expiry > now { return cached.image } } var request = URLRequest(url: url) request.timeoutInterval = 20 request.cachePolicy = forceRefresh ? .reloadIgnoringLocalCacheData : .returnCacheDataElseLoad if forceRefresh == false, let diskCached = URLCache.shared.cachedResponse(for: request), let image = platformImage(from: diskCached.data) { queue.sync { entries[key] = Entry(image: image, expiry: now.addingTimeInterval(ttl)) } return image } do { let (data, response) = try await URLSession.shared.data(for: request) guard let image = platformImage(from: data) else { return nil } URLCache.shared.storeCachedResponse(CachedURLResponse(response: response, data: data), for: request) queue.sync { entries[key] = Entry(image: image, expiry: now.addingTimeInterval(ttl)) } return image } catch { return nil } } func invalidateAll() { queue.sync { entries.removeAll() } URLCache.shared.removeAllCachedResponses() } private func configureURLCacheIfNeeded() { let current = URLCache.shared let minMemoryCapacity = 64 * 1024 * 1024 let minDiskCapacity = 256 * 1024 * 1024 if current.memoryCapacity < minMemoryCapacity || current.diskCapacity < minDiskCapacity { URLCache.shared = URLCache(memoryCapacity: minMemoryCapacity, diskCapacity: minDiskCapacity) } } private func platformImage(from data: Data) -> PlatformImage? { UIImage(data: data) } }