fetch items in background task

This commit is contained in:
Satindar Dhillon 2022-05-19 16:12:53 -07:00
parent 2eeec7cc77
commit d6f9acca11
6 changed files with 150 additions and 22 deletions

View file

@ -27,7 +27,7 @@ extension Services {
if let task = task as? BGAppRefreshTask {
EventTracker.trackForDebugging("executing app.omnivore.fetchLinkedItems bg task")
logger.debug("in background task register closure")
startBackgroundFetch(task: task)
performBackgroundFetch(task: task)
}
}
}
@ -45,7 +45,8 @@ extension Services {
}
}
static func startBackgroundFetch(task: BGAppRefreshTask) {
static func performBackgroundFetch(task: BGAppRefreshTask) {
Services.logger.debug("starting background fetch")
scheduleBackgroundFetch()
let services = Services()
@ -54,22 +55,25 @@ extension Services {
logger.debug("handling background fetch expiration")
}
services.peformBackgroundFetch()
EventTracker.trackForDebugging("background fetch task completed successfully")
task.setTaskCompleted(success: true)
}
func peformBackgroundFetch() {
Services.logger.debug("starting background fetch")
guard authenticator.hasValidAuthToken else {
EventTracker.trackForDebugging("background fetch failed: user does not habe a valid auth token")
guard services.authenticator.hasValidAuthToken else {
EventTracker.trackForDebugging("background fetch failed: user does not have a valid auth token")
Services.logger.debug("background fetch failed: user does not habe a valid auth token")
task.setTaskCompleted(success: false)
return
}
Services.logger.debug("ayo this is a background task!")
// TODO: perform tasks using data service
Task {
do {
try await services.dataService.fetchLinkedItemsBackgroundTask()
logger.debug("fetch complete")
EventTracker.trackForDebugging("background fetch task completed successfully")
task.setTaskCompleted(success: true)
} catch {
logger.debug("fetch failed")
EventTracker.trackForDebugging("background fetch task failed")
task.setTaskCompleted(success: false)
}
}
}
}

View file

@ -98,7 +98,9 @@ import Views
isLoading = false
receivedIdx = thisSearchIdx
cursor = queryResult.cursor
await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID))
if let username = dataService.currentViewer?.username {
await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID), username: username)
}
} else {
updateFetchController(dataService: dataService)
}

View file

@ -0,0 +1,57 @@
import CoreData
import Foundation
import Models
import SwiftGraphQL
extension DataService {
public func fetchLinkedItemsBackgroundTask() async throws {
// Query the server for item IDs and compare against CoreData
// to see what we're missing
let missingItemIds = try await fetchMissingItemIDs()
guard !missingItemIds.isEmpty else { return }
let username: String? = await backgroundContext.perform(schedule: .immediate) {
let fetchRequest: NSFetchRequest<Models.Viewer> = Viewer.fetchRequest()
fetchRequest.fetchLimit = 1 // we should only have one viewer saved
return try? self.backgroundContext.fetch(fetchRequest).first?.username
}
guard let username = username else {
throw BasicError.message(messageText: "could not retrieve username from core data")
}
// Fetch the items
for itemID in missingItemIds { // TOOD: run these in parallel
logger.debug("fetching item with ID: \(itemID)")
_ = try await articleContent(username: username, itemID: itemID, useCache: false)
logger.debug("done fetching item with ID: \(itemID)")
}
}
func fetchMissingItemIDs(previouslyFetchedIDs: [String] = [], cursor: String? = nil) async throws -> [String] {
logger.debug("fetching more IDS: \(cursor ?? "no cursor")")
let maxItemCount = 30
let fetchResult = try await fetchLinkedItemIDs(limit: 10, cursor: cursor)
let newItemsToFetch = await itemsNotInStore(from: fetchResult.itemIDs)
let itemsToFetch = previouslyFetchedIDs + newItemsToFetch
if newItemsToFetch.isEmpty || itemsToFetch.count > maxItemCount || fetchResult.cursor == nil {
return itemsToFetch
} else {
return try await fetchMissingItemIDs(previouslyFetchedIDs: itemsToFetch, cursor: fetchResult.cursor)
}
}
func itemsNotInStore(from itemIDs: [String]) async -> [String] {
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id IN %@", itemIDs)
return await backgroundContext.perform(schedule: .immediate) {
if let foundIDs = (try? self.backgroundContext.fetch(fetchRequest).map(\.unwrappedID)) {
return itemIDs.filter { !foundIDs.contains($0) }
} else {
return []
}
}
}
}

View file

@ -9,9 +9,9 @@ extension DataService {
let retryCount: Int
}
public func prefetchPages(itemIDs: [String]) async {
guard let username = currentViewer?.username else { return }
public func prefetchPages(itemIDs: [String], username: String) async {
// TODO: make this concurrent
// TODO: make a non-pending page option for BG tasks
for itemID in itemIDs {
await prefetchPage(pendingLink: PendingLink(itemID: itemID, retryCount: 1), username: username)
}
@ -183,9 +183,8 @@ extension DataService {
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %@", item.id)
let linkedItem = try? self.backgroundContext.fetch(fetchRequest).first
guard let linkedItem = linkedItem else { return }
let existingItem = try? self.backgroundContext.fetch(fetchRequest).first
let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext)
let highlightObjects = highlights.map {
$0.asManagedObject(context: self.backgroundContext)

View file

@ -0,0 +1,67 @@
import Foundation
import Models
import SwiftGraphQL
extension DataService {
struct LinkedItemIDFetchResult {
let itemIDs: [String]
let cursor: String?
}
func fetchLinkedItemIDs(limit: Int, cursor: String?) async throws -> LinkedItemIDFetchResult {
enum QueryResult {
case success(result: LinkedItemIDFetchResult)
case error(error: String)
}
let articleIDSelection = Selection.SearchItemEdge {
try $0.node(selection: Selection.SearchItem { try $0.id() })
}
let selection = Selection<QueryResult, Unions.SearchResult> {
try $0.on(
searchError: .init {
QueryResult.error(error: try $0.errorCodes().description)
},
searchSuccess: .init {
QueryResult.success(
result: LinkedItemIDFetchResult(
itemIDs: try $0.edges(selection: articleIDSelection.list),
cursor: try $0.pageInfo(selection: Selection.PageInfo {
try $0.endCursor()
})
)
)
}
)
}
let query = Selection.Query {
try $0.search(
after: OptionalArgument(cursor),
first: OptionalArgument(limit),
query: OptionalArgument(nil),
selection: selection
)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return try await withCheckedThrowingContinuation { continuation in
send(query, to: path, headers: headers) { queryResult in
guard let payload = try? queryResult.get() else {
continuation.resume(throwing: BasicError.message(messageText: "network error"))
return
}
switch payload.data {
case let .success(result: result):
continuation.resume(returning: result)
case .error:
continuation.resume(throwing: BasicError.message(messageText: "LinkedItem fetch error"))
}
}
}
}
}

View file

@ -54,7 +54,6 @@ struct InternalLinkedItem {
}
extension Sequence where Element == InternalLinkedItem {
// TODO: -optimization use batch update?
func persist(context: NSManagedObjectContext) -> [LinkedItem]? {
var linkedItems: [LinkedItem]?
context.performAndWait {