From c74911ea86750a9261d234385cd6174e1d33c472 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 16 Nov 2022 13:29:59 +0800 Subject: [PATCH] Improve how we handle loading the initial items on iOS --- .../App/Views/Home/HomeFeedViewModel.swift | 73 ++++++++++++------- .../Sources/Models/DataModels/FeedItem.swift | 10 +++ .../Public/LinkedItemContentLoading.swift | 11 ++- .../Public/LinkedItemLoading.swift | 16 ++-- 4 files changed, 73 insertions(+), 37 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 55ff72e95..418db4b50 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -91,33 +91,45 @@ import Views items.insert(item, at: 0) } - func loadItems(dataService: DataService, audioController: AudioController, isRefresh: Bool) async { - let syncStartTime = Date() - let thisSearchIdx = searchIdx - searchIdx += 1 - - isLoading = true - showLoadingBar = true - + func loadCurrentViewer(dataService: DataService) async { // Cache the viewer if dataService.currentViewer == nil { - Task { _ = try? await dataService.fetchViewer() } + _ = try? await dataService.fetchViewer() } + } - // Fetch labels if none are available locally + func loadLabels(dataService: DataService) async { let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() fetchRequest.fetchLimit = 1 if (try? dataService.viewContext.count(for: fetchRequest)) == 0 { _ = try? await dataService.labels() } + } - // Sync items if necessary + func syncItems(dataService: DataService, syncStartTime: Date) async { let lastSyncDate = dateFormatter.date(from: lastItemSyncTime) ?? Date(timeIntervalSinceReferenceDate: 0) let syncResult = try? await dataService.syncLinkedItems(since: lastSyncDate, cursor: nil) if syncResult != nil { lastItemSyncTime = dateFormatter.string(from: syncStartTime) } + // If possible start prefetching new pages in the background + if let itemIDs = syncResult?.updatedItemIDs, + let username = dataService.currentViewer?.username, + itemIDs.count > 0 { + Task { + await dataService.prefetchPages(itemIDs: itemIDs.map { $0.uriRepresentation().absoluteString }, username: username) + } + } + } + + func loadSearchQuery(dataService: DataService, isRefresh: Bool) async { + let thisSearchIdx = searchIdx + searchIdx += 1 + + if thisSearchIdx > 0, thisSearchIdx <= receivedIdx { + return + } let queryResult = try? await dataService.loadLinkedItems( limit: 10, @@ -125,15 +137,6 @@ import Views cursor: isRefresh ? nil : cursor ) - // Search results aren't guaranteed to return in order so this - // will discard old results that are returned while a user is typing. - // For example if a user types 'Canucks', often the search results - // for 'C' are returned after 'Canucks' because it takes the backend - // much longer to compute. - if thisSearchIdx > 0, thisSearchIdx <= receivedIdx { - return - } - if let queryResult = queryResult { let newItems: [LinkedItem] = { var itemObjects = [LinkedItem]() @@ -159,17 +162,33 @@ import Views cursor = queryResult.cursor if let username = dataService.currentViewer?.username { await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID), username: username) - // Only preload the first item in the list. We are doing this during the beta - // because it will kick off the user's future items being automatically transcribed. - // This happens because when an article is saved, we check if the user has a recent - // listen. If they do, we will automatically transcribe their message. - if let first = newItems.filter({ !$0.isPDF }).first?.id { - _ = await audioController.preload(itemIDs: [first]) - } } } else { updateFetchController(dataService: dataService) } + } + + func loadItems(dataService: DataService, audioController _: AudioController, isRefresh: Bool) async { + let syncStartTime = Date() + let start = CFAbsoluteTimeGetCurrent() + + isLoading = true + showLoadingBar = true + + _ = await [ + loadCurrentViewer(dataService: dataService), + loadLabels(dataService: dataService), + syncItems(dataService: dataService, syncStartTime: syncStartTime) + ] + print("fetched all prelim data", CFAbsoluteTimeGetCurrent() - start) + + if searchTerm.replacingOccurrences(of: " ", with: "").isEmpty { + updateFetchController(dataService: dataService) + } else { + print("PERFORMING SEARCH FOR ITEMS") + await loadSearchQuery(dataService: dataService, isRefresh: isRefresh) + } + print("loaded the linked items", CFAbsoluteTimeGetCurrent() - start) isLoading = false showLoadingBar = false diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index 18a464152..8d2e54aa6 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -12,6 +12,16 @@ public struct LinkedItemQueryResult { } } +public struct LinkedItemSyncResult { + public let updatedItemIDs: [String] + public let cursor: String? + + public init(updatedItemIDs: [String], cursor: String?) { + self.updatedItemIDs = updatedItemIDs + self.cursor = cursor + } +} + public struct LinkedItemAudioProperties { public let itemID: String public let objectID: NSManagedObjectID diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift index 3af0f0737..a4bc86ea1 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemContentLoading.swift @@ -5,9 +5,14 @@ import Utils public extension DataService { func prefetchPages(itemIDs: [String], username: String) async { - // TODO: make this concurrent - for itemID in itemIDs { - await prefetchPage(pendingLink: PendingLink(itemID: itemID, retryCount: 1), username: username) + await withTaskGroup(of: Void.self) { group in + print(" PREFETCHING PAGES: ", itemIDs) + for itemID in itemIDs { + group.addTask { + await self.prefetchPage(pendingLink: PendingLink(itemID: itemID, retryCount: 1), username: username) + } + } + await group.waitForAll() } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift index f1e7b517d..ea5d8ff0e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift @@ -14,8 +14,8 @@ public extension DataService { func syncLinkedItems( since date: Date, cursor: String?, - previousQueryResult: LinkedItemQueryResult? = nil - ) async throws -> LinkedItemQueryResult { + previousQueryResult: LinkedItemSyncResult? = nil + ) async throws -> LinkedItemSyncResult? { if previousQueryResult == nil { // Send offline changes to server before fetching items // only on the first call of this function @@ -26,16 +26,18 @@ public extension DataService { LinkedItem.deleteItems(ids: fetchResult.deletedItemIDs, context: backgroundContext) - guard let itemIDs = fetchResult.items.persist(context: backgroundContext) else { + if fetchResult.items.persist(context: backgroundContext) == nil { throw BasicError.message(messageText: "CoreData error") } - let result = LinkedItemQueryResult( - itemIDs: itemIDs + (previousQueryResult?.itemIDs ?? []), + + let prev = previousQueryResult?.updatedItemIDs ?? [] + let result = LinkedItemSyncResult( + updatedItemIDs: prev + fetchResult.items.map { $0.id }, cursor: fetchResult.cursor ) - if fetchResult.hasMoreItems, (previousQueryResult?.itemIDs.count ?? 0) < 200 { + if fetchResult.hasMoreItems, (previousQueryResult?.updatedItemIDs.count ?? 0) < 200 { return try await syncLinkedItems( since: date, cursor: fetchResult.cursor, @@ -58,7 +60,7 @@ public extension DataService { cursor: String? ) async throws -> LinkedItemQueryResult { // Send offline changes to server before fetching items - try? await syncOfflineItemsWithServerIfNeeded() + // try? await syncOfflineItemsWithServerIfNeeded() let fetchResult = try await fetchLinkedItems(limit: limit, searchQuery: searchQuery, cursor: cursor)