Registers com.kisani.jarvis.feed.refresh as a BGAppRefreshTask. On each fire it fetches the latest 40 stories and upserts CachedStory rows so the SwiftData cache is warm before the user opens the app. Already-cached rows have signalScore / sourceCount refreshed in-place. Single ModelContainer owned by JarvisApp and shared via BackgroundRefreshManager.container to avoid multi-container conflicts. BG task ID registered in AppDelegate and declared in project.yml. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
2.5 KiB
Swift
78 lines
2.5 KiB
Swift
// 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<CachedStory>(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)
|
|
}
|
|
}
|