import AppKit /// In-memory thumbnail cache backed by `NSCache`. NSCache already evicts /// entries under memory pressure; the explicit `countLimit` and /// `totalCostLimit` here are the deterministic upper bounds the manager /// enforces so a runaway explore session can't grow the cache without /// limit. Both are configurable through `init` so tests can shrink the /// budget without depending on system-wide memory signals. class ImageCache { static let shared = ImageCache() /// Default cap on the number of cached images. Sixty-four entries is /// enough to cover an explore screen of thumbnails plus a screen of /// pre-fetched neighbours without keeping multiple megabytes of decoded /// pixel data resident indefinitely. static let defaultCountLimit = 64 /// Default cost budget in bytes. `NSImage` does not expose a precise /// pixel cost, so we estimate `width * height * 4` (RGBA) when callers /// know the source dimensions. The default of ~64 MiB keeps the cache /// under a tenth of a typical app's compressed memory allowance. static let defaultTotalCostLimit = 64 * 1024 * 1024 private let cache = NSCache() private let session: URLSession init( session: URLSession = .shared, countLimit: Int = ImageCache.defaultCountLimit, totalCostLimit: Int = ImageCache.defaultTotalCostLimit ) { self.session = session self.cache.countLimit = countLimit self.cache.totalCostLimit = totalCostLimit } func image(for url: URL) -> NSImage? { return cache.object(forKey: url.absoluteString as NSString) } func insertImage(_ image: NSImage, for url: URL) { cache.setObject(image, forKey: url.absoluteString as NSString, cost: estimatedCost(of: image)) } func fetchImage(url: URL) async throws -> NSImage { if let cached = image(for: url) { return cached } let (data, response) = try await session.data(from: url) guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else { throw WallhavenError.apiError("Image download failed") } guard let image = NSImage(data: data) else { throw WallhavenError.decodingError(NSError(domain: "ImageCache", code: -1, userInfo: nil)) } insertImage(image, for: url) return image } /// Estimated RGBA byte cost of `image`. Used by NSCache so eviction /// respects a real budget instead of just the entry count. Returns zero /// for images without pixel dimensions (e.g. vector PDFs) so the count /// limit alone bounds them. private func estimatedCost(of image: NSImage) -> Int { let size = image.size let width = Int(size.width.rounded()) let height = Int(size.height.rounded()) guard width > 0, height > 0 else { return 0 } return width * height * 4 } }