// CacheMaintenance.swift // Jarvis — bounds the on-device SwiftData caches so they don't grow forever. // Saved (bookmarked) stories are always protected. import Foundation import SwiftData @MainActor enum CacheMaintenance { private static let hour: TimeInterval = 3_600 private static let day: TimeInterval = 86_400 private static let recentHours = 84.0 // 3.5 days: survives weekends private static let markerHours = 96.0 // 4 days: active window + buffer private static let articleDays = 14.0 // cached article bodies (backend article retention) /// Age-based eviction. Run on launch. Cheap; protects saved stories. static func prune(_ context: ModelContext) { let now = Date() let seenCutoff = now.addingTimeInterval(-markerHours * hour) let readCutoff = now.addingTimeInterval(-markerHours * hour) let articleCutoff = now.addingTimeInterval(-articleDays * day) let storyCutoff = now.addingTimeInterval(-recentHours * hour) // Markers: simple age batch deletes. try? context.delete(model: SeenStory.self, where: #Predicate { $0.seenAt < seenCutoff }) try? context.delete(model: ReadStory.self, where: #Predicate { $0.readAt < readCutoff }) // Offline content: drop old entries that aren't saved. let saved = savedIDs(context) if let arts = try? context.fetch( FetchDescriptor(predicate: #Predicate { $0.cachedAt < articleCutoff })) { for a in arts where !saved.contains(a.storyId) { context.delete(a) } } if let stories = try? context.fetch( FetchDescriptor(predicate: #Predicate { $0.cachedAt < storyCutoff })) { for s in stories where !saved.contains(s.id) { context.delete(s) } } try? context.save() } /// Manual "Clear cache" — wipes offline stories/articles + seen/read state, /// keeps your saved bookmarks (and their articles). static func clear(_ context: ModelContext) { try? context.delete(model: SeenStory.self) try? context.delete(model: ReadStory.self) let saved = savedIDs(context) if let arts = try? context.fetch(FetchDescriptor()) { for a in arts where !saved.contains(a.storyId) { context.delete(a) } } if let stories = try? context.fetch(FetchDescriptor()) { for s in stories where !saved.contains(s.id) { context.delete(s) } } try? context.save() } private static func savedIDs(_ context: ModelContext) -> Set { Set((try? context.fetch(FetchDescriptor()))?.map(\.id) ?? []) } }