diff --git a/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate.swift index 0febfad22..b2e1a626b 100644 --- a/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/FeedlyAccountDelegate.swift @@ -129,6 +129,11 @@ final class FeedlyAccountDelegate: AccountDelegate { refreshing = true defer { refreshing = false } + let date = Date() + defer { + os_log(.debug, log: log, "Sync took %{public}.3f seconds", -date.timeIntervalSinceNow) + } + // Send any read/unread/starred article statuses to Feedly before anything else. try await sendArticleStatuses() @@ -159,103 +164,15 @@ final class FeedlyAccountDelegate: AccountDelegate { try await downloadArticles(missingArticleIDs: missingArticleIDs, updatedArticleIDs: updatedArticleIDs) } - private func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { - assert(Thread.isMainThread) + func syncArticleStatus(for account: Account) async throws { - guard currentSyncAllOperation == nil else { - os_log(.debug, log: log, "Ignoring refreshAll: Feedly sync already in progress.") - completion(.success(())) - return - } - - guard let credentials = credentials else { - os_log(.debug, log: log, "Ignoring refreshAll: Feedly account has no credentials.") - completion(.failure(FeedlyAccountDelegateError.notLoggedIn)) - return - } - - let log = self.log - - let syncAllOperation = FeedlySyncAllOperation(account: account, feedlyUserID: credentials.username, caller: caller, database: syncDatabase, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress, log: log) - - syncAllOperation.downloadProgress = refreshProgress - - let date = Date() - syncAllOperation.syncCompletionHandler = { [weak self] result in - if case .success = result { - self?.accountMetadata?.lastArticleFetchStartTime = date - self?.accountMetadata?.lastArticleFetchEndTime = Date() - } - - os_log(.debug, log: log, "Sync took %{public}.3f seconds", -date.timeIntervalSinceNow) - completion(result) - } - - currentSyncAllOperation = syncAllOperation - - operationQueue.add(syncAllOperation) - } - - @MainActor func syncArticleStatus(for account: Account) async throws { - - try await withCheckedThrowingContinuation { continuation in - sendArticleStatus(for: account) { result in - switch result { - case .success: - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } - case .failure(let error): - continuation.resume(throwing: error) - } - } - } + try await sendArticleStatus(for: account) + try await refreshArticleStatus(for: account) } public func sendArticleStatus(for account: Account) async throws { - try await withCheckedThrowingContinuation { continuation in - - self.sendArticleStatus(for: account) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - - @MainActor private func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - // Ensure remote articles have the same status as they do locally. - let send = FeedlySendArticleStatusesOperation(database: syncDatabase, service: caller, log: log) - send.completionBlock = { operation in - // TODO: not call with success if operation was canceled? Not sure. - DispatchQueue.main.async { - completion(.success(())) - } - } - operationQueue.add(send) - } - - func refreshArticleStatus(for account: Account) async throws { - - try await withCheckedThrowingContinuation { continuation in - self.refreshArticleStatus(for: account) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } - } + try await sendArticleStatuses() } /// Attempts to ensure local articles have the same status as they do remotely. @@ -263,34 +180,10 @@ final class FeedlyAccountDelegate: AccountDelegate { /// this app does its part to ensure the articles have a consistent status between both. /// /// - Parameter account: The account whose articles have a remote status. - /// - Parameter completion: Call on the main queue. - private func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - guard let credentials = credentials else { - return completion(.success(())) - } + func refreshArticleStatus(for account: Account) async throws { - let group = DispatchGroup() - - let ingestUnread = FeedlyIngestUnreadArticleIDsOperation(account: account, userID: credentials.username, service: caller, database: syncDatabase, newerThan: nil, log: log) - - group.enter() - ingestUnread.completionBlock = { _ in - group.leave() - - } - - let ingestStarred = FeedlyIngestStarredArticleIDsOperation(account: account, userID: credentials.username, service: caller, database: syncDatabase, newerThan: nil, log: log) - - group.enter() - ingestStarred.completionBlock = { _ in - group.leave() - } - - group.notify(queue: .main) { - completion(.success(())) - } - - operationQueue.addOperations([ingestUnread, ingestStarred]) + try await fetchAndProcessUnreadArticleIDs() + try await fetchAndProcessStarredArticleIDs() } func importOPML(for account: Account, opmlFile: URL) async throws { @@ -366,49 +259,9 @@ final class FeedlyAccountDelegate: AccountDelegate { func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool) async throws -> Feed { - try await withCheckedThrowingContinuation { continuation in - self.createFeed(for: account, url: url, name: name, container: container, validateFeed: validateFeed) { result in - switch result { - case .success(let feed): - continuation.resume(returning: feed) - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } + // TODO: make this work - private func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { - - do { - guard let credentials = credentials else { - throw FeedlyAccountDelegateError.notLoggedIn - } - - let addNewFeed = try FeedlyAddNewFeedOperation(account: account, - credentials: credentials, - url: url, - feedName: name, - searchService: caller, - addToCollectionService: caller, - syncUnreadIDsService: caller, - getStreamContentsService: caller, - database: syncDatabase, - container: container, - progress: refreshProgress, - log: log) - - addNewFeed.addCompletionHandler = { result in - completion(result) - } - - operationQueue.add(addNewFeed) - - } catch { - DispatchQueue.main.async { - completion(.failure(error)) - } - } + throw FeedlyAccountDelegateError.notLoggedIn } func renameFeed(for account: Account, with feed: Feed, to name: String) async throws { @@ -435,48 +288,8 @@ final class FeedlyAccountDelegate: AccountDelegate { func addFeed(for account: Account, with feed: Feed, to container: any Container) async throws { - try await withCheckedThrowingContinuation { continuation in - - self.addFeed(for: account, with: feed, to: container) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - - func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result) -> Void) { - - do { - guard let credentials = credentials else { - throw FeedlyAccountDelegateError.notLoggedIn - } - - let resource = FeedlyFeedResourceID(id: feed.feedID) - let addExistingFeed = try FeedlyAddExistingFeedOperation(account: account, - credentials: credentials, - resource: resource, - service: caller, - container: container, - progress: refreshProgress, - log: log, - customFeedName: feed.editedName) - - - addExistingFeed.addCompletionHandler = { result in - completion(result) - } - - operationQueue.add(addExistingFeed) - - } catch { - DispatchQueue.main.async { - completion(.failure(error)) - } - } + let resourceID = FeedlyFeedResourceID(id: feed.feedID) + try await addExistingFeed(resourceID: resourceID, container: container) } func removeFeed(for account: Account, with feed: Feed, from container: any Container) async throws { @@ -517,119 +330,42 @@ final class FeedlyAccountDelegate: AccountDelegate { func restoreFeed(for account: Account, feed: Feed, container: any Container) async throws { - try await withCheckedThrowingContinuation { continuation in - - self.restoreFeed(for: account, feed: feed, container: container) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - - private func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { - if let existingFeed = account.existingFeed(withURL: feed.url) { - - Task { @MainActor in - do { - try await account.addFeed(existingFeed, to: container) - completion(.success(())) - } catch { - completion(.failure(error)) - } - } + try await account.addFeed(existingFeed, to: container) } else { - createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in - switch result { - case .success: - completion(.success(())) - case .failure(let error): - completion(.failure(error)) - } - } + try await createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) } } func restoreFolder(for account: Account, folder: Folder) async throws { - try await withCheckedThrowingContinuation { continuation in - self.restoreFolder(for: account, folder: folder) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - - private func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { - let group = DispatchGroup() - for feed in folder.topLevelFeeds { folder.topLevelFeeds.remove(feed) - group.enter() - restoreFeed(for: account, feed: feed, container: folder) { result in - group.leave() - switch result { - case .success: - break - case .failure(let error): - os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription) - } + do { + try await restoreFeed(for: account, feed: feed, container: folder) + + } catch { + os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription) } - } - group.notify(queue: .main) { - account.addFolder(folder) - completion(.success(())) - } + account.addFolder(folder) } func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) async throws { - try await withCheckedThrowingContinuation { continuation in - self.markArticles(for: account, articles: articles, statusKey: statusKey, flag: flag) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } + let articles = try await account.update(articles: articles, statusKey: statusKey, flag: flag) + + let syncStatuses = articles.map { article in + return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) } - } - private func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result) -> Void) { + try? await syncDatabase.insertStatuses(syncStatuses) - Task { @MainActor in - - do { - - let articles = try await account.update(articles: articles, statusKey: statusKey, flag: flag) - - let syncStatuses = articles.map { article in - return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag) - } - - try? await self.syncDatabase.insertStatuses(syncStatuses) - - if let count = try? await self.syncDatabase.selectPendingCount(), count > 100 { - self.sendArticleStatus(for: account) { _ in } - } - completion(.success(())) - - } catch { - completion(.failure(error)) - } + if let count = try? await syncDatabase.selectPendingCount(), count > 100 { + try? await sendArticleStatus(for: account) } } @@ -638,10 +374,10 @@ final class FeedlyAccountDelegate: AccountDelegate { credentials = try? account.retrieveCredentials(type: .oauthAccessToken) } - @MainActor func accountWillBeDeleted(_ account: Account) { - let logout = FeedlyLogoutOperation(service: caller, log: log) - // Dispatch on the shared queue because the lifetime of the account delegate is uncertain. - MainThreadOperationQueue.shared.add(logout) + func accountWillBeDeleted(_ account: Account) { + Task { + try? await logout(account: account) + } } static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider) async throws -> Credentials? { diff --git a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift index 1bedc6ef0..e7ab9044b 100644 --- a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift +++ b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift @@ -259,7 +259,7 @@ protocol FeedlyAPICallerDelegate: AnyObject { } } -extension FeedlyAPICaller: FeedlyAddFeedToCollectionService { +extension FeedlyAPICaller { @MainActor func addFeed(with feedID: FeedlyFeedResourceID, title: String? = nil, toCollectionWith collectionID: String) async throws -> [FeedlyFeed] { @@ -533,7 +533,7 @@ extension FeedlyAPICaller: FeedlyMarkArticlesService { } } -extension FeedlyAPICaller: FeedlySearchService { +extension FeedlyAPICaller { func getFeeds(for query: String, count: Int, localeIdentifier: String) async throws -> FeedlyFeedsSearchResponse { @@ -563,7 +563,7 @@ extension FeedlyAPICaller: FeedlySearchService { } } -extension FeedlyAPICaller: FeedlyLogoutService { +extension FeedlyAPICaller { func logout() async throws { diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift deleted file mode 100644 index 820d35398..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// FeedlyAddExistingFeedOperation.swift -// Account -// -// Created by Kiel Gillard on 27/11/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Web -import Secrets -import Core -import Feedly - -@MainActor final class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate { - - private let operationQueue = MainThreadOperationQueue() - var addCompletionHandler: ((Result) -> ())? - - @MainActor init(account: Account, credentials: Credentials, resource: FeedlyFeedResourceID, service: FeedlyAddFeedToCollectionService, container: Container, progress: DownloadProgress, log: OSLog, customFeedName: String? = nil) throws { - - let validator = FeedlyFeedContainerValidator(container: container) - let (folder, collectionID) = try validator.getValidContainer() - - self.operationQueue.suspend() - - super.init() - - self.downloadProgress = progress - - let addRequest = FeedlyAddFeedToCollectionOperation(folder: folder, feedResource: resource, feedName: customFeedName, collectionID: collectionID, service: service) - addRequest.delegate = self - addRequest.downloadProgress = progress - self.operationQueue.add(addRequest) - - let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log) - createFeeds.downloadProgress = progress - createFeeds.addDependency(addRequest) - self.operationQueue.add(createFeeds) - - let finishOperation = FeedlyCheckpointOperation() - finishOperation.checkpointDelegate = self - finishOperation.downloadProgress = progress - finishOperation.addDependency(createFeeds) - self.operationQueue.add(finishOperation) - } - - override func run() { - operationQueue.resume() - } - - override func didCancel() { - operationQueue.cancelAllOperations() - addCompletionHandler = nil - super.didCancel() - } - - func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { - addCompletionHandler?(.failure(error)) - addCompletionHandler = nil - - cancel() - } - - func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { - guard !isCanceled else { - return - } - - addCompletionHandler?(.success(())) - addCompletionHandler = nil - - didFinish() - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift deleted file mode 100644 index 6d7adfb65..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// FeedlyAddFeedToCollectionOperation.swift -// Account -// -// Created by Kiel Gillard on 11/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import CommonErrors -import Feedly - -protocol FeedlyAddFeedToCollectionService { - func addFeed(with feedID: FeedlyFeedResourceID, title: String?, toCollectionWith collectionID: String) async throws -> [FeedlyFeed] -} - -final class FeedlyAddFeedToCollectionOperation: FeedlyOperation, FeedlyFeedsAndFoldersProviding, FeedlyResourceProviding { - - let feedName: String? - let collectionID: String - let service: FeedlyAddFeedToCollectionService - let folder: Folder - let feedResource: FeedlyFeedResourceID - - init(folder: Folder, feedResource: FeedlyFeedResourceID, feedName: String? = nil, collectionID: String, service: FeedlyAddFeedToCollectionService) { - self.folder = folder - self.feedResource = feedResource - self.feedName = feedName - self.collectionID = collectionID - self.service = service - } - - private(set) var feedsAndFolders = [([FeedlyFeed], Folder)]() - - var resource: FeedlyResourceID { - return feedResource - } - - override func run() { - - Task { @MainActor in - - do { - let feedlyFeeds = try await service.addFeed(with: feedResource, title: feedName, toCollectionWith: collectionID) - - feedsAndFolders = [(feedlyFeeds, folder)] - - let feedsWithCreatedFeedID = feedlyFeeds.filter { $0.id == resource.id } - if feedsWithCreatedFeedID.isEmpty { - didFinish(with: AccountError.createErrorNotFound) - } else { - didFinish() - } - - } catch { - didFinish(with: error) - } - } - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift deleted file mode 100644 index 15eef5ea1..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// FeedlyAddNewFeedOperation.swift -// Account -// -// Created by Kiel Gillard on 27/11/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import SyncDatabase -import Web -import Secrets -import Core -import CommonErrors -import Feedly - -final class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate { - - private let operationQueue = MainThreadOperationQueue() - private let folder: Folder - private let collectionID: String - private let url: String - private let account: Account - private let credentials: Credentials - private let database: SyncDatabase - private let feedName: String? - private let addToCollectionService: FeedlyAddFeedToCollectionService - private let syncUnreadIDsService: FeedlyGetStreamIDsService - private let getStreamContentsService: FeedlyGetStreamContentsService - private let log: OSLog - private var feedResourceID: FeedlyFeedResourceID? - var addCompletionHandler: ((Result) -> ())? - - @MainActor init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIDsService: FeedlyGetStreamIDsService, getStreamContentsService: FeedlyGetStreamContentsService, database: SyncDatabase, container: Container, progress: DownloadProgress, log: OSLog) throws { - - - let validator = FeedlyFeedContainerValidator(container: container) - (self.folder, self.collectionID) = try validator.getValidContainer() - - self.url = url - self.operationQueue.suspend() - self.account = account - self.credentials = credentials - self.database = database - self.feedName = feedName - self.addToCollectionService = addToCollectionService - self.syncUnreadIDsService = syncUnreadIDsService - self.getStreamContentsService = getStreamContentsService - self.log = log - - super.init() - - self.downloadProgress = progress - - let search = FeedlySearchOperation(query: url, locale: .current, service: searchService) - search.delegate = self - search.searchDelegate = self - search.downloadProgress = progress - self.operationQueue.add(search) - } - - override func run() { - operationQueue.resume() - } - - override func didCancel() { - operationQueue.cancelAllOperations() - addCompletionHandler = nil - super.didCancel() - } - - override func didFinish(with error: Error) { - assert(Thread.isMainThread) - addCompletionHandler?(.failure(error)) - addCompletionHandler = nil - super.didFinish(with: error) - } - - @MainActor func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse) { - guard !isCanceled else { - return - } - guard let first = response.results.first else { - return didFinish(with: AccountError.createErrorNotFound) - } - - let feedResourceID = FeedlyFeedResourceID(id: first.feedID) - self.feedResourceID = feedResourceID - - let addRequest = FeedlyAddFeedToCollectionOperation(folder: folder, feedResource: feedResourceID, feedName: feedName, collectionID: collectionID, service: addToCollectionService) - addRequest.delegate = self - addRequest.downloadProgress = downloadProgress - operationQueue.add(addRequest) - - let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log) - createFeeds.delegate = self - createFeeds.addDependency(addRequest) - createFeeds.downloadProgress = downloadProgress - operationQueue.add(createFeeds) - - let syncUnread = FeedlyIngestUnreadArticleIDsOperation(account: account, userID: credentials.username, service: syncUnreadIDsService, database: database, newerThan: nil, log: log) - syncUnread.addDependency(createFeeds) - syncUnread.downloadProgress = downloadProgress - syncUnread.delegate = self - operationQueue.add(syncUnread) - - let syncFeed = FeedlySyncStreamContentsOperation(account: account, resource: feedResourceID, service: getStreamContentsService, isPagingEnabled: false, newerThan: nil, log: log) - syncFeed.addDependency(syncUnread) - syncFeed.downloadProgress = downloadProgress - syncFeed.delegate = self - operationQueue.add(syncFeed) - - let finishOperation = FeedlyCheckpointOperation() - finishOperation.checkpointDelegate = self - finishOperation.downloadProgress = downloadProgress - finishOperation.addDependency(syncFeed) - finishOperation.delegate = self - operationQueue.add(finishOperation) - } - - func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { - addCompletionHandler?(.failure(error)) - addCompletionHandler = nil - - os_log(.debug, log: log, "Unable to add new feed: %{public}@.", error as NSError) - - cancel() - } - - func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { - guard !isCanceled else { - return - } - defer { - didFinish() - } - - guard let handler = addCompletionHandler else { - return - } - if let feedResource = feedResourceID, let feed = folder.existingFeed(withFeedID: feedResource.id) { - handler(.success(feed)) - } - else { - handler(.failure(AccountError.createErrorNotFound)) - } - addCompletionHandler = nil - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyCheckpointOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyCheckpointOperation.swift deleted file mode 100644 index b3614a2d9..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyCheckpointOperation.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// FeedlyCheckpointOperation.swift -// Account -// -// Created by Kiel Gillard on 18/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import Feedly - -protocol FeedlyCheckpointOperationDelegate: AnyObject { - @MainActor func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) -} - -/// Let the delegate know an instance is executing. The semantics are up to the delegate. -final class FeedlyCheckpointOperation: FeedlyOperation { - - weak var checkpointDelegate: FeedlyCheckpointOperationDelegate? - - override func run() { - defer { - didFinish() - } - checkpointDelegate?.feedlyCheckpointOperationDidReachCheckpoint(self) - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyCreateFeedsForCollectionFoldersOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyCreateFeedsForCollectionFoldersOperation.swift deleted file mode 100644 index 529e2ebb9..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyCreateFeedsForCollectionFoldersOperation.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// FeedlyCreateFeedsForCollectionFoldersOperation.swift -// Account -// -// Created by Kiel Gillard on 20/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Core -import Feedly - -/// Single responsibility is to accurately reflect Collections and their Feeds as Folders and their Feeds. -final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation { - - let account: Account - let feedsAndFoldersProvider: FeedlyFeedsAndFoldersProviding - let log: OSLog - - init(account: Account, feedsAndFoldersProvider: FeedlyFeedsAndFoldersProviding, log: OSLog) { - self.feedsAndFoldersProvider = feedsAndFoldersProvider - self.account = account - self.log = log - } - - public override func run() { - defer { - didFinish() - } - - let pairs = feedsAndFoldersProvider.feedsAndFolders - - let feedsBefore = Set(pairs - .map { $0.1 } - .flatMap { $0.topLevelFeeds }) - - // Remove feeds in a folder which are not in the corresponding collection. - for (collectionFeeds, folder) in pairs { - let feedsInFolder = folder.topLevelFeeds - let feedsInCollection = Set(collectionFeeds.map { $0.id }) - let feedsToRemove = feedsInFolder.filter { !feedsInCollection.contains($0.feedID) } - if !feedsToRemove.isEmpty { - folder.removeFeeds(feedsToRemove) -// os_log(.debug, log: log, "\"%@\" - removed: %@", collection.label, feedsToRemove.map { $0.feedID }, feedsInCollection) - } - - } - - // Pair each Feed with its Folder. - var feedsAdded = Set() - - let feedsAndFolders = pairs - .map({ (collectionFeeds, folder) -> [(FeedlyFeed, Folder)] in - return collectionFeeds.map { feed -> (FeedlyFeed, Folder) in - return (feed, folder) // pairs a folder for every feed in parallel - } - }) - .flatMap { $0 } - .compactMap { (collectionFeed, folder) -> (Feed, Folder) in - - // find an existing feed previously added to the account - if let feed = account.existingFeed(withFeedID: collectionFeed.id) { - - // If the feed was renamed on Feedly, ensure we ingest the new name. - if feed.nameForDisplay != collectionFeed.title { - feed.name = collectionFeed.title - - // Let the rest of the app (e.g.: the sidebar) know the feed name changed - // `editedName` would post this if its value is changing. - // Setting the `name` property has no side effects like this. - if feed.editedName != nil { - feed.editedName = nil - } else { - feed.postDisplayNameDidChangeNotification() - } - } - return (feed, folder) - } else { - // find an existing feed we created below in an earlier value - for feed in feedsAdded where feed.feedID == collectionFeed.id { - return (feed, folder) - } - } - - // no existing feed, create a new one - let parser = FeedlyFeedParser(feed: collectionFeed) - let feed = account.createFeed(with: parser.title, - url: parser.url, - feedID: parser.feedID, - homePageURL: parser.homePageURL) - - // So the same feed isn't created more than once. - feedsAdded.insert(feed) - - return (feed, folder) - } - - os_log(.debug, log: log, "Processing %i feeds.", feedsAndFolders.count) - for (feed, folder) in feedsAndFolders { - if !folder.has(feed) { - folder.addFeed(feed) - } - } - - // Remove feeds without folders/collections. - let feedsAfter = Set(feedsAndFolders.map { $0.0 }) - let feedsWithoutCollections = feedsBefore.subtracting(feedsAfter) - account.removeFeeds(feedsWithoutCollections) - - if !feedsWithoutCollections.isEmpty { - os_log(.debug, log: log, "Removed %i feeds", feedsWithoutCollections.count) - } - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift deleted file mode 100644 index 3e1cbe742..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// FeedlyDownloadArticlesOperation.swift -// Account -// -// Created by Kiel Gillard on 9/1/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Web -import Core -import Feedly - -class FeedlyDownloadArticlesOperation: FeedlyOperation { - - private let account: Account - private let log: OSLog - private let missingArticleEntryIDProvider: FeedlyEntryIdentifierProviding - private let updatedArticleEntryIDProvider: FeedlyEntryIdentifierProviding - private let getEntriesService: FeedlyGetEntriesService - private let operationQueue = MainThreadOperationQueue() - private let finishOperation: FeedlyCheckpointOperation - - @MainActor init(account: Account, missingArticleEntryIDProvider: FeedlyEntryIdentifierProviding, updatedArticleEntryIDProvider: FeedlyEntryIdentifierProviding, getEntriesService: FeedlyGetEntriesService, log: OSLog) { - self.account = account - self.operationQueue.suspend() - self.missingArticleEntryIDProvider = missingArticleEntryIDProvider - self.updatedArticleEntryIDProvider = updatedArticleEntryIDProvider - self.getEntriesService = getEntriesService - self.finishOperation = FeedlyCheckpointOperation() - self.log = log - super.init() - self.finishOperation.checkpointDelegate = self - self.operationQueue.add(self.finishOperation) - } - - override func run() { - var articleIDs = missingArticleEntryIDProvider.entryIDs - articleIDs.formUnion(updatedArticleEntryIDProvider.entryIDs) - - os_log(.debug, log: log, "Requesting %{public}i articles.", articleIDs.count) - - let feedlyAPILimitBatchSize = 1000 - for articleIDs in Array(articleIDs).chunked(into: feedlyAPILimitBatchSize) { - - Task { @MainActor in - let provider = FeedlyEntryIdentifierProvider(entryIDs: Set(articleIDs)) - let getEntries = FeedlyGetEntriesOperation(service: self.getEntriesService, provider: provider, log: self.log) - getEntries.delegate = self - self.operationQueue.add(getEntries) - - let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(parsedItemProvider: getEntries, - log: log) - organiseByFeed.delegate = self - organiseByFeed.addDependency(getEntries) - self.operationQueue.add(organiseByFeed) - - let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, - organisedItemsProvider: organiseByFeed, - log: log) - - updateAccount.delegate = self - updateAccount.addDependency(organiseByFeed) - self.operationQueue.add(updateAccount) - - finishOperation.addDependency(updateAccount) - } - } - - operationQueue.resume() - } - - override func didCancel() { - // TODO: fix error on below line: "Expression type '()' is ambiguous without more context" - //os_log(.debug, log: log, "Cancelling %{public}@.", self) - operationQueue.cancelAllOperations() - super.didCancel() - } -} - -extension FeedlyDownloadArticlesOperation: FeedlyCheckpointOperationDelegate { - - func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { - didFinish() - } -} - -extension FeedlyDownloadArticlesOperation: FeedlyOperationDelegate { - - func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { - assert(Thread.isMainThread) - - // Having this log is useful for debugging missing required JSON keys in the response from Feedly, for example. - os_log(.debug, log: log, "%{public}@ failed with error: %{public}@.", String(describing: operation), error as NSError) - - cancel() - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyFetchIDsForMissingArticlesOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyFetchIDsForMissingArticlesOperation.swift deleted file mode 100644 index d7f86da24..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyFetchIDsForMissingArticlesOperation.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// FeedlyFetchIDsForMissingArticlesOperation.swift -// Account -// -// Created by Kiel Gillard on 7/1/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Feedly - -final class FeedlyFetchIDsForMissingArticlesOperation: FeedlyOperation, FeedlyEntryIdentifierProviding { - - private let account: Account - - private(set) var entryIDs = Set() - - init(account: Account) { - self.account = account - } - - override func run() { - - Task { @MainActor in - - do { - if let articleIDs = try await account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate() { - self.entryIDs.formUnion(articleIDs) - } - - self.didFinish() - - } catch { - self.didFinish(with: error) - } - } - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIDsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIDsOperation.swift deleted file mode 100644 index 3d09b99bb..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIDsOperation.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// FeedlyIngestStarredArticleIDsOperation.swift -// Account -// -// Created by Kiel Gillard on 15/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import SyncDatabase -import Secrets -import Feedly - -/// Clone locally the remote starred article state. -/// -/// Typically, it pages through the article ids of the global.saved stream. -/// When all the article ids are collected, a status is created for each. -/// The article ids previously marked as starred but not collected become unstarred. -/// So this operation has side effects *for the entire account* it operates on. -final class FeedlyIngestStarredArticleIDsOperation: FeedlyOperation { - - private let account: Account - private let resource: FeedlyResourceID - private let service: FeedlyGetStreamIDsService - private let database: SyncDatabase - private var remoteEntryIDs = Set() - private let log: OSLog - - convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { - let resource = FeedlyTagResourceID.Global.saved(for: userID) - self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log) - } - - init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { - self.account = account - self.resource = resource - self.service = service - self.database = database - self.log = log - } - - override func run() { - getStreamIDs(nil) - } - - private func getStreamIDs(_ continuation: String?) { - - Task { @MainActor in - - do { - let streamIDs = try await service.getStreamIDs(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil) - remoteEntryIDs.formUnion(streamIDs.ids) - - guard let continuation = streamIDs.continuation else { - try await removeEntryIDsWithPendingStatus() - didFinish() - return - } - - getStreamIDs(continuation) - - } catch { - didFinish(with: error) - } - } - } - - /// Do not override pending statuses with the remote statuses of the same articles, otherwise an article will temporarily re-acquire the remote status before the pending status is pushed and subseqently pulled. - private func removeEntryIDsWithPendingStatus() async throws { - - if let pendingArticleIDs = try await database.selectPendingStarredStatusArticleIDs() { - remoteEntryIDs.subtract(pendingArticleIDs) - } - try await updateStarredStatuses() - } - - private func updateStarredStatuses() async throws { - - if let localStarredArticleIDs = try await account.fetchStarredArticleIDs() { - try await processStarredArticleIDs(localStarredArticleIDs) - } - } - - func processStarredArticleIDs(_ localStarredArticleIDs: Set) async throws { - - var markAsStarredError: Error? - var markAsUnstarredError: Error? - - let remoteStarredArticleIDs = remoteEntryIDs - do { - try await account.markAsStarred(remoteStarredArticleIDs) - } catch { - markAsStarredError = error - } - - let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs) - do { - try await account.markAsUnstarred(deltaUnstarredArticleIDs) - } catch { - markAsUnstarredError = error - } - - if let markingError = markAsStarredError ?? markAsUnstarredError { - throw markingError - } - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIDsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIDsOperation.swift deleted file mode 100644 index e7958c064..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIDsOperation.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// FeedlyIngestStreamArticleIDsOperation.swift -// Account -// -// Created by Kiel Gillard on 9/1/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Secrets -import Database -import Feedly - -/// Ensure a status exists for every article id the user might be interested in. -/// -/// Typically, it pages through the article ids of the global.all stream. -/// As the article ids are collected, a default read status is created for each. -/// So this operation has side effects *for the entire account* it operates on. -class FeedlyIngestStreamArticleIDsOperation: FeedlyOperation { - - private let account: Account - private let resource: FeedlyResourceID - private let service: FeedlyGetStreamIDsService - private let log: OSLog - - init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, log: OSLog) { - self.account = account - self.resource = resource - self.service = service - self.log = log - } - - convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, log: OSLog) { - let all = FeedlyCategoryResourceID.Global.all(for: userID) - self.init(account: account, resource: all, service: service, log: log) - } - - override func run() { - getStreamIDs(nil) - } - - private func getStreamIDs(_ continuation: String?) { - - Task { @MainActor in - - do { - let streamIDs = try await service.getStreamIDs(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil) - - try await account.createStatusesIfNeeded(articleIDs: Set(streamIDs.ids)) - - guard let continuation = streamIDs.continuation else { - os_log(.debug, log: self.log, "Reached end of stream for %@", self.resource.id) - self.didFinish() - return - } - self.getStreamIDs(continuation) - - } catch { - didFinish(with: error) - } - } - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIDsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIDsOperation.swift deleted file mode 100644 index e8f691786..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIDsOperation.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// FeedlyIngestUnreadArticleIDsOperation.swift -// Account -// -// Created by Kiel Gillard on 18/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Parser -import SyncDatabase -import Secrets -import Feedly - -/// Clone locally the remote unread article state. -/// -/// Typically, it pages through the unread article ids of the global.all stream. -/// When all the unread article ids are collected, a status is created for each. -/// The article ids previously marked as unread but not collected become read. -/// So this operation has side effects *for the entire account* it operates on. -final class FeedlyIngestUnreadArticleIDsOperation: FeedlyOperation { - - private let account: Account - private let resource: FeedlyResourceID - private let service: FeedlyGetStreamIDsService - private let database: SyncDatabase - private var remoteEntryIDs = Set() - private let log: OSLog - - public convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { - let resource = FeedlyCategoryResourceID.Global.all(for: userID) - self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log) - } - - public init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { - self.account = account - self.resource = resource - self.service = service - self.database = database - self.log = log - } - - override func run() { - getStreamIDs(nil) - } - - private func getStreamIDs(_ continuation: String?) { - - Task { @MainActor in - - do { - let streamIDs = try await service.getStreamIDs(for: resource, continuation: continuation, newerThan: nil, unreadOnly: true) - remoteEntryIDs.formUnion(streamIDs.ids) - - guard let continuation = streamIDs.continuation else { - try await removeEntryIDsWithPendingStatus() - didFinish() - return - } - - getStreamIDs(continuation) - - } catch { - didFinish(with: error) - } - } - } - - /// Do not override pending statuses with the remote statuses of the same articles, otherwise an article will temporarily re-acquire the remote status before the pending status is pushed and subseqently pulled. - private func removeEntryIDsWithPendingStatus() async throws { - - if let pendingArticleIDs = try await database.selectPendingReadStatusArticleIDs() { - remoteEntryIDs.subtract(pendingArticleIDs) - } - try await updateUnreadStatuses() - } - - private func updateUnreadStatuses() async throws { - - if let localUnreadArticleIDs = try await account.fetchUnreadArticleIDs() { - try await processUnreadArticleIDs(localUnreadArticleIDs) - } - } - - private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set) async throws { - - let remoteUnreadArticleIDs = remoteEntryIDs - - var markAsUnreadError: Error? - var markAsReadError: Error? - - do { - try await account.markAsUnread(remoteUnreadArticleIDs) - } catch { - markAsUnreadError = error - } - - let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs) - do { - try await account.markAsRead(articleIDsToMarkRead) - } catch { - markAsReadError = error - } - - if let markingError = markAsReadError ?? markAsUnreadError { - throw markingError - } - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift deleted file mode 100644 index af769f60d..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// FeedlyMirrorCollectionsAsFoldersOperation.swift -// Account -// -// Created by Kiel Gillard on 20/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Feedly - -protocol FeedlyFeedsAndFoldersProviding { - @MainActor var feedsAndFolders: [([FeedlyFeed], Folder)] { get } -} - -/// Reflect Collections from Feedly as Folders. -final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyFeedsAndFoldersProviding { - - let account: Account - let collectionsProvider: FeedlyCollectionProviding - let log: OSLog - - private(set) var feedsAndFolders = [([FeedlyFeed], Folder)]() - - init(account: Account, collectionsProvider: FeedlyCollectionProviding, log: OSLog) { - self.collectionsProvider = collectionsProvider - self.account = account - self.log = log - } - - override func run() { - defer { - didFinish() - } - - let localFolders = account.folders ?? Set() - let collections = collectionsProvider.collections - - feedsAndFolders = collections.compactMap { collection -> ([FeedlyFeed], Folder)? in - let parser = FeedlyCollectionParser(collection: collection) - guard let folder = account.ensureFolder(with: parser.folderName) else { - assertionFailure("Why wasn't a folder created?") - return nil - } - folder.externalID = parser.externalID - return (collection.feeds, folder) - } - - os_log(.debug, log: log, "Ensured %i folders for %i collections.", feedsAndFolders.count, collections.count) - - // Remove folders without a corresponding collection - let collectionFolders = Set(feedsAndFolders.map { $0.1 }) - let foldersWithoutCollections = localFolders.subtracting(collectionFolders) - - if !foldersWithoutCollections.isEmpty { - for unmatched in foldersWithoutCollections { - account.removeFolder(folder: unmatched) - } - - os_log(.debug, log: log, "Removed %i folders: %@", foldersWithoutCollections.count, foldersWithoutCollections.map { $0.externalID ?? $0.nameForDisplay }) - } - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlySyncAllOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlySyncAllOperation.swift deleted file mode 100644 index 85d6647c0..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlySyncAllOperation.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// FeedlySyncAllOperation.swift -// Account -// -// Created by Kiel Gillard on 19/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import SyncDatabase -import Web -import Secrets -import Core -import Feedly - -/// Compose the operations necessary to get the entire set of articles, feeds and folders with the statuses the user expects between now and a certain date in the past. -final class FeedlySyncAllOperation: FeedlyOperation { - - private let operationQueue = MainThreadOperationQueue() - private let log: OSLog - let syncUUID: UUID - - var syncCompletionHandler: ((Result) -> ())? - - /// These requests to Feedly determine which articles to download: - /// 1. The set of all article ids we might need or show. - /// 2. The set of all unread article ids we might need or show (a subset of 1). - /// 3. The set of all article ids changed since the last sync (a subset of 1). - /// 4. The set of all starred article ids. - /// - /// On the response for 1, create statuses for each article id. - /// On the response for 2, create unread statuses for each article id and mark as read those no longer in the response. - /// On the response for 4, create starred statuses for each article id and mark as unstarred those no longer in the response. - /// - /// Download articles for statuses at the union of those statuses without its corresponding article and those included in 3 (changed since last successful sync). - /// - @MainActor init(account: Account, feedlyUserID: String, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIDsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIDsService, getStreamIDsService: FeedlyGetStreamIDsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) { - self.syncUUID = UUID() - self.log = log - self.operationQueue.suspend() - - super.init() - - self.downloadProgress = downloadProgress - - // Send any read/unread/starred article statuses to Feedly before anything else. - let sendArticleStatuses = FeedlySendArticleStatusesOperation(database: database, service: markArticlesService, log: log) - sendArticleStatuses.delegate = self - sendArticleStatuses.downloadProgress = downloadProgress - self.operationQueue.add(sendArticleStatuses) - - // Get all the Collections the user has. - let getCollections = FeedlyGetCollectionsOperation(service: getCollectionsService, log: log) - getCollections.delegate = self - getCollections.downloadProgress = downloadProgress - getCollections.addDependency(sendArticleStatuses) - self.operationQueue.add(getCollections) - - // Ensure a folder exists for each Collection, removing Folders without a corresponding Collection. - let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, log: log) - mirrorCollectionsAsFolders.delegate = self - mirrorCollectionsAsFolders.addDependency(getCollections) - self.operationQueue.add(mirrorCollectionsAsFolders) - - // Ensure feeds are created and grouped by their folders. - let createFeedsOperation = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: mirrorCollectionsAsFolders, log: log) - createFeedsOperation.delegate = self - createFeedsOperation.addDependency(mirrorCollectionsAsFolders) - self.operationQueue.add(createFeedsOperation) - - let getAllArticleIDs = FeedlyIngestStreamArticleIDsOperation(account: account, userID: feedlyUserID, service: getStreamIDsService, log: log) - getAllArticleIDs.delegate = self - getAllArticleIDs.downloadProgress = downloadProgress - getAllArticleIDs.addDependency(createFeedsOperation) - self.operationQueue.add(getAllArticleIDs) - - // Get each page of unread article ids in the global.all stream for the last 31 days (nil = Feedly API default). - let getUnread = FeedlyIngestUnreadArticleIDsOperation(account: account, userID: feedlyUserID, service: getUnreadService, database: database, newerThan: nil, log: log) - getUnread.delegate = self - getUnread.addDependency(getAllArticleIDs) - getUnread.downloadProgress = downloadProgress - self.operationQueue.add(getUnread) - - // Get each page of the article ids which have been update since the last successful fetch start date. - // If the date is nil, this operation provides an empty set (everything is new, nothing is updated). - let getUpdated = FeedlyGetUpdatedArticleIDsOperation(userID: feedlyUserID, service: getStreamIDsService, newerThan: lastSuccessfulFetchStartDate, log: log) - getUpdated.delegate = self - getUpdated.downloadProgress = downloadProgress - getUpdated.addDependency(createFeedsOperation) - self.operationQueue.add(getUpdated) - - // Get each page of the article ids for starred articles. - let getStarred = FeedlyIngestStarredArticleIDsOperation(account: account, userID: feedlyUserID, service: getStarredService, database: database, newerThan: nil, log: log) - getStarred.delegate = self - getStarred.downloadProgress = downloadProgress - getStarred.addDependency(createFeedsOperation) - self.operationQueue.add(getStarred) - - // Now all the possible article ids we need have a status, fetch the article ids for missing articles. - let getMissingIDs = FeedlyFetchIDsForMissingArticlesOperation(account: account) - getMissingIDs.delegate = self - getMissingIDs.downloadProgress = downloadProgress - getMissingIDs.addDependency(getAllArticleIDs) - getMissingIDs.addDependency(getUnread) - getMissingIDs.addDependency(getStarred) - getMissingIDs.addDependency(getUpdated) - self.operationQueue.add(getMissingIDs) - - // Download all the missing and updated articles - let downloadMissingArticles = FeedlyDownloadArticlesOperation(account: account, - missingArticleEntryIDProvider: getMissingIDs, - updatedArticleEntryIDProvider: getUpdated, - getEntriesService: getEntriesService, - log: log) - downloadMissingArticles.delegate = self - downloadMissingArticles.downloadProgress = downloadProgress - downloadMissingArticles.addDependency(getMissingIDs) - downloadMissingArticles.addDependency(getUpdated) - self.operationQueue.add(downloadMissingArticles) - - // Once this operation's dependencies, their dependencies etc finish, we can finish. - let finishOperation = FeedlyCheckpointOperation() - finishOperation.checkpointDelegate = self - finishOperation.downloadProgress = downloadProgress - finishOperation.addDependency(downloadMissingArticles) - self.operationQueue.add(finishOperation) - } - - @MainActor convenience init(account: Account, feedlyUserID: String, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) { - self.init(account: account, feedlyUserID: feedlyUserID, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIDsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log) - } - - override func run() { - os_log(.debug, log: log, "Starting sync %{public}@", syncUUID.uuidString) - operationQueue.resume() - } - - override func didCancel() { - os_log(.debug, log: log, "Cancelling sync %{public}@", syncUUID.uuidString) - self.operationQueue.cancelAllOperations() - syncCompletionHandler = nil - super.didCancel() - } -} - -extension FeedlySyncAllOperation: FeedlyCheckpointOperationDelegate { - - func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { - assert(Thread.isMainThread) - os_log(.debug, log: self.log, "Sync completed: %{public}@", syncUUID.uuidString) - - syncCompletionHandler?(.success(())) - syncCompletionHandler = nil - - didFinish() - } -} - -extension FeedlySyncAllOperation: FeedlyOperationDelegate { - - func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { - assert(Thread.isMainThread) - - // Having this log is useful for debugging missing required JSON keys in the response from Feedly, for example. - os_log(.debug, log: log, "%{public}@ failed with error: %{public}@.", String(describing: operation), error as NSError) - - syncCompletionHandler?(.failure(error)) - syncCompletionHandler = nil - - cancel() - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlySyncStreamContentsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlySyncStreamContentsOperation.swift deleted file mode 100644 index aa67892b7..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlySyncStreamContentsOperation.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// FeedlySyncStreamContentsOperation.swift -// Account -// -// Created by Kiel Gillard on 17/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Parser -import Web -import Secrets -import Core -import Feedly - -final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate { - - private let account: Account - private let resource: FeedlyResourceID - private let operationQueue = MainThreadOperationQueue() - private let service: FeedlyGetStreamContentsService - private let newerThan: Date? - private let isPagingEnabled: Bool - private let log: OSLog - private let finishOperation: FeedlyCheckpointOperation - - @MainActor init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamContentsService, isPagingEnabled: Bool, newerThan: Date?, log: OSLog) { - self.account = account - self.resource = resource - self.service = service - self.isPagingEnabled = isPagingEnabled - self.operationQueue.suspend() - self.newerThan = newerThan - self.log = log - self.finishOperation = FeedlyCheckpointOperation() - - super.init() - - self.operationQueue.add(self.finishOperation) - self.finishOperation.checkpointDelegate = self - enqueueOperations(for: nil) - } - - @MainActor convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamContentsService, newerThan: Date?, log: OSLog) { - let all = FeedlyCategoryResourceID.Global.all(for: credentials.username) - self.init(account: account, resource: all, service: service, isPagingEnabled: true, newerThan: newerThan, log: log) - } - - override func run() { - operationQueue.resume() - } - - override func didCancel() { - os_log(.debug, log: log, "Canceling sync stream contents for %{public}@", resource.id) - operationQueue.cancelAllOperations() - super.didCancel() - } - - @MainActor func enqueueOperations(for continuation: String?) { - os_log(.debug, log: log, "Requesting page for %{public}@", resource.id) - let operations = pageOperations(for: continuation) - operationQueue.addOperations(operations) - } - - func pageOperations(for continuation: String?) -> [MainThreadOperation] { - let getPage = FeedlyGetStreamContentsOperation(resource: resource, - service: service, - continuation: continuation, - newerThan: newerThan, - log: log) - - - let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(parsedItemProvider: getPage, log: log) - - let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: organiseByFeed, log: log) - - getPage.delegate = self - getPage.streamDelegate = self - - organiseByFeed.addDependency(getPage) - organiseByFeed.delegate = self - - updateAccount.addDependency(organiseByFeed) - updateAccount.delegate = self - - finishOperation.addDependency(updateAccount) - - return [getPage, organiseByFeed, updateAccount] - } - - @MainActor func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) { - guard !isCanceled else { - os_log(.debug, log: log, "Cancelled requesting page for %{public}@", resource.id) - return - } - - os_log(.debug, log: log, "Ingesting %i items from %{public}@", stream.items.count, stream.id) - - guard isPagingEnabled, let continuation = stream.continuation else { - os_log(.debug, log: log, "Reached end of stream for %{public}@", stream.id) - return - } - - enqueueOperations(for: continuation) - } - - func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { - os_log(.debug, log: log, "Completed ingesting items from %{public}@", resource.id) - didFinish() - } - - func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { - operationQueue.cancelAllOperations() - didFinish(with: error) - } -} diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift deleted file mode 100644 index 623aa4f7a..000000000 --- a/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// FeedlyUpdateAccountFeedsWithItemsOperation.swift -// Account -// -// Created by Kiel Gillard on 20/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import Parser -import os.log -import Database -import Feedly - -/// Combine the articles with their feeds for a specific account. -final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation { - - private let account: Account - private let organisedItemsProvider: FeedlyParsedItemsByFeedProviding - private let log: OSLog - - init(account: Account, organisedItemsProvider: FeedlyParsedItemsByFeedProviding, log: OSLog) { - - self.account = account - self.organisedItemsProvider = organisedItemsProvider - self.log = log - } - - override func run() { - - let feedIDsAndItems = organisedItemsProvider.parsedItemsKeyedByFeedID - - Task { @MainActor in - do { - - try await account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) - os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", feedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName) - self.didFinish() - - } catch { - self.didFinish(with: error) - } - } - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift deleted file mode 100644 index a92dc3b64..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlyGetCollectionsOperation.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// FeedlyGetCollectionsOperation.swift -// Account -// -// Created by Kiel Gillard on 19/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log - -public protocol FeedlyCollectionProviding: AnyObject { - - @MainActor var collections: [FeedlyCollection] { get } -} - -/// Get Collections from Feedly. -public final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding { - - let service: FeedlyGetCollectionsService - let log: OSLog - - private(set) public var collections = [FeedlyCollection]() - - public init(service: FeedlyGetCollectionsService, log: OSLog) { - self.service = service - self.log = log - } - - public override func run() { - - Task { @MainActor in - os_log(.debug, log: log, "Requesting collections.") - - do { - let collections = try await service.getCollections() - os_log(.debug, log: self.log, "Received collections: %{public}@", collections.map { $0.id }) - self.collections = Array(collections) - self.didFinish() - - } catch { - os_log(.debug, log: self.log, "Unable to request collections: %{public}@.", error as NSError) - self.didFinish(with: error) - } - } - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlyGetEntriesOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyGetEntriesOperation.swift deleted file mode 100644 index 64e5ced70..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlyGetEntriesOperation.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// FeedlyGetEntriesOperation.swift -// Account -// -// Created by Kiel Gillard on 28/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Parser - -/// Get full entries for the entry identifiers. -public final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding { - - let service: FeedlyGetEntriesService - let provider: FeedlyEntryIdentifierProviding - let log: OSLog - - public init(service: FeedlyGetEntriesService, provider: FeedlyEntryIdentifierProviding, log: OSLog) { - self.service = service - self.provider = provider - self.log = log - } - - private (set) public var entries = [FeedlyEntry]() - - private var storedParsedEntries: Set? - - public var parsedEntries: Set { - if let entries = storedParsedEntries { - return entries - } - - let parsed = Set(entries.compactMap { - FeedlyEntryParser(entry: $0).parsedItemRepresentation - }) - - // TODO: Fix the below. There’s an error on the os.log line: "Expression type '()' is ambiguous without more context" -// if parsed.count != entries.count { -// let entryIDs = Set(entries.map { $0.id }) -// let parsedIDs = Set(parsed.map { $0.uniqueID }) -// let difference = entryIDs.subtracting(parsedIDs) -// os_log(.debug, log: log, "%{public}@ dropping articles with ids: %{public}@.", self, difference) -// } - - storedParsedEntries = parsed - - return parsed - } - - public var parsedItemProviderName: String { - return name ?? String(describing: Self.self) - } - - public override func run() { - - Task { @MainActor in - - do { - let entries = try await service.getEntries(for: provider.entryIDs) - self.entries = entries - self.didFinish() - } catch { - os_log(.debug, log: self.log, "Unable to get entries: %{public}@.", error as NSError) - self.didFinish(with: error) - } - } - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlyGetStreamContentsOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyGetStreamContentsOperation.swift deleted file mode 100644 index c3a1f3854..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlyGetStreamContentsOperation.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// FeedlyGetStreamOperation.swift -// Account -// -// Created by Kiel Gillard on 20/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import Parser -import os.log - -public protocol FeedlyEntryProviding { - @MainActor var entries: [FeedlyEntry] { get } -} - -public protocol FeedlyParsedItemProviding { - @MainActor var parsedItemProviderName: String { get } - @MainActor var parsedEntries: Set { get } -} - -public protocol FeedlyGetStreamContentsOperationDelegate: AnyObject { - func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) -} - -/// Get the stream content of a Collection from Feedly. -public final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding { - - @MainActor struct ResourceProvider: FeedlyResourceProviding { - var resource: FeedlyResourceID - } - - let resourceProvider: FeedlyResourceProviding - - public var parsedItemProviderName: String { - return resourceProvider.resource.id - } - - public var entries: [FeedlyEntry] { - guard let entries = stream?.items else { -// assert(isFinished, "This should only be called when the operation finishes without error.") - assertionFailure("Has this operation been addeded as a dependency on the caller?") - return [] - } - return entries - } - - public var parsedEntries: Set { - if let entries = storedParsedEntries { - return entries - } - - let parsed = Set(entries.compactMap { - FeedlyEntryParser(entry: $0).parsedItemRepresentation - }) - - if parsed.count != entries.count { - let entryIDs = Set(entries.map { $0.id }) - let parsedIDs = Set(parsed.map { $0.uniqueID }) - let difference = entryIDs.subtracting(parsedIDs) - os_log(.debug, log: log, "Dropping articles with ids: %{public}@.", difference) - } - - storedParsedEntries = parsed - - return parsed - } - - private(set) var stream: FeedlyStream? { - didSet { - storedParsedEntries = nil - } - } - - private var storedParsedEntries: Set? - - let service: FeedlyGetStreamContentsService - let unreadOnly: Bool? - let newerThan: Date? - let continuation: String? - let log: OSLog - - public weak var streamDelegate: FeedlyGetStreamContentsOperationDelegate? - - public init(resource: FeedlyResourceID, service: FeedlyGetStreamContentsService, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) { - - self.resourceProvider = ResourceProvider(resource: resource) - self.service = service - self.continuation = continuation - self.unreadOnly = unreadOnly - self.newerThan = newerThan - self.log = log - } - - convenience init(resourceProvider: FeedlyResourceProviding, service: FeedlyGetStreamContentsService, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) { - - self.init(resource: resourceProvider.resource, service: service, newerThan: newerThan, unreadOnly: unreadOnly, log: log) - } - - public override func run() { - - Task { @MainActor in - - do { - let stream = try await service.getStreamContents(for: resourceProvider.resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) - - self.stream = stream - self.streamDelegate?.feedlyGetStreamContentsOperation(self, didGetContentsOf: stream) - self.didFinish() - - } catch { - os_log(.debug, log: self.log, "Unable to get stream contents: %{public}@.", error as NSError) - self.didFinish(with: error) - } - } - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlyGetUpdatedArticleIDsOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyGetUpdatedArticleIDsOperation.swift deleted file mode 100644 index 0fc52ab60..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlyGetUpdatedArticleIDsOperation.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// FeedlyGetUpdatedArticleIDsOperation.swift -// Account -// -// Created by Kiel Gillard on 11/1/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log -import Secrets - -/// Single responsibility is to identify articles that have changed since a particular date. -/// -/// Typically, it pages through the article ids of the global.all stream. -/// When all the article ids are collected, it is the responsibility of another operation to download them when appropriate. -public final class FeedlyGetUpdatedArticleIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding { - - private let resource: FeedlyResourceID - private let service: FeedlyGetStreamIDsService - private let newerThan: Date? - private let log: OSLog - - public init(resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) { - - self.resource = resource - self.service = service - self.newerThan = newerThan - self.log = log - } - - public convenience init(userID: String, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) { - let all = FeedlyCategoryResourceID.Global.all(for: userID) - self.init(resource: all, service: service, newerThan: newerThan, log: log) - } - - public var entryIDs: Set { - return storedUpdatedArticleIDs - } - - private var storedUpdatedArticleIDs = Set() - - public override func run() { - getStreamIDs(nil) - } - - private func getStreamIDs(_ continuation: String?) { - - Task { @MainActor in - guard let date = newerThan else { - os_log(.debug, log: log, "No date provided so everything must be new (nothing is updated).") - didFinish() - return - } - - do { - let streamIDs = try await service.getStreamIDs(for: resource, continuation: continuation, newerThan: date, unreadOnly: nil) - - storedUpdatedArticleIDs.formUnion(streamIDs.ids) - guard let continuation = streamIDs.continuation else { - os_log(.debug, log: log, "%{public}i articles updated since last successful sync start date.", storedUpdatedArticleIDs.count) - didFinish() - return - } - - getStreamIDs(continuation) - - } catch { - didFinish(with: error) - } - } - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlyLogoutOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyLogoutOperation.swift deleted file mode 100644 index 78336014a..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlyLogoutOperation.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// FeedlyLogoutOperation.swift -// Account -// -// Created by Kiel Gillard on 15/11/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import os.log - -public protocol FeedlyLogoutService { - - @MainActor func logout() async throws -} - -public final class FeedlyLogoutOperation: FeedlyOperation { - - let service: FeedlyLogoutService - let log: OSLog - - public init(service: FeedlyLogoutService, log: OSLog) { - self.service = service - self.log = log - } - - public override func run() { - - Task { @MainActor in - - do { - os_log("Requesting logout of Feedly account.") - try await service.logout() - os_log("Logged out of Feedly account.") - - // TODO: fix removing credentials -// try account.removeCredentials(type: .oauthAccessToken) -// try account.removeCredentials(type: .oauthRefreshToken) - - didFinish() - - } catch { - os_log("Logout failed because %{public}@.", error as NSError) - didFinish(with: error) - } - } - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlyOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyOperation.swift deleted file mode 100644 index 8f809aab5..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlyOperation.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// FeedlyOperation.swift -// Account -// -// Created by Kiel Gillard on 20/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import Web -import Core - -public protocol FeedlyOperationDelegate: AnyObject { - @MainActor func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) -} - -/// Abstract base class for Feedly sync operations. -/// -/// Normally we don’t do inheritance — but in this case -/// it’s the best option. -@MainActor open class FeedlyOperation: MainThreadOperation { - - public weak var delegate: FeedlyOperationDelegate? - public var downloadProgress: DownloadProgress? { - didSet { - oldValue?.completeTask() - downloadProgress?.addToNumberOfTasksAndRemaining(1) - } - } - - // MainThreadOperation - public var isCanceled = false { - didSet { - if isCanceled { - didCancel() - } - } - } - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - public init() {} - - open func run() { - } - - open func didFinish() { - if !isCanceled { - operationDelegate?.operationDidComplete(self) - } - downloadProgress?.completeTask() - } - - open func didFinish(with error: Error) { - delegate?.feedlyOperation(self, didFailWith: error) - didFinish() - } - - open func didCancel() { - didFinish() - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift deleted file mode 100644 index d9e6442d1..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// FeedlyOrganiseParsedItemsByFeedOperation.swift -// Account -// -// Created by Kiel Gillard on 20/9/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import Parser -import os.log - -public protocol FeedlyParsedItemsByFeedProviding { - - @MainActor var parsedItemsByFeedProviderName: String { get } - @MainActor var parsedItemsKeyedByFeedID: [String: Set] { get } -} - -/// Group articles by their feeds. -public final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyParsedItemsByFeedProviding { - - private let parsedItemProvider: FeedlyParsedItemProviding - private let log: OSLog - - public var parsedItemsByFeedProviderName: String { - return name ?? String(describing: Self.self) - } - - public var parsedItemsKeyedByFeedID: [String : Set] { - precondition(Thread.isMainThread) // Needs to be on main thread because Feed is a main-thread-only model type. - return itemsKeyedByFeedID - } - - private var itemsKeyedByFeedID = [String: Set]() - - public init(parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) { - - self.parsedItemProvider = parsedItemProvider - self.log = log - } - - public override func run() { - defer { - didFinish() - } - - let items = parsedItemProvider.parsedEntries - var dict = [String: Set](minimumCapacity: items.count) - - for item in items { - let key = item.feedURL - let value: Set = { - if var items = dict[key] { - items.insert(item) - return items - } else { - return [item] - } - }() - dict[key] = value - } - - os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.parsedItemProviderName) - - itemsKeyedByFeedID = dict - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlySearchOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlySearchOperation.swift deleted file mode 100644 index 6de1e861a..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlySearchOperation.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// FeedlySearchOperation.swift -// Account -// -// Created by Kiel Gillard on 1/12/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -public protocol FeedlySearchService: AnyObject { - - @MainActor func getFeeds(for query: String, count: Int, localeIdentifier: String) async throws -> FeedlyFeedsSearchResponse -} - -public protocol FeedlySearchOperationDelegate: AnyObject { - - @MainActor func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse) -} - -/// Find one and only one feed for a given query (usually, a URL). -/// What happens when a feed is found for the URL is delegated to the `searchDelegate`. -public final class FeedlySearchOperation: FeedlyOperation { - - let query: String - let locale: Locale - let searchService: FeedlySearchService - public weak var searchDelegate: FeedlySearchOperationDelegate? - - public init(query: String, locale: Locale = .current, service: FeedlySearchService) { - self.query = query - self.locale = locale - self.searchService = service - } - - public override func run() { - - Task { @MainActor in - - do { - let searchResponse = try await searchService.getFeeds(for: query, count: 1, localeIdentifier: locale.identifier) - self.searchDelegate?.feedlySearchOperation(self, didGet: searchResponse) - self.didFinish() - - } catch { - self.didFinish(with: error) - } - } - } -} diff --git a/Feedly/Sources/Feedly/Operations/FeedlySendArticleStatusesOperation.swift b/Feedly/Sources/Feedly/Operations/FeedlySendArticleStatusesOperation.swift deleted file mode 100644 index bb1b5320f..000000000 --- a/Feedly/Sources/Feedly/Operations/FeedlySendArticleStatusesOperation.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// FeedlySendArticleStatusesOperation.swift -// Account -// -// Created by Kiel Gillard on 14/10/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import Articles -import SyncDatabase -import os.log - -/// Take changes to statuses of articles locally and apply them to the corresponding the articles remotely. -public final class FeedlySendArticleStatusesOperation: FeedlyOperation { - - private let database: SyncDatabase - private let log: OSLog - private let service: FeedlyMarkArticlesService - - public init(database: SyncDatabase, service: FeedlyMarkArticlesService, log: OSLog) { - self.database = database - self.service = service - self.log = log - } - - public override func run() { - os_log(.debug, log: log, "Sending article statuses...") - - Task { @MainActor in - - do { - let syncStatuses = (try await self.database.selectForProcessing()) ?? Set() - self.processStatuses(Array(syncStatuses)) - } catch { - self.didFinish() - } - } - } -} - -private extension FeedlySendArticleStatusesOperation { - - func processStatuses(_ pending: [SyncStatus]) { - - let statuses: [(status: SyncStatus.Key, flag: Bool, action: FeedlyMarkAction)] = [ - (.read, false, .unread), - (.read, true, .read), - (.starred, true, .saved), - (.starred, false, .unsaved), - ] - - Task { @MainActor in - - for pairing in statuses { - - let articleIDs = pending.filter { $0.key == pairing.status && $0.flag == pairing.flag } - guard !articleIDs.isEmpty else { - continue - } - - let ids = Set(articleIDs.map { $0.articleID }) - - do { - try await service.mark(ids, as: pairing.action) - try? await database.deleteSelectedForProcessing(Array(ids)) - } catch { - try? await database.resetSelectedForProcessing(Array(ids)) - } - } - - os_log(.debug, log: self.log, "Done sending article statuses.") - self.didFinish() - } - } -}