From d6f9acca1149a1d29297c4338ffec311923a9735 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 19 May 2022 16:12:53 -0700 Subject: [PATCH] fetch items in background task --- apple/OmnivoreKit/Sources/App/Services.swift | 32 +++++---- .../App/Views/Home/HomeFeedViewModel.swift | 4 +- .../FetchLinkedItemsBackgroundTask.swift | 57 ++++++++++++++++ .../Queries/ArticleContentQuery.swift | 11 ++- .../Queries/LinkedItemIDsQuery.swift | 67 +++++++++++++++++++ .../InternalModels/InternalLinkedItem.swift | 1 - 6 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemIDsQuery.swift diff --git a/apple/OmnivoreKit/Sources/App/Services.swift b/apple/OmnivoreKit/Sources/App/Services.swift index 46f9dd7de..c12951763 100644 --- a/apple/OmnivoreKit/Sources/App/Services.swift +++ b/apple/OmnivoreKit/Sources/App/Services.swift @@ -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) + } + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index a099b971f..c11d66d2c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -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) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift new file mode 100644 index 000000000..939f3330e --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift @@ -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 = 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 = 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 [] + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index bbea73ead..ebb8a3bf8 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -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 = 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) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemIDsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemIDsQuery.swift new file mode 100644 index 000000000..f6dede0d3 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemIDsQuery.swift @@ -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 { + 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")) + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift index 7214897c9..5e39aa6b9 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift @@ -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 {