mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
move function to load content by requestID into data service
This commit is contained in:
parent
e399a5e185
commit
1cdfe4a439
7 changed files with 263 additions and 152 deletions
|
|
@ -10,112 +10,8 @@ import Utils
|
|||
@Published var errorMessage: String?
|
||||
|
||||
func loadItem(dataService: DataService, requestID: String) async {
|
||||
let username: String? = await {
|
||||
if let cachedUsername = dataService.currentViewer?.username {
|
||||
return cachedUsername
|
||||
}
|
||||
|
||||
if let viewerObjectID = try? await dataService.fetchViewer() {
|
||||
let viewer = dataService.viewContext.object(with: viewerObjectID) as? Viewer
|
||||
return viewer?.unwrappedUsername
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
guard let username = username else { return }
|
||||
|
||||
let existing = existingItemOrItemId(dataService: dataService, requestID: requestID)
|
||||
if let existingItem = existing.existingItem, existingItem.isReadyToRead {
|
||||
item = existingItem
|
||||
return
|
||||
}
|
||||
|
||||
// If the page was locally created, make sure they are synced before we pull content
|
||||
await dataService.syncUnsyncedArticleContent(itemID: existing.itemID)
|
||||
|
||||
// Fetch the item and it's content
|
||||
let item = await fetchLinkedItem(dataService: dataService, requestID: existing.itemID, username: username)
|
||||
if let item = item, let itemID = item.id {
|
||||
do {
|
||||
let articleContent = try await dataService.loadArticleContentWithRetries(itemID: itemID, username: username, requestCount: 0)
|
||||
// We've fetched the article content, now reload the item from core data
|
||||
if let linkedItem = dataService.viewContext.object(with: item.objectID) as? LinkedItem {
|
||||
self.item = linkedItem
|
||||
} else {
|
||||
self.item = nil
|
||||
}
|
||||
} catch {
|
||||
self.item = nil
|
||||
}
|
||||
} else {
|
||||
self.item = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchLinkedItem(
|
||||
dataService: DataService,
|
||||
requestID: String,
|
||||
username: String,
|
||||
requestCount: Int = 1
|
||||
) async -> LinkedItem? {
|
||||
guard requestCount < 7 else {
|
||||
errorMessage = "Unable to fetch item."
|
||||
return nil
|
||||
}
|
||||
|
||||
if let objectID = try? await dataService.loadLinkedItem(username: username, itemID: requestID) {
|
||||
if let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem {
|
||||
return linkedItem
|
||||
} else {
|
||||
errorMessage = "Unable to fetch item."
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retry on error
|
||||
do {
|
||||
let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000)
|
||||
try await Task.sleep(nanoseconds: retryDelayInNanoSeconds)
|
||||
|
||||
let existing = existingItemOrItemId(dataService: dataService, requestID: requestID)
|
||||
if let existingItem = existing.existingItem, existingItem.isReadyToRead {
|
||||
print(" - FROM CORE DATA SERVICE", existingItem)
|
||||
return existingItem
|
||||
}
|
||||
|
||||
let result = await fetchLinkedItem(
|
||||
dataService: dataService,
|
||||
requestID: existing.itemID,
|
||||
username: username,
|
||||
requestCount: requestCount + 1
|
||||
)
|
||||
if let result = result {
|
||||
return result
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Unable to fetch item."
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func existingItemOrItemId(dataService: DataService, requestID: String) -> (existingItem: LinkedItem?, itemID: String) {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "createdId == %@ OR id == %@", requestID, requestID)
|
||||
if let existingItem = try? dataService.viewContext.fetch(fetchRequest).first {
|
||||
// If the existing item is synced, we can use it
|
||||
if let itemID = existingItem.id, existingItem.serverSyncStatus == ServerSyncStatus.isNSync.rawValue {
|
||||
item = existingItem
|
||||
return (existingItem: item, itemID: itemID)
|
||||
}
|
||||
|
||||
// If the existing item is not synced, we might have an updated request id
|
||||
if let existingID = existingItem.id {
|
||||
return (existingItem: nil, itemID: existingID)
|
||||
}
|
||||
}
|
||||
|
||||
return (existingItem: nil, itemID: requestID)
|
||||
guard let objectID = try? await dataService.loadItemContentUsingRequestID(requestID: requestID) else { return }
|
||||
item = dataService.viewContext.object(with: objectID) as? LinkedItem
|
||||
}
|
||||
|
||||
func trackReadEvent() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public enum ArticleContentStatus: String {
|
||||
|
|
@ -12,17 +13,20 @@ public struct ArticleContent {
|
|||
public let htmlContent: String
|
||||
public let highlightsJSONString: String
|
||||
public let contentStatus: ArticleContentStatus
|
||||
public let objectID: NSManagedObjectID?
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
htmlContent: String,
|
||||
highlightsJSONString: String,
|
||||
contentStatus: ArticleContentStatus
|
||||
contentStatus: ArticleContentStatus,
|
||||
objectID: NSManagedObjectID?
|
||||
) {
|
||||
self.title = title
|
||||
self.htmlContent = htmlContent
|
||||
self.highlightsJSONString = highlightsJSONString
|
||||
self.contentStatus = contentStatus
|
||||
self.objectID = objectID
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public extension LinkedItem {
|
|||
return PDFUtils.exists(filename: localPDF) || PDFUtils.tempExists(tempPDFURL: tempPDFURL)
|
||||
}
|
||||
// Check the state and whether we have HTML
|
||||
return state == "SUCCEEDED"
|
||||
return state == ArticleContentStatus.succeeded.rawValue
|
||||
}
|
||||
|
||||
var isPDF: Bool {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
import Utils
|
||||
|
||||
struct PendingLink {
|
||||
let itemID: String
|
||||
let retryCount: Int
|
||||
}
|
||||
|
||||
extension DataService {
|
||||
func prefetchPage(pendingLink: PendingLink, username: String) async {
|
||||
let content = try? await loadArticleContent(username: username, itemID: pendingLink.itemID, useCache: false)
|
||||
|
||||
if content?.contentStatus == .processing, pendingLink.retryCount < 7 {
|
||||
let retryDelayInNanoSeconds = UInt64(pendingLink.retryCount * 2 * 1_000_000_000)
|
||||
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: retryDelayInNanoSeconds)
|
||||
logger.debug("fetching content for \(pendingLink.itemID). retry count: \(pendingLink.retryCount)")
|
||||
|
||||
await prefetchPage(
|
||||
pendingLink: PendingLink(
|
||||
itemID: pendingLink.itemID,
|
||||
retryCount: pendingLink.retryCount + 1
|
||||
),
|
||||
username: username
|
||||
)
|
||||
} catch {
|
||||
logger.debug("prefetching task was cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadArticleContent(username: String, itemID: String, useCache: Bool) async throws -> ArticleContent {
|
||||
var objectID: NSManagedObjectID?
|
||||
if useCache, let cachedContent = await cachedArticleContent(itemID: itemID) {
|
||||
return cachedContent
|
||||
}
|
||||
|
||||
// If the page was locally created, make sure they are synced before we pull content
|
||||
await syncUnsyncedArticleContent(itemID: itemID)
|
||||
|
||||
let fetchResult = try await articleContentFetch(username: username, itemID: itemID)
|
||||
let contentStatus = fetchResult.item.isPDF ? .succeeded : fetchResult.item.state
|
||||
|
||||
if contentStatus == .succeeded {
|
||||
do {
|
||||
objectID = try await persistArticleContent(articleProps: fetchResult)
|
||||
} catch {
|
||||
var message = "unknown error"
|
||||
let basicError = (error as? BasicError) ?? BasicError.message(messageText: "unknown error")
|
||||
if case let BasicError.message(messageText) = basicError {
|
||||
message = messageText
|
||||
}
|
||||
throw ContentFetchError.unknown(description: message)
|
||||
}
|
||||
}
|
||||
|
||||
return ArticleContent(
|
||||
title: fetchResult.item.title,
|
||||
htmlContent: fetchResult.htmlContent,
|
||||
highlightsJSONString: fetchResult.highlights.asJSONString,
|
||||
contentStatus: fetchResult.item.isPDF ? .succeeded : fetchResult.item.state,
|
||||
objectID: objectID
|
||||
)
|
||||
}
|
||||
|
||||
func cachedArticleContent(itemID: String) async -> ArticleContent? {
|
||||
let linkedItemFetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
linkedItemFetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", itemID
|
||||
)
|
||||
|
||||
let context = backgroundContext
|
||||
|
||||
return await context.perform(schedule: .immediate) {
|
||||
guard let linkedItem = try? context.fetch(linkedItemFetchRequest).first else { return nil }
|
||||
guard let htmlContent = linkedItem.htmlContent else { return nil }
|
||||
|
||||
let highlights = linkedItem
|
||||
.highlights
|
||||
.asArray(of: Highlight.self)
|
||||
.filter { $0.serverSyncStatus != ServerSyncStatus.needsDeletion.rawValue }
|
||||
|
||||
return ArticleContent(
|
||||
title: linkedItem.unwrappedTitle,
|
||||
htmlContent: htmlContent,
|
||||
highlightsJSONString: highlights.map { InternalHighlight.make(from: $0) }.asJSONString,
|
||||
contentStatus: .succeeded,
|
||||
objectID: linkedItem.objectID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
func persistArticleContent(articleProps: ArticleProps) async throws -> NSManagedObjectID? {
|
||||
var needsPDFDownload = false
|
||||
var objectID: NSManagedObjectID?
|
||||
|
||||
await backgroundContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "id == %@", articleProps.item.id)
|
||||
|
||||
let existingItem = try? self.backgroundContext.fetch(fetchRequest).first
|
||||
let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext)
|
||||
objectID = linkedItem.objectID
|
||||
|
||||
let highlightObjects = articleProps.highlights.map {
|
||||
$0.asManagedObject(context: self.backgroundContext)
|
||||
}
|
||||
linkedItem.addToHighlights(NSSet(array: highlightObjects))
|
||||
linkedItem.htmlContent = articleProps.htmlContent
|
||||
linkedItem.id = articleProps.item.id
|
||||
linkedItem.state = articleProps.item.state.rawValue
|
||||
linkedItem.title = articleProps.item.title
|
||||
linkedItem.createdAt = articleProps.item.createdAt
|
||||
linkedItem.savedAt = articleProps.item.savedAt
|
||||
linkedItem.readingProgress = articleProps.item.readingProgress
|
||||
linkedItem.readingProgressAnchor = Int64(articleProps.item.readingProgressAnchor)
|
||||
linkedItem.imageURLString = articleProps.item.imageURLString
|
||||
linkedItem.onDeviceImageURLString = articleProps.item.onDeviceImageURLString
|
||||
linkedItem.pageURLString = articleProps.item.pageURLString
|
||||
linkedItem.descriptionText = articleProps.item.descriptionText
|
||||
linkedItem.publisherURLString = articleProps.item.publisherURLString
|
||||
linkedItem.author = articleProps.item.author
|
||||
linkedItem.publishDate = articleProps.item.publishDate
|
||||
linkedItem.slug = articleProps.item.slug
|
||||
linkedItem.readAt = articleProps.item.readAt
|
||||
linkedItem.isArchived = articleProps.item.isArchived
|
||||
linkedItem.contentReader = articleProps.item.contentReader
|
||||
linkedItem.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue)
|
||||
|
||||
if articleProps.item.isPDF {
|
||||
needsPDFDownload = true
|
||||
|
||||
// Check if we already have the PDF item locally. Either in temporary
|
||||
// space, or in the documents directory
|
||||
if let localPDF = existingItem?.localPDF {
|
||||
if PDFUtils.exists(filename: localPDF) {
|
||||
linkedItem.localPDF = localPDF
|
||||
needsPDFDownload = false
|
||||
}
|
||||
}
|
||||
|
||||
if let tempPDFURL = existingItem?.tempPDFURL {
|
||||
linkedItem.localPDF = try? PDFUtils.moveToLocal(url: tempPDFURL)
|
||||
_ = PDFUtils.exists(filename: linkedItem.localPDF)
|
||||
if linkedItem.localPDF != nil {
|
||||
needsPDFDownload = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if articleProps.item.isPDF, needsPDFDownload {
|
||||
_ = try await fetchPDFData(slug: articleProps.item.slug, pageURLString: articleProps.item.pageURLString)
|
||||
}
|
||||
|
||||
try await backgroundContext.perform { [weak self] in
|
||||
do {
|
||||
try self?.backgroundContext.save()
|
||||
logger.debug("ArticleContent saved succesfully")
|
||||
} catch {
|
||||
self?.backgroundContext.rollback()
|
||||
logger.debug("Failed to save ArticleContent")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return objectID
|
||||
}
|
||||
|
||||
/// Queries CoreData for a LinkedItem using a requestID.
|
||||
/// - Parameter requestID: A requestID used to check on a newly created item.
|
||||
/// - Returns: The id of the CoreData object if found.
|
||||
func linkedItemID(from requestID: String) -> String? {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "createdId == %@ OR id == %@", requestID, requestID)
|
||||
return try? backgroundContext.fetch(fetchRequest).first?.unwrappedID
|
||||
}
|
||||
|
||||
func syncUnsyncedArticleContent(itemID: String) async {
|
||||
let linkedItemFetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
linkedItemFetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", itemID
|
||||
)
|
||||
|
||||
let context = backgroundContext
|
||||
|
||||
var id: String?
|
||||
var url: String?
|
||||
var title: String?
|
||||
var originalHtml: String?
|
||||
var serverSyncStatus: Int64?
|
||||
|
||||
backgroundContext.performAndWait {
|
||||
guard let linkedItem = try? context.fetch(linkedItemFetchRequest).first else { return }
|
||||
id = linkedItem.unwrappedID
|
||||
url = linkedItem.unwrappedPageURLString
|
||||
title = linkedItem.unwrappedTitle
|
||||
originalHtml = linkedItem.originalHtml
|
||||
serverSyncStatus = linkedItem.serverSyncStatus
|
||||
}
|
||||
|
||||
guard let id = id, let url = url, let title = title,
|
||||
let serverSyncStatus = serverSyncStatus,
|
||||
serverSyncStatus == ServerSyncStatus.needsCreation.rawValue
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let originalHtml = originalHtml {
|
||||
_ = try await savePage(id: id, url: url, title: title, originalHtml: originalHtml)
|
||||
} else {
|
||||
_ = try await saveURL(id: id, url: url)
|
||||
}
|
||||
} catch {
|
||||
// We don't propogate these errors, we just let it pass through so
|
||||
// the user can attempt to fetch content again.
|
||||
print("Error syncUnsyncedArticleContent")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,19 @@ public final class DataService: ObservableObject {
|
|||
return try? persistentContainer.viewContext.fetch(fetchRequest).first
|
||||
}
|
||||
|
||||
public func username() async -> String? {
|
||||
if let cachedUsername = currentViewer?.username {
|
||||
return cachedUsername
|
||||
}
|
||||
|
||||
if let viewerObjectID = try? await fetchViewer() {
|
||||
let viewer = backgroundContext.object(with: viewerObjectID) as? Viewer
|
||||
return viewer?.unwrappedUsername
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func switchAppEnvironment(appEnvironment: AppEnvironment) {
|
||||
do {
|
||||
try ValetKey.appEnvironmentString.setValue(appEnvironment.rawValue)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
import Utils
|
||||
|
||||
public extension DataService {
|
||||
func prefetchPages(itemIDs: [String], username: String) async {
|
||||
|
|
@ -32,52 +33,11 @@ public extension DataService {
|
|||
let retryDelayInNanoSeconds = UInt64(requestCount * 2 * 1_000_000_000)
|
||||
try await Task.sleep(nanoseconds: retryDelayInNanoSeconds)
|
||||
logger.debug("fetching content for \(itemID). request count: \(requestCount)")
|
||||
return try await loadArticleContentWithRetries(itemID: itemID, username: username, requestCount: requestCount + 1)
|
||||
// Check for an updated requestID
|
||||
let updatedItemID = linkedItemID(from: itemID) ?? itemID
|
||||
return try await loadArticleContentWithRetries(itemID: updatedItemID, username: username, requestCount: requestCount + 1)
|
||||
case .succeeded, .unknown:
|
||||
return fetchedContent
|
||||
}
|
||||
}
|
||||
|
||||
func syncUnsyncedArticleContent(itemID: String) async {
|
||||
let linkedItemFetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
linkedItemFetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", itemID
|
||||
)
|
||||
|
||||
let context = backgroundContext
|
||||
|
||||
var id: String?
|
||||
var url: String?
|
||||
var title: String?
|
||||
var originalHtml: String?
|
||||
var serverSyncStatus: Int64?
|
||||
|
||||
backgroundContext.performAndWait {
|
||||
guard let linkedItem = try? context.fetch(linkedItemFetchRequest).first else { return }
|
||||
id = linkedItem.unwrappedID
|
||||
url = linkedItem.unwrappedPageURLString
|
||||
title = linkedItem.unwrappedTitle
|
||||
originalHtml = linkedItem.originalHtml
|
||||
serverSyncStatus = linkedItem.serverSyncStatus
|
||||
}
|
||||
|
||||
guard let id = id, let url = url, let title = title,
|
||||
let serverSyncStatus = serverSyncStatus,
|
||||
serverSyncStatus == ServerSyncStatus.needsCreation.rawValue
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let originalHtml = originalHtml {
|
||||
_ = try await savePage(id: id, url: url, title: title, originalHtml: originalHtml)
|
||||
} else {
|
||||
_ = try await saveURL(id: id, url: url)
|
||||
}
|
||||
} catch {
|
||||
// We don't propogate these errors, we just let it pass through so
|
||||
// the user can attempt to fetch content again.
|
||||
print("Error syncUnsyncedArticleContent")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,4 +40,15 @@ public extension DataService {
|
|||
|
||||
return persistedItemID
|
||||
}
|
||||
|
||||
func loadItemContentUsingRequestID(requestID: String) async throws -> NSManagedObjectID? {
|
||||
let username: String? = await username()
|
||||
guard let username = username else { throw BasicError.message(messageText: "unauthorized user") }
|
||||
|
||||
// If the page was locally created, make sure they are synced before we pull content
|
||||
await syncUnsyncedArticleContent(itemID: requestID)
|
||||
|
||||
let articleContent = try await loadArticleContentWithRetries(itemID: requestID, username: username, requestCount: 0)
|
||||
return articleContent.objectID
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue