Files
jarvis/Jarvis/Store/CacheMaintenance.swift
Robin Kutesa 798f46fbb1
Some checks failed
CI / Build · macos-14 · Debug (push) Has been cancelled
CI / Build · macos-15 · Debug (push) Has been cancelled
CI / Build · macos-14 · Release (push) Has been cancelled
CI / Build · macos-15 · Release (push) Has been cancelled
Align cache eviction to the backend aging model
Retention now mirrors the backend: cached stories age out at RECENT_HOURS (72h,
the active feed window), seen/read markers at 96h (window + buffer), and cached
article bodies at 14 days (raw-article retention). Saved bookmarks stay protected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:31:52 +03:00

64 lines
2.9 KiB
Swift

// 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
// Aligned to the backend's aging model:
// RECENT_HOURS = 72 active feed window; drop cached stories past it
// raw-article retention ~14 days match for cached article bodies
private static let recentHours = 72.0 // backend active window (story list)
private static let markerHours = 96.0 // seen/read markers: 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<CachedArticle>(predicate: #Predicate { $0.cachedAt < articleCutoff })) {
for a in arts where !saved.contains(a.storyId) { context.delete(a) }
}
if let stories = try? context.fetch(
FetchDescriptor<CachedStory>(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<CachedArticle>()) {
for a in arts where !saved.contains(a.storyId) { context.delete(a) }
}
if let stories = try? context.fetch(FetchDescriptor<CachedStory>()) {
for s in stories where !saved.contains(s.id) { context.delete(s) }
}
try? context.save()
}
private static func savedIDs(_ context: ModelContext) -> Set<String> {
Set((try? context.fetch(FetchDescriptor<SavedStory>()))?.map(\.id) ?? [])
}
}