// BackgroundRefreshManager.swift // Jarvis — background story cache refresh. Fires via BGAppRefreshTask every ~30 min, // fetches the latest feed, and upserts CachedStory rows so the SwiftData cache is // warm before the user opens the app. import Foundation import BackgroundTasks import SwiftData let jarvisFeedRefreshID = "com.kisani.jarvis.feed.refresh" enum BackgroundRefreshManager { // Injected once from JarvisApp so the BG task can open a context on the same store. static var container: ModelContainer? // MARK: - Task handler static func handleFeedRefresh(task: BGAppRefreshTask) { // Reschedule first — if the task is killed we still get another run later. scheduleFeedRefresh() guard let container else { task.setTaskCompleted(success: false) return } let work = Task { await fetchAndCache(container: container) } task.expirationHandler = { work.cancel() task.setTaskCompleted(success: false) } Task { _ = await work.result task.setTaskCompleted(success: !work.isCancelled) } } // MARK: - Core fetch + upsert private static func fetchAndCache(container: ModelContainer) async { do { let page = try await APIClient.shared.fetchStories(limit: 40) let context = ModelContext(container) for story in page.data { let id = story.id if let existing = (try? context.fetch( FetchDescriptor(predicate: #Predicate { $0.id == id })))?.first { // Refresh signal score and freshness on already-cached rows. existing.signalScore = story.signalScore existing.sourceCount = story.sourceCount existing.updatedAt = story.updatedAt } else { context.insert(CachedStory(from: story)) } } try? context.save() } catch { // Network unavailable in background — fail silently, next run will catch up. } } // MARK: - Schedule static func scheduleFeedRefresh() { let req = BGAppRefreshTaskRequest(identifier: jarvisFeedRefreshID) // iOS enforces a minimum interval (~15 min); 30 min is a reasonable target. req.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) try? BGTaskScheduler.shared.submit(req) } }