From a1feffaf036944ba1021340f1293861e3a4c612c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 30 Mar 2022 16:39:51 -0700 Subject: [PATCH 1/3] Prefetch page content on iOS --- .../App/Views/Home/HomeFeedViewModel.swift | 3 ++ .../Views/WebReader/WebReaderViewModel.swift | 10 ++++++ .../Sources/Models/ArticleContent.swift | 8 +++++ .../Services/DataService/DataService.swift | 32 +++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index e0ebabb8d..6a917f435 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -76,6 +76,9 @@ final class HomeFeedViewModel: ObservableObject { if thisSearchIdx > 0, thisSearchIdx <= self?.receivedIdx ?? 0 { return } + + dataService.prefetchPages(items: result.items) + self?.items = isRefresh ? result.items : (self?.items ?? []) + result.items self?.isLoading = false self?.receivedIdx = thisSearchIdx diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 1dd5aa8dd..f111608c9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -28,6 +28,15 @@ final class WebReaderViewModel: ObservableObject { guard let viewer = dataService.currentViewer else { return } + if let content = dataService.pageCache.object(forKey: NSString(string: slug)) { + print("RETRIEVED FORM CACHE", slug) + isLoading = false + articleContent = content.value + return + } else { + print("MISSED CACHE", slug) + } + dataService.articleContentPublisher(username: viewer.username, slug: slug).sink( receiveCompletion: { [weak self] completion in guard case .failure = completion else { return } @@ -35,6 +44,7 @@ final class WebReaderViewModel: ObservableObject { }, receiveValue: { [weak self] articleContent in self?.articleContent = articleContent + dataService.pageCache.setObject(CachedPageContent(articleContent), forKey: NSString(string: slug)) } ) .store(in: &subscriptions) diff --git a/apple/OmnivoreKit/Sources/Models/ArticleContent.swift b/apple/OmnivoreKit/Sources/Models/ArticleContent.swift index bc55cf570..7a3d5b0e7 100644 --- a/apple/OmnivoreKit/Sources/Models/ArticleContent.swift +++ b/apple/OmnivoreKit/Sources/Models/ArticleContent.swift @@ -1,5 +1,13 @@ import Foundation +public class CachedPageContent: NSObject { + public let value: ArticleContent + + public init(_ content: ArticleContent) { + self.value = content + } +} + public struct ArticleContent { public let htmlContent: String public let highlights: [Highlight] diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 12deb1da3..aca49bdfa 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -1,6 +1,13 @@ +import Combine import Foundation import Models +public class CacheManager: NSObject, NSCacheDelegate { + public func cache(_: NSCache, willEvictObject obj: Any) { + print("evicting object", obj) + } +} + public final class DataService: ObservableObject { public static var registerIntercomUser: ((String) -> Void)? public static var showIntercomMessenger: (() -> Void)? @@ -9,12 +16,37 @@ public final class DataService: ObservableObject { public internal(set) var currentViewer: Viewer? let networker: Networker + public let pageCache = NSCache() + let pageCacheQueue = DispatchQueue.global(qos: .background) + let highlightsCache = NSCache() let highlightsCacheQueue = DispatchQueue(label: "app.omnivore.highlights.cache.queue", attributes: .concurrent) + let cacheManager: CacheManager + var subscriptions = Set() + public init(appEnvironment: AppEnvironment, networker: Networker) { self.appEnvironment = appEnvironment self.networker = networker + self.cacheManager = CacheManager() + pageCache.delegate = cacheManager + } + + public func prefetchPages(items: [FeedItem]) { + print("prefetching items", items, "cost limit", pageCache.countLimit) + + guard let viewer = currentViewer else { return } + + for item in items { + let slug = item.slug + articleContentPublisher(username: viewer.username, slug: slug).sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] articleContent in + self?.pageCache.setObject(CachedPageContent(articleContent), forKey: NSString(string: slug)) + } + ) + .store(in: &subscriptions) + } } public func clearHighlights() { From 183776c7912963bae72d5f8276498c65c05c06f6 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 30 Mar 2022 17:09:37 -0700 Subject: [PATCH 2/3] Reduce the reader overly length now that items are precached --- .../Sources/App/Views/WebReader/WebReaderContainer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 503f67417..7cc5bf938 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -204,7 +204,7 @@ struct WebReaderContainerView: View { Color.systemBackground .transition(.opacity) .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { withAnimation(.linear(duration: 0.2)) { showOverlay = false } From 13cd668cdd3644caa9ab6c40e8ae4c9f13231a4c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 31 Mar 2022 15:04:22 -0700 Subject: [PATCH 3/3] Add invalidation when highlights are added to items --- .../Views/WebReader/WebReaderViewModel.swift | 18 +++--- .../Sources/Models/ArticleContent.swift | 4 +- .../Services/DataService/DataService.swift | 56 +++++++++++++------ 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index f111608c9..5764bac83 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -21,20 +21,18 @@ final class WebReaderViewModel: ObservableObject { @Published var isLoading = false @Published var articleContent: ArticleContent? + var slug: String? var subscriptions = Set() func loadContent(dataService: DataService, slug: String) { + self.slug = slug isLoading = true guard let viewer = dataService.currentViewer else { return } - if let content = dataService.pageCache.object(forKey: NSString(string: slug)) { - print("RETRIEVED FORM CACHE", slug) - isLoading = false - articleContent = content.value - return - } else { - print("MISSED CACHE", slug) + if let content = dataService.pageFromCache(slug: slug) { + articleContent = content + // continue to load from the web if possible } dataService.articleContentPublisher(username: viewer.username, slug: slug).sink( @@ -44,7 +42,7 @@ final class WebReaderViewModel: ObservableObject { }, receiveValue: { [weak self] articleContent in self?.articleContent = articleContent - dataService.pageCache.setObject(CachedPageContent(articleContent), forKey: NSString(string: slug)) + dataService.pageCache.setObject(CachedPageContent(slug, articleContent), forKey: NSString(string: slug)) } ) .store(in: &subscriptions) @@ -177,12 +175,16 @@ final class WebReaderViewModel: ObservableObject { switch actionID { case "deleteHighlight": + dataService.invalidateCachedPage(slug: slug) deleteHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) case "createHighlight": + dataService.invalidateCachedPage(slug: slug) createHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) case "mergeHighlight": + dataService.invalidateCachedPage(slug: slug) mergeHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) case "updateHighlight": + dataService.invalidateCachedPage(slug: slug) updateHighlight(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) case "articleReadingProgress": updateReadingProgress(messageBody: messageBody, replyHandler: replyHandler, dataService: dataService) diff --git a/apple/OmnivoreKit/Sources/Models/ArticleContent.swift b/apple/OmnivoreKit/Sources/Models/ArticleContent.swift index 7a3d5b0e7..032ccf434 100644 --- a/apple/OmnivoreKit/Sources/Models/ArticleContent.swift +++ b/apple/OmnivoreKit/Sources/Models/ArticleContent.swift @@ -1,9 +1,11 @@ import Foundation public class CachedPageContent: NSObject { + public let slug: String public let value: ArticleContent - public init(_ content: ArticleContent) { + public init(_ slug: String, _ content: ArticleContent) { + self.slug = slug self.value = content } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index aca49bdfa..12e72e406 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -4,7 +4,10 @@ import Models public class CacheManager: NSObject, NSCacheDelegate { public func cache(_: NSCache, willEvictObject obj: Any) { - print("evicting object", obj) + // This is just used for debugging + if let content = obj as? CachedPageContent { + print("evicting page from cache", content.slug) + } } } @@ -32,23 +35,6 @@ public final class DataService: ObservableObject { pageCache.delegate = cacheManager } - public func prefetchPages(items: [FeedItem]) { - print("prefetching items", items, "cost limit", pageCache.countLimit) - - guard let viewer = currentViewer else { return } - - for item in items { - let slug = item.slug - articleContentPublisher(username: viewer.username, slug: slug).sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] articleContent in - self?.pageCache.setObject(CachedPageContent(articleContent), forKey: NSString(string: slug)) - } - ) - .store(in: &subscriptions) - } - } - public func clearHighlights() { highlightsCache.removeAllObjects() } @@ -62,3 +48,37 @@ public final class DataService: ObservableObject { } } } + +public extension DataService { + func prefetchPages(items: [FeedItem]) { + print("prefetching pages") + guard let viewer = currentViewer else { return } + + for item in items { + let slug = item.slug + articleContentPublisher(username: viewer.username, slug: slug).sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] articleContent in + self?.pageCache.setObject(CachedPageContent(slug, articleContent), forKey: NSString(string: slug)) + } + ) + .store(in: &subscriptions) + } + } + + func pageFromCache(slug: String) -> ArticleContent? { + if let content = pageCache.object(forKey: NSString(string: slug)) { + print("cache hit", slug) + return content.value + } else { + print("cache miss", slug) + } + return nil + } + + func invalidateCachedPage(slug: String?) { + if let slug = slug { + pageCache.removeObject(forKey: NSString(string: slug)) + } + } +}