From 96b09fec8e04b476162a8e2176c0f259e7624417 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 16 Jun 2022 11:04:15 -0700 Subject: [PATCH] separate linkeditem network fetches from persistence functions --- .../App/Views/Home/HomeFeedViewModel.swift | 4 +- .../WebReader/WebReaderLoadingContainer.swift | 5 +- .../Sources/Models/DataModels/FeedItem.swift | 8 +- .../Public/LinkedItemLoading.swift | 43 +++++++++ ...ery.swift => LinkedItemNetworkQuery.swift} | 91 +++++++------------ .../InternalModels/InternalLinkedItem.swift | 9 +- 6 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift rename apple/OmnivoreKit/Sources/Services/DataService/Queries/{LibraryItemsQuery.swift => LinkedItemNetworkQuery.swift} (59%) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 2d104d0ee..837b41376 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -62,7 +62,7 @@ import Views Task { _ = try? await dataService.fetchViewer() } } - let queryResult = try? await dataService.fetchLinkedItems( + let queryResult = try? await dataService.loadLinkedItems( limit: 10, searchQuery: searchQuery, cursor: isRefresh ? nil : cursor @@ -81,7 +81,7 @@ import Views let newItems: [LinkedItem] = { var itemObjects = [LinkedItem]() dataService.viewContext.performAndWait { - itemObjects = queryResult.items.compactMap { dataService.viewContext.object(with: $0) as? LinkedItem } + itemObjects = queryResult.itemIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItem } } return itemObjects }() diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index adb28284f..45065ca63 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -64,11 +64,8 @@ import Utils return nil } - print("FETCHING", requestID, requestCount) - - if let objectID = try? await dataService.fetchLinkedItem(username: username, itemID: requestID) { + if let objectID = try? await dataService.loadLinkedItem(username: username, itemID: requestID) { if let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem { - print(" - FROM DATA SERVICE", linkedItem) return linkedItem } else { errorMessage = "Unable to fetch item." diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index 09b375545..765c9f329 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -2,12 +2,12 @@ import CoreData import Foundation import Utils -public struct HomeFeedData { // TODO: rename this - public let items: [NSManagedObjectID] +public struct LinkedItemQueryResult { + public let itemIDs: [NSManagedObjectID] public let cursor: String? - public init(items: [NSManagedObjectID], cursor: String?) { - self.items = items + public init(itemIDs: [NSManagedObjectID], cursor: String?) { + self.itemIDs = itemIDs self.cursor = cursor } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift new file mode 100644 index 000000000..b3db826aa --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift @@ -0,0 +1,43 @@ +import CoreData +import Foundation +import Models + +public extension DataService { + /// Requests `LinkedItem`s from the server and stores it in CoreData. + /// - Parameters: + /// - limit: max count of items + /// - searchQuery: search terms and filters + /// - cursor: cursor when loading batch for infinite list + /// - Returns: `LinkedItemQueryResult` (managed object IDs and an optional cursor) + func loadLinkedItems( + limit: Int, + searchQuery: String?, + cursor: String? + ) async throws -> LinkedItemQueryResult { + // Send offline changes to server before fetching items + try? await syncOfflineItemsWithServerIfNeeded() + + let fetchResult = try await fetchLinkedItems(limit: limit, searchQuery: searchQuery, cursor: cursor) + + guard let itemIDs = fetchResult.items.persist(context: backgroundContext) else { + throw BasicError.message(messageText: "CoreData error") + } + + return LinkedItemQueryResult(itemIDs: itemIDs, cursor: fetchResult.cursor) + } + + /// Requests a single `LinkedItem` from the server and stores it in CoreData + /// - Parameters: + /// - username: the Viewer's username + /// - itemID: id of item being requested + /// - Returns: The `NSManagedObjectID` of the `LinkedItem` + func loadLinkedItem(username: String, itemID: String) async throws -> NSManagedObjectID { + let item = try await fetchLinkedItem(username: username, itemID: itemID) + + guard let persistedItemID = [item].persist(context: backgroundContext)?.first else { + throw BasicError.message(messageText: "CoreData error") + } + + return persistedItemID + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift similarity index 59% rename from apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift rename to apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift index 99c9fcece..e5f614589 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LibraryItemsQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift @@ -3,22 +3,25 @@ import Foundation import Models import SwiftGraphQL -public extension DataService { +struct InternalLinkedItemQueryResult { + let items: [InternalLinkedItem] + let cursor: String? +} + +extension DataService { + /// Performs GraphQL request to fetch `InternalLinkedItem`s and a cursor value + /// - Parameters: + /// - limit: max number of items to return + /// - searchQuery: search query used by server to narrow search + /// - cursor: cursor to indicate batch cutoff + /// - Returns: `InternalLinkedItemQueryResult` or a `ContentFetchError` if request fails. func fetchLinkedItems( limit: Int, searchQuery: String?, cursor: String? - ) async throws -> HomeFeedData { - // Send offline changes to server before fetching items - try? await syncOfflineItemsWithServerIfNeeded() - - struct InternalHomeFeedData { - let items: [InternalLinkedItem] - let cursor: String? - } - + ) async throws -> InternalLinkedItemQueryResult { enum QueryResult { - case success(result: InternalHomeFeedData) + case success(result: InternalLinkedItemQueryResult) case error(error: String) } @@ -29,7 +32,7 @@ public extension DataService { }, articlesSuccess: .init { QueryResult.success( - result: InternalHomeFeedData( + result: InternalLinkedItemQueryResult( items: try $0.edges(selection: articleEdgeSelection.list), cursor: try $0.pageInfo(selection: Selection.PageInfo { try $0.endCursor() @@ -61,27 +64,29 @@ public extension DataService { let headers = networker.defaultHeaders return try await withCheckedThrowingContinuation { continuation in - send(query, to: path, headers: headers) { [weak self] queryResult in + send(query, to: path, headers: headers) { queryResult in guard let payload = try? queryResult.get() else { - continuation.resume(throwing: BasicError.message(messageText: "network error")) + continuation.resume(throwing: ContentFetchError.network) return } switch payload.data { case let .success(result: result): - if let context = self?.backgroundContext, let items = result.items.persist(context: context) { - continuation.resume(returning: HomeFeedData(items: items.map(\.objectID), cursor: result.cursor)) - } else { - continuation.resume(throwing: BasicError.message(messageText: "CoreData error")) - } - case .error: - continuation.resume(throwing: BasicError.message(messageText: "LinkedItem fetch error")) + continuation.resume(returning: result) + case let .error(error): + continuation.resume(throwing: ContentFetchError.unknown(description: error.description)) } } } } - func fetchLinkedItem(username: String, itemID: String) async throws -> NSManagedObjectID { + /// Performs GraphQL request to fetch a single `InternalLinkedItem` + /// - Parameters: + /// - username: the Viewer's username + /// - itemID: id of the item being requested + /// - Returns: Returns an `InternalLinkedItem` or throws a `ContentFetchError` if + /// request could not be completed + func fetchLinkedItem(username: String, itemID: String) async throws -> InternalLinkedItem { struct ArticleProps { let item: InternalLinkedItem } @@ -91,41 +96,13 @@ public extension DataService { case error(error: String) } - let articleSelection = Selection.Article { - InternalLinkedItem( - id: try $0.id(), - title: try $0.title(), - createdAt: try $0.createdAt().value ?? Date(), - savedAt: try $0.savedAt().value ?? Date(), - readAt: try $0.readAt()?.value, - updatedAt: try $0.updatedAt().value ?? Date(), - state: try $0.state()?.rawValue ?? "SUCCEEDED", - readingProgress: try $0.readingProgressPercent(), - readingProgressAnchor: try $0.readingProgressAnchorIndex(), - imageURLString: try $0.image(), - onDeviceImageURLString: nil, - documentDirectoryPath: nil, - pageURLString: try $0.url(), - descriptionText: try $0.description(), - publisherURLString: try $0.originalArticleUrl(), - siteName: try $0.siteName(), - author: try $0.author(), - publishDate: try $0.publishedAt()?.value, - slug: try $0.slug(), - isArchived: try $0.isArchived(), - contentReader: try $0.contentReader().rawValue, - originalHtml: nil, - labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? [] - ) - } - let selection = Selection { try $0.on( articleError: .init { QueryResult.error(error: try $0.errorCodes().description) }, articleSuccess: .init { - QueryResult.success(result: try $0.article(selection: articleSelection)) + QueryResult.success(result: try $0.article(selection: libraryArticleSelection)) } ) } @@ -139,20 +116,16 @@ public extension DataService { let headers = networker.defaultHeaders return try await withCheckedThrowingContinuation { continuation in - send(query, to: path, headers: headers) { [weak self] queryResult in + send(query, to: path, headers: headers) { queryResult in guard let payload = try? queryResult.get() else { continuation.resume(throwing: ContentFetchError.network) return } switch payload.data { case let .success(result: result): - if let context = self?.backgroundContext, let item = [result].persist(context: context)?.first { - continuation.resume(returning: item.objectID) - } else { - continuation.resume(throwing: BasicError.message(messageText: "CoreData error")) - } - case .error: - continuation.resume(throwing: BasicError.message(messageText: "LinkedItem fetch error")) + continuation.resume(returning: result) + case let .error(error): + continuation.resume(throwing: ContentFetchError.unknown(description: error.description)) } } } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift index dde255691..115f6aa03 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift @@ -70,7 +70,7 @@ struct InternalLinkedItem { } extension Sequence where Element == InternalLinkedItem { - func persist(context: NSManagedObjectContext) -> [LinkedItem]? { + func persist(context: NSManagedObjectContext) -> [NSManagedObjectID]? { var linkedItems: [LinkedItem]? context.performAndWait { linkedItems = map { $0.asManagedObject(inContext: context) } @@ -83,7 +83,12 @@ extension Sequence where Element == InternalLinkedItem { print("Failed to save LinkedItems: \(error.localizedDescription)") } } - return linkedItems + + if let linkedItems = linkedItems { + return linkedItems.map(\.objectID) + } else { + return nil + } } }